diff --git a/Makefile b/Makefile index d10ff7ab85..c6779758c7 100644 --- a/Makefile +++ b/Makefile @@ -157,7 +157,16 @@ local-build: IMAGE_TAG = local local-build: image .PHONY: run-local -run-local: local-build kind-create deploy +run-local: local-build kind-create deploy tmp-certs + +.PHONY: tmp-certs +tmp-certs: + kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.18.2/cert-manager.yaml + kubectl wait --for=condition=Available --namespace=cert-manager deployment/cert-manager-webhook --timeout=60s + kubectl wait --for=condition=Available --namespace=cert-manager deployment/cert-manager-cainjector --timeout=60s + kubectl wait --for=condition=Available --namespace=cert-manager deployment/cert-manager --timeout=60s + kubectl apply -f issuer.yaml + kubectl apply -f certificate.yaml .PHONY: clean clean: #HELP Clean up build artifacts @@ -233,14 +242,19 @@ deploy: $(KIND) $(HELM) #HELP Deploy OLM to kind cluster $KIND_CLUSTER_NAME (def --set debug=true \ --set olm.image.ref=$(OLM_IMAGE) \ --set olm.image.pullPolicy=IfNotPresent \ + --set olm.tlsSecret=olm-cert \ + --set olm.clientCASecret=olm-cert \ + --set olm.service.internalPort=8443 \ --set catalog.image.ref=$(OLM_IMAGE) \ --set catalog.image.pullPolicy=IfNotPresent \ + --set catalog.tlsSecret=olm-cert \ + --set catalog.clientCASecret=olm-cert \ + --set catalog.service.internalPort=8443 \ --set catalog.commandArgs=--configmapServerImage=$(CONFIGMAP_SERVER_IMAGE) \ --set catalog.opmImageArgs=--opmImage=$(OPERATOR_REGISTRY_IMAGE) \ --set package.image.ref=$(OLM_IMAGE) \ --set package.image.pullPolicy=IfNotPresent \ - $(HELM_INSTALL_OPTS) \ - --wait; + $(HELM_INSTALL_OPTS); .PHONY: undeploy undeploy: $(KIND) $(HELM) #HELP Uninstall OLM from kind cluster $KIND_CLUSTER_NAME (default: kind-olmv0) diff --git a/certificate.yaml b/certificate.yaml new file mode 100644 index 0000000000..8a7656379e --- /dev/null +++ b/certificate.yaml @@ -0,0 +1,22 @@ +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: olm + namespace: operator-lifecycle-manager +spec: + secretName: olm-cert + isCA: false + usages: + - server auth + dnsNames: + - localhost + - catalog-operator.operator-lifecycle-manager.svc + - olm-operator.operator-lifecycle-manager.svc + issuerRef: + name: issuer + # We can reference ClusterIssuers by changing the kind here. + # The default value is Issuer (i.e. a locally namespaced Issuer) + kind: Issuer + # This is optional since cert-manager will default to this value however + # if you are using an external issuer, change this to that issuer group. + group: cert-manager.io diff --git a/cmd/catalog/main.go b/cmd/catalog/main.go index b82f1689cb..4092bf8922 100644 --- a/cmd/catalog/main.go +++ b/cmd/catalog/main.go @@ -57,9 +57,16 @@ func (o *options) run(ctx context.Context, logger *logrus.Logger) error { o.catalogNamespace = catalogNamespaceEnvVarValue } + // create a config client for operator status + config, err := clientcmd.BuildConfigFromFlags("", o.kubeconfig) + if err != nil { + return fmt.Errorf("error configuring client: %s", err.Error()) + } + listenAndServe, err := server.GetListenAndServeFunc( server.WithLogger(logger), server.WithTLS(&o.tlsCertPath, &o.tlsKeyPath, &o.clientCAPath), + server.WithKubeConfig(config), server.WithDebug(o.debug), ) if err != nil { @@ -72,11 +79,6 @@ func (o *options) run(ctx context.Context, logger *logrus.Logger) error { } }() - // create a config client for operator status - config, err := clientcmd.BuildConfigFromFlags("", o.kubeconfig) - if err != nil { - return fmt.Errorf("error configuring client: %s", err.Error()) - } configClient, err := configv1client.NewForConfig(config) if err != nil { return fmt.Errorf("error configuring client: %s", err.Error()) diff --git a/cmd/olm/main.go b/cmd/olm/main.go index 715ae9aea0..10dab00f8c 100644 --- a/cmd/olm/main.go +++ b/cmd/olm/main.go @@ -123,7 +123,18 @@ func main() { } logger.Infof("log level %s", logger.Level) - listenAndServe, err := server.GetListenAndServeFunc(server.WithLogger(logger), server.WithTLS(tlsCertPath, tlsKeyPath, clientCAPath), server.WithDebug(*debug)) + mgr, err := Manager(ctx, *debug) + if err != nil { + logger.WithError(err).Fatal("error configuring controller manager") + } + config := mgr.GetConfig() + + listenAndServe, err := server.GetListenAndServeFunc( + server.WithLogger(logger), + server.WithTLS(tlsCertPath, tlsKeyPath, clientCAPath), + server.WithKubeConfig(config), + server.WithDebug(*debug), + ) if err != nil { logger.Fatalf("Error setting up health/metric/pprof service: %v", err) } @@ -134,12 +145,6 @@ func main() { } }() - mgr, err := Manager(ctx, *debug) - if err != nil { - logger.WithError(err).Fatal("error configuring controller manager") - } - config := mgr.GetConfig() - // create a config that validates we're creating objects with labels validatingConfig := validatingroundtripper.Wrap(config, mgr.GetScheme()) diff --git a/issuer.yaml b/issuer.yaml new file mode 100644 index 0000000000..5f9bcd3134 --- /dev/null +++ b/issuer.yaml @@ -0,0 +1,8 @@ +--- +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + name: issuer + namespace: operator-lifecycle-manager +spec: + selfSigned: {} diff --git a/pkg/lib/server/server.go b/pkg/lib/server/server.go index 3d79a192e0..5651a9c1cf 100644 --- a/pkg/lib/server/server.go +++ b/pkg/lib/server/server.go @@ -6,11 +6,15 @@ import ( "fmt" "net/http" "path/filepath" + "time" "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/filemonitor" "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/profile" "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/sirupsen/logrus" + "k8s.io/client-go/rest" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/metrics/filters" ) // Option applies a configuration option to the given config. @@ -43,11 +47,18 @@ func WithDebug(debug bool) Option { } } +func WithKubeConfig(config *rest.Config) Option { + return func(sc *serverConfig) { + sc.kubeConfig = config + } +} + type serverConfig struct { logger *logrus.Logger tlsCertPath *string tlsKeyPath *string clientCAPath *string + kubeConfig *rest.Config debug bool } @@ -62,6 +73,7 @@ func defaultServerConfig() serverConfig { tlsCertPath: nil, tlsKeyPath: nil, clientCAPath: nil, + kubeConfig: nil, logger: nil, debug: false, } @@ -90,12 +102,53 @@ func (sc serverConfig) getListenAndServeFunc() (func() error, error) { } mux := http.NewServeMux() - mux.Handle("/metrics", promhttp.Handler()) mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) }) profile.RegisterHandlers(mux, profile.WithTLS(tlsEnabled || !sc.debug)) + // Set up authenticated metrics endpoint if kubeConfig is provided + sc.logger.Infof("DEBUG: Checking authentication setup - kubeConfig != nil: %v, tlsEnabled: %v", sc.kubeConfig != nil, tlsEnabled) + if sc.kubeConfig != nil && tlsEnabled { + sc.logger.Info("DEBUG: Setting up authenticated metrics endpoint") + // Create authentication filter using controller-runtime + sc.logger.Info("DEBUG: Creating authentication filter with controller-runtime") + filter, err := filters.WithAuthenticationAndAuthorization(sc.kubeConfig, &http.Client{ + Timeout: 30 * time.Second, + }) + if err != nil { + sc.logger.Errorf("DEBUG: Failed to create authentication filter: %v", err) + return nil, fmt.Errorf("failed to create authentication filter: %w", err) + } + sc.logger.Info("DEBUG: Authentication filter created successfully") + // Create authenticated metrics handler + sc.logger.Info("DEBUG: Wrapping metrics handler with authentication") + logger := log.FromContext(context.Background()) + authenticatedMetricsHandler, err := filter(logger, promhttp.Handler()) + if err != nil { + sc.logger.Errorf("DEBUG: Failed to wrap metrics handler: %v", err) + return nil, fmt.Errorf("failed to wrap metrics handler with authentication: %w", err) + } + sc.logger.Info("DEBUG: Metrics handler wrapped successfully") + // Add debugging wrapper to log authentication attempts + debugAuthHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + sc.logger.Infof("DEBUG: Metrics request from %s, Auth header present: %v, User-Agent: %s", + r.RemoteAddr, r.Header.Get("Authorization") != "", r.Header.Get("User-Agent")) + authenticatedMetricsHandler.ServeHTTP(w, r) + }) + mux.Handle("/metrics", debugAuthHandler) + sc.logger.Info("Metrics endpoint configured with authentication and authorization") + } else { + // Fallback to unprotected metrics (for development/testing) + sc.logger.Warnf("DEBUG: Using unprotected metrics - kubeConfig != nil: %v, tlsEnabled: %v", sc.kubeConfig != nil, tlsEnabled) + mux.Handle("/metrics", promhttp.Handler()) + if sc.kubeConfig == nil { + sc.logger.Warn("No Kubernetes config provided - metrics endpoint will be unprotected") + } else if !tlsEnabled { + sc.logger.Warn("TLS not enabled - metrics endpoint will be unprotected") + } + } + s := http.Server{ Handler: mux, Addr: sc.getAddress(tlsEnabled), @@ -141,6 +194,7 @@ func (sc serverConfig) getListenAndServeFunc() (func() error, error) { ClientAuth: tls.VerifyClientCertIfGiven, }, nil }, + NextProtos: []string{"http/1.1"}, // Disable HTTP/2 for security } return func() error { return s.ListenAndServeTLS("", "") diff --git a/vendor/modules.txt b/vendor/modules.txt index b4eeeecbd0..9b0cd4528e 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1873,6 +1873,7 @@ sigs.k8s.io/controller-runtime/pkg/log/zap sigs.k8s.io/controller-runtime/pkg/manager sigs.k8s.io/controller-runtime/pkg/manager/signals sigs.k8s.io/controller-runtime/pkg/metrics +sigs.k8s.io/controller-runtime/pkg/metrics/filters sigs.k8s.io/controller-runtime/pkg/metrics/server sigs.k8s.io/controller-runtime/pkg/predicate sigs.k8s.io/controller-runtime/pkg/reconcile diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/metrics/filters/filters.go b/vendor/sigs.k8s.io/controller-runtime/pkg/metrics/filters/filters.go new file mode 100644 index 0000000000..1659502bcf --- /dev/null +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/metrics/filters/filters.go @@ -0,0 +1,122 @@ +package filters + +import ( + "fmt" + "net/http" + "strings" + "time" + + "github.com/go-logr/logr" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/apiserver/pkg/apis/apiserver" + "k8s.io/apiserver/pkg/authentication/authenticatorfactory" + "k8s.io/apiserver/pkg/authorization/authorizer" + "k8s.io/apiserver/pkg/authorization/authorizerfactory" + authenticationv1 "k8s.io/client-go/kubernetes/typed/authentication/v1" + authorizationv1 "k8s.io/client-go/kubernetes/typed/authorization/v1" + "k8s.io/client-go/rest" + + metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" +) + +// WithAuthenticationAndAuthorization provides a metrics.Filter for authentication and authorization. +// Metrics will be authenticated (via TokenReviews) and authorized (via SubjectAccessReviews) with the +// kube-apiserver. +// For the authentication and authorization the controller needs a ClusterRole +// with the following rules: +// * apiGroups: authentication.k8s.io, resources: tokenreviews, verbs: create +// * apiGroups: authorization.k8s.io, resources: subjectaccessreviews, verbs: create +// +// To scrape metrics e.g. via Prometheus the client needs a ClusterRole +// with the following rule: +// * nonResourceURLs: "/metrics", verbs: get +// +// Note: Please note that configuring this metrics provider will introduce a dependency to "k8s.io/apiserver" +// to your go module. +func WithAuthenticationAndAuthorization(config *rest.Config, httpClient *http.Client) (metricsserver.Filter, error) { + authenticationV1Client, err := authenticationv1.NewForConfigAndClient(config, httpClient) + if err != nil { + return nil, err + } + authorizationV1Client, err := authorizationv1.NewForConfigAndClient(config, httpClient) + if err != nil { + return nil, err + } + + authenticatorConfig := authenticatorfactory.DelegatingAuthenticatorConfig{ + Anonymous: &apiserver.AnonymousAuthConfig{Enabled: false}, // Require authentication. + CacheTTL: 1 * time.Minute, + TokenAccessReviewClient: authenticationV1Client, + TokenAccessReviewTimeout: 10 * time.Second, + // wait.Backoff is copied from: https://github.com/kubernetes/apiserver/blob/v0.29.0/pkg/server/options/authentication.go#L43-L50 + // options.DefaultAuthWebhookRetryBackoff is not used to avoid a dependency on "k8s.io/apiserver/pkg/server/options". + WebhookRetryBackoff: &wait.Backoff{ + Duration: 500 * time.Millisecond, + Factor: 1.5, + Jitter: 0.2, + Steps: 5, + }, + } + delegatingAuthenticator, _, err := authenticatorConfig.New() + if err != nil { + return nil, fmt.Errorf("failed to create authenticator: %w", err) + } + + authorizerConfig := authorizerfactory.DelegatingAuthorizerConfig{ + SubjectAccessReviewClient: authorizationV1Client, + AllowCacheTTL: 5 * time.Minute, + DenyCacheTTL: 30 * time.Second, + // wait.Backoff is copied from: https://github.com/kubernetes/apiserver/blob/v0.29.0/pkg/server/options/authentication.go#L43-L50 + // options.DefaultAuthWebhookRetryBackoff is not used to avoid a dependency on "k8s.io/apiserver/pkg/server/options". + WebhookRetryBackoff: &wait.Backoff{ + Duration: 500 * time.Millisecond, + Factor: 1.5, + Jitter: 0.2, + Steps: 5, + }, + } + delegatingAuthorizer, err := authorizerConfig.New() + if err != nil { + return nil, fmt.Errorf("failed to create authorizer: %w", err) + } + + return func(log logr.Logger, handler http.Handler) (http.Handler, error) { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + ctx := req.Context() + + res, ok, err := delegatingAuthenticator.AuthenticateRequest(req) + if err != nil { + log.Error(err, "Authentication failed") + http.Error(w, "Authentication failed", http.StatusInternalServerError) + return + } + if !ok { + log.V(4).Info("Authentication failed") + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + + attributes := authorizer.AttributesRecord{ + User: res.User, + Verb: strings.ToLower(req.Method), + Path: req.URL.Path, + } + + authorized, reason, err := delegatingAuthorizer.Authorize(ctx, attributes) + if err != nil { + msg := fmt.Sprintf("Authorization for user %s failed", attributes.User.GetName()) + log.Error(err, msg) + http.Error(w, msg, http.StatusInternalServerError) + return + } + if authorized != authorizer.DecisionAllow { + msg := fmt.Sprintf("Authorization denied for user %s", attributes.User.GetName()) + log.V(4).Info(fmt.Sprintf("%s: %s", msg, reason)) + http.Error(w, msg, http.StatusForbidden) + return + } + + handler.ServeHTTP(w, req) + }), nil + }, nil +}