diff --git a/bundle-hack/container_digest.sh b/bundle-hack/container_digest.sh index 7e44d2504..bab0806f1 100644 --- a/bundle-hack/container_digest.sh +++ b/bundle-hack/container_digest.sh @@ -3,7 +3,3 @@ export OPERATOR_IMAGE_PULLSPEC='registry.redhat.io/albo/aws-load-balancer-rhel9-operator@sha256:369c8c618d251106127fc181a7442726f948d3c9e6b9bef761f4f26936fbb179' # Controller export OPERAND_IMAGE_PULLSPEC='registry.redhat.io/albo/aws-load-balancer-controller-rhel9@sha256:e416e2d857b3b2481f102611b70f5e2df71463fe7ea03bf3cc57cc9985c41552' -# kube-rbac-proxy -# Latest version of v4.19 tag is used. -# Catalog link (health grade A): https://catalog.redhat.com/en/software/containers/openshift4/ose-kube-rbac-proxy-rhel9/652809a5244cb343fb4a4b66?image=6a291e91c7ee40ca259b3f3a -export KUBE_RBAC_PROXY_IMAGE_PULLSPEC='registry.redhat.io/openshift4/ose-kube-rbac-proxy-rhel9@sha256:32540431240e12c07d35f9f390b196aae5cc2188e9db6365e41e6bbe7070d8c2' diff --git a/bundle-hack/update_bundle.sh b/bundle-hack/update_bundle.sh index 070fee3e2..c966e78f6 100755 --- a/bundle-hack/update_bundle.sh +++ b/bundle-hack/update_bundle.sh @@ -15,7 +15,6 @@ source ./bundle_vars.sh # Check for environment variables pertaining to the bundle if [ -z "${OPERATOR_IMAGE_PULLSPEC}" ] || [ -z "${OPERAND_IMAGE_PULLSPEC}" ] || - [ -z "${KUBE_RBAC_PROXY_IMAGE_PULLSPEC}" ] || [ -z "${MANIFESTS_DIR}" ] || [ -z "${METADATA_DIR}" ] || [ -z "${SUPPORTED_OCP_VERSIONS}" ] || @@ -23,7 +22,6 @@ if [ -z "${OPERATOR_IMAGE_PULLSPEC}" ] || echo "ERROR: Not all required environment variables are set" echo " OPERATOR_IMAGE_PULLSPEC" echo " OPERAND_IMAGE_PULLSPEC" - echo " KUBE_RBAC_PROXY_IMAGE_PULLSPEC" echo " MANIFESTS_DIR" echo " METADATA_DIR" echo " SUPPORTED_OCP_VERSIONS" @@ -37,9 +35,7 @@ CSV_FILE=${MANIFESTS_DIR}/aws-load-balancer-operator.clusterserviceversion.yaml sed -i -e "s|openshift.io/aws-load-balancer-operator:latest|${OPERATOR_IMAGE_PULLSPEC}|g" \ -e "s|docker.io/amazon/aws-alb-ingress-controller:.*$|${OPERAND_IMAGE_PULLSPEC}|g" \ -e "s|quay.io/aws-load-balancer-operator/aws-load-balancer-controller:.*$|${OPERAND_IMAGE_PULLSPEC}|g" \ - -e "s|quay.io/aws-load-balancer-operator/aws-load-balancer-controller@.*$|${OPERAND_IMAGE_PULLSPEC}|g" \ - -e "s|gcr.io/kubebuilder/kube-rbac-proxy:.*$|${KUBE_RBAC_PROXY_IMAGE_PULLSPEC}|g" \ - -e "s|quay.io/openshift/origin-kube-rbac-proxy:.*$|${KUBE_RBAC_PROXY_IMAGE_PULLSPEC}|g" "${CSV_FILE}" + -e "s|quay.io/aws-load-balancer-operator/aws-load-balancer-controller@.*$|${OPERAND_IMAGE_PULLSPEC}|g" "${CSV_FILE}" export EPOC_TIMESTAMP=$(date +%s) export TARGET_CSV_FILE="${CSV_FILE}" @@ -71,7 +67,6 @@ version = os.getenv('VERSION') replaces = os.getenv('REPLACES_VERSION') operator_pullspec = os.getenv('OPERATOR_IMAGE_PULLSPEC', '') operand_pullspec = os.getenv('OPERAND_IMAGE_PULLSPEC', '') -kube_rbac_proxy_pullspec = os.getenv('KUBE_RBAC_PROXY_IMAGE_PULLSPEC', '') csv = load_manifest(os.getenv('TARGET_CSV_FILE')) # Update metadata @@ -96,7 +91,6 @@ operator_sha = operator_pullspec.split('@sha256:')[1] annotation_image_name = f'aws-load-balancer-rhel9-operator-{operator_sha}-annotation' csv['spec']['relatedImages'] = [ {'name': annotation_image_name, 'image': operator_pullspec}, - {'name': 'kube-rbac-proxy', 'image': kube_rbac_proxy_pullspec}, {'name': 'manager', 'image': operator_pullspec}, {'name': 'controller', 'image': operand_pullspec} ] diff --git a/bundle/manifests/aws-load-balancer-operator.clusterserviceversion.yaml b/bundle/manifests/aws-load-balancer-operator.clusterserviceversion.yaml index 14b32a268..37dd12eb2 100644 --- a/bundle/manifests/aws-load-balancer-operator.clusterserviceversion.yaml +++ b/bundle/manifests/aws-load-balancer-operator.clusterserviceversion.yaml @@ -69,7 +69,7 @@ metadata: features.operators.openshift.io/disconnected: "false" features.operators.openshift.io/fips-compliant: "true" features.operators.openshift.io/proxy-aware: "true" - features.operators.openshift.io/tls-profiles: "false" + features.operators.openshift.io/tls-profiles: "true" features.operators.openshift.io/token-auth-aws: "true" features.operators.openshift.io/token-auth-azure: "false" features.operators.openshift.io/token-auth-gcp: "false" @@ -159,6 +159,7 @@ spec: - apiGroups: - config.openshift.io resources: + - apiservers - infrastructures verbs: - get diff --git a/config/manifests/bases/aws-load-balancer-operator.clusterserviceversion.yaml b/config/manifests/bases/aws-load-balancer-operator.clusterserviceversion.yaml index 8a94f0b21..a676fd3a7 100644 --- a/config/manifests/bases/aws-load-balancer-operator.clusterserviceversion.yaml +++ b/config/manifests/bases/aws-load-balancer-operator.clusterserviceversion.yaml @@ -7,7 +7,7 @@ metadata: features.operators.openshift.io/disconnected: "false" features.operators.openshift.io/fips-compliant: "true" features.operators.openshift.io/proxy-aware: "true" - features.operators.openshift.io/tls-profiles: "false" + features.operators.openshift.io/tls-profiles: "true" features.operators.openshift.io/token-auth-aws: "true" features.operators.openshift.io/token-auth-azure: "false" features.operators.openshift.io/token-auth-gcp: "false" diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index b785e8697..a875e7241 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -34,6 +34,7 @@ rules: - apiGroups: - config.openshift.io resources: + - apiservers - infrastructures verbs: - get diff --git a/main.go b/main.go index 08a4a41ef..b50c839d2 100644 --- a/main.go +++ b/main.go @@ -22,6 +22,7 @@ import ( "flag" "fmt" "os" + "reflect" "time" // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) @@ -44,10 +45,12 @@ import ( ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/cache" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" "sigs.k8s.io/controller-runtime/pkg/healthz" "sigs.k8s.io/controller-runtime/pkg/log/zap" "sigs.k8s.io/controller-runtime/pkg/metrics/filters" metrics "sigs.k8s.io/controller-runtime/pkg/metrics/server" + "sigs.k8s.io/controller-runtime/pkg/reconcile" "sigs.k8s.io/controller-runtime/pkg/webhook" networkingolmv1 "github.com/openshift/aws-load-balancer-operator/api/v1" @@ -117,9 +120,26 @@ func main() { ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) + // The manager runs with a cancelable context so that the TLS profile watcher + // can trigger a graceful shutdown when the cluster TLS configuration changes, + // letting the Deployment restart the pod to pick up the new profile. + ctx, cancel := context.WithCancel(ctrl.SetupSignalHandler()) + defer cancel() + + restConfig := ctrl.GetConfigOrDie() + profile := getTLSSecurityProfile(ctx, restConfig) + tlsConfig, err := getTLSConfigFromProfile(profile.spec) + if err != nil { + setupLog.Error(err, "unable to get TLS configuration from profile") + os.Exit(1) + } + webhookSrv := webhook.NewServer(webhook.Options{ TLSOpts: []func(config *tls.Config){ func(config *tls.Config) { + config.MinVersion = tlsConfig.MinVersion + config.CipherSuites = tlsConfig.CipherSuites + config.CurvePreferences = tlsConfig.CurvePreferences if webhookDisableHTTP2 { config.NextProtos = []string{"http/1.1"} } @@ -128,14 +148,6 @@ func main() { Port: 9443, }) - restConfig := ctrl.GetConfigOrDie() - profile := getTLSSecurityProfile(context.TODO(), restConfig) - tlsConfig, err := getTLSConfigFromProfile(profile) - if err != nil { - setupLog.Error(err, "unable to get TLS configuration from profile") - os.Exit(1) - } - mgr, err := ctrl.NewManager(restConfig, ctrl.Options{ Scheme: scheme, Metrics: metrics.Options{ @@ -224,6 +236,13 @@ func main() { } //+kubebuilder:scaffold:builder + // Watch the cluster APIServer so that runtime changes to the TLS security + // profile or adherence policy trigger a restart to re-apply them. + if err = setupTLSProfileWatch(mgr, cancel, profile); err != nil { + setupLog.Error(err, "unable to set up TLS profile watch") + os.Exit(1) + } + if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { setupLog.Error(err, "unable to set up health check") os.Exit(1) @@ -234,7 +253,7 @@ func main() { } setupLog.Info("starting manager") - if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { + if err := mgr.Start(ctx); err != nil { setupLog.Error(err, "problem running manager") os.Exit(1) } @@ -295,19 +314,110 @@ var tlsGroupToCurveID = map[configv1.TLSGroup]tls.CurveID{ configv1.TLSGroupX25519MLKEM768: tls.X25519MLKEM768, } -func getTLSSecurityProfile(ctx context.Context, config *rest.Config) *configv1.TLSSecurityProfile { +// shouldHonorClusterTLSProfile returns true if the component should honor the +// cluster-wide TLS security profile settings from apiserver.config.openshift.io/cluster. +// Unknown enum values are treated as StrictAllComponents for forward compatibility. +func shouldHonorClusterTLSProfile(adherence configv1.TLSAdherencePolicy) bool { + switch adherence { + case configv1.TLSAdherencePolicyNoOpinion, configv1.TLSAdherencePolicyLegacyAdheringComponentsOnly: + return false + default: + return true + } +} + +// tlsProfileWatcher watches the cluster APIServer for changes to the TLS security +// profile or adherence policy. When either changes from the value observed at +// startup, it cancels the manager context so the pod restarts and re-applies the +// new configuration to its TLS servers. +type tlsProfileWatcher struct { + client client.Client + cancel context.CancelFunc + initialProfile *configv1.TLSSecurityProfile + initialAdherence configv1.TLSAdherencePolicy +} + +func (w *tlsProfileWatcher) Reconcile(ctx context.Context, _ reconcile.Request) (reconcile.Result, error) { + var apiServer configv1.APIServer + if err := w.client.Get(ctx, types.NamespacedName{Name: "cluster"}, &apiServer); err != nil { + return reconcile.Result{}, client.IgnoreNotFound(err) + } + + // An adherence change always requires re-evaluation. A profile change only + // matters when the current adherence policy makes the operator honor the + // profile; in legacy or unset mode the profile does not affect the operator's + // TLS configuration, so a profile-only change must not trigger a restart. + adherenceChanged := apiServer.Spec.TLSAdherence != w.initialAdherence + profileChanged := shouldHonorClusterTLSProfile(apiServer.Spec.TLSAdherence) && + !reflect.DeepEqual(apiServer.Spec.TLSSecurityProfile, w.initialProfile) + + if adherenceChanged || profileChanged { + setupLog.Info("cluster TLS configuration changed, shutting down to re-apply it", + "oldAdherence", w.initialAdherence, "newAdherence", apiServer.Spec.TLSAdherence) + w.cancel() + } + + return reconcile.Result{}, nil +} + +// setupTLSProfileWatch registers a controller that watches the cluster APIServer +// and cancels the manager context when the TLS profile or adherence policy +// changes. It is a no-op when the APIServer config API is unavailable +// (e.g. on non-OpenShift clusters). +func setupTLSProfileWatch(mgr ctrl.Manager, cancel context.CancelFunc, profile *tlsProfile) error { + if !profile.found { + return nil + } + + watcher := &tlsProfileWatcher{ + client: mgr.GetClient(), + cancel: cancel, + initialProfile: profile.spec, + initialAdherence: profile.adherence, + } + + return ctrl.NewControllerManagedBy(mgr). + Named("tlsprofilewatcher"). + WithOptions(controller.Options{NeedLeaderElection: ptr.To(false)}). + For(&configv1.APIServer{}). + Complete(watcher) +} + +type tlsProfile struct { + spec *configv1.TLSSecurityProfile + adherence configv1.TLSAdherencePolicy + // found reports whether the APIServer config was read; + // when false the TLS profile watch is not set up. + found bool +} + +func getTLSSecurityProfile(ctx context.Context, config *rest.Config) *tlsProfile { + profile := &tlsProfile{} + cl, err := client.New(config, client.Options{Scheme: scheme}) if err != nil { - setupLog.Info("failed to create temporary client to fetch APIServer config, using default intermediate profile") - return nil + setupLog.Info("failed to create temporary client to fetch APIServer config, using default intermediate profile", "error", err) + return profile } var apiServer configv1.APIServer err = cl.Get(ctx, types.NamespacedName{Name: "cluster"}, &apiServer) if err != nil { - setupLog.Info("failed to fetch APIServer config, using default intermediate profile") - return nil + setupLog.Info("failed to fetch APIServer config, using default intermediate profile", "error", err) + return profile } - return apiServer.Spec.TLSSecurityProfile + + profile.found = true + profile.adherence = apiServer.Spec.TLSAdherence + + // Only honor the cluster TLS profile if tlsAdherence is set to StrictAllComponents + if !shouldHonorClusterTLSProfile(apiServer.Spec.TLSAdherence) { + setupLog.Info("not honoring cluster TLS profile due to tlsAdherence policy", "tlsAdherence", apiServer.Spec.TLSAdherence) + return profile + } + + profile.spec = apiServer.Spec.TLSSecurityProfile + + return profile } func getTLSConfigFromProfile(profile *configv1.TLSSecurityProfile) (*tls.Config, error) { diff --git a/pkg/controllers/awsloadbalancercontroller/controller.go b/pkg/controllers/awsloadbalancercontroller/controller.go index aa466d3ba..80db1d79c 100644 --- a/pkg/controllers/awsloadbalancercontroller/controller.go +++ b/pkg/controllers/awsloadbalancercontroller/controller.go @@ -79,6 +79,7 @@ type AWSLoadBalancerControllerReconciler struct { //+kubebuilder:rbac:groups="",resources=configmaps,namespace=system,verbs=get;list;watch //+kubebuilder:rbac:groups="networking.k8s.io",resources=ingressclasses,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups="config.openshift.io",resources=infrastructures,verbs=get;list;watch +//+kubebuilder:rbac:groups="config.openshift.io",resources=apiservers,verbs=get;list;watch //+kubebuilder:rbac:groups="apps",resources=deployments,namespace=system,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups="networking.k8s.io",resources=networkpolicies,namespace=system,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups="",resources=serviceaccounts,namespace=system,verbs=get;list;watch;create;update;patch;delete