From aa3a6ba1e6b962e5a7e105997800b2d69f021d87 Mon Sep 17 00:00:00 2001 From: Andrey Lebedev Date: Wed, 16 Sep 2026 19:17:49 +0200 Subject: [PATCH 1/4] Grant RBAC to read the cluster APIServer config Add a kubebuilder marker granting get;list;watch on `config.openshift.io/apiservers` and regenerate `config/rbac/role.yaml` and the bundle `ClusterServiceVersion`. The operator reads the cluster `apiservers` resource to resolve the `tlsSecurityProfile` and `tlsAdherence`, and the upcoming watch lists and watches it. Without this permission those reads are forbidden and the operator silently falls back to the default intermediate profile. Include the concrete error in the `getTLSSecurityProfile` log messages so such failures (e.g. a missing permission) are visible. Co-Authored-By: Claude --- .../aws-load-balancer-operator.clusterserviceversion.yaml | 1 + config/rbac/role.yaml | 1 + main.go | 4 ++-- pkg/controllers/awsloadbalancercontroller/controller.go | 1 + 4 files changed, 5 insertions(+), 2 deletions(-) diff --git a/bundle/manifests/aws-load-balancer-operator.clusterserviceversion.yaml b/bundle/manifests/aws-load-balancer-operator.clusterserviceversion.yaml index 14b32a268..db4cff3e3 100644 --- a/bundle/manifests/aws-load-balancer-operator.clusterserviceversion.yaml +++ b/bundle/manifests/aws-load-balancer-operator.clusterserviceversion.yaml @@ -159,6 +159,7 @@ spec: - apiGroups: - config.openshift.io resources: + - apiservers - infrastructures verbs: - get 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..2685e75c3 100644 --- a/main.go +++ b/main.go @@ -298,13 +298,13 @@ var tlsGroupToCurveID = map[configv1.TLSGroup]tls.CurveID{ func getTLSSecurityProfile(ctx context.Context, config *rest.Config) *configv1.TLSSecurityProfile { 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") + setupLog.Info("failed to create temporary client to fetch APIServer config, using default intermediate profile", "error", err) return nil } 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") + setupLog.Info("failed to fetch APIServer config, using default intermediate profile", "error", err) return nil } return apiServer.Spec.TLSSecurityProfile 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 From 26b65085fcd9e7d4f6ee880e7e8e53f919f35f66 Mon Sep 17 00:00:00 2001 From: Andrey Lebedev Date: Wed, 16 Sep 2026 19:18:06 +0200 Subject: [PATCH 2/4] Honor APIServer tlsAdherence and watch for TLS profile changes Read the cluster `apiservers` config once at startup into a `tlsProfile` holding the security profile and adherence policy, and use `tlsAdherence` to decide whether to honor the cluster TLS profile: only honor it under `StrictAllComponents`, keeping `LegacyAdheringComponentsOnly` on the operator's own defaults while treating unknown enum values as secure. The resolved profile drives the metrics server TLS configuration. Add an inline controller that watches the cluster `apiservers` resource and cancels the manager context when the `tlsSecurityProfile` or `tlsAdherence` changes from the value observed at startup, letting the Deployment restart the pod to re-apply the new configuration. The same single observation feeds both the startup TLS config and the watcher baseline, so an adherence change is not absorbed into an independent baseline. An adherence change always triggers a restart, while a profile change triggers one only when the current adherence honors the profile. The manager runs with a cancelable context wrapping the signal handler so the watcher can trigger a graceful shutdown. The watch is a no-op on clusters where the APIServer config API is unavailable. Co-Authored-By: Claude --- main.go | 121 ++++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 114 insertions(+), 7 deletions(-) diff --git a/main.go b/main.go index 2685e75c3..f64de92dc 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" @@ -128,9 +131,15 @@ func main() { Port: 9443, }) + // 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(context.TODO(), restConfig) - tlsConfig, err := getTLSConfigFromProfile(profile) + 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) @@ -224,6 +233,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 +250,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 +311,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", "error", err) - return nil + 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", "error", err) - return nil + return profile + } + + 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 } - return apiServer.Spec.TLSSecurityProfile + + profile.spec = apiServer.Spec.TLSSecurityProfile + + return profile } func getTLSConfigFromProfile(profile *configv1.TLSSecurityProfile) (*tls.Config, error) { From 2b0096384d09d10e9af772625768f04cd67963b5 Mon Sep 17 00:00:00 2001 From: Andrey Lebedev Date: Wed, 16 Sep 2026 19:18:23 +0200 Subject: [PATCH 3/4] Apply cluster TLS profile to the webhook server The conversion webhook server ignored the cluster `tlsSecurityProfile`, unlike the metrics server. Apply the same `MinVersion`, `CipherSuites` and `CurvePreferences` to the webhook `TLSOpts` so both servers honor the cluster TLS profile. The profile is now resolved before the webhook server is created so its config is available at construction time. Co-Authored-By: Claude --- main.go | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/main.go b/main.go index f64de92dc..b50c839d2 100644 --- a/main.go +++ b/main.go @@ -120,17 +120,6 @@ func main() { ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) - webhookSrv := webhook.NewServer(webhook.Options{ - TLSOpts: []func(config *tls.Config){ - func(config *tls.Config) { - if webhookDisableHTTP2 { - config.NextProtos = []string{"http/1.1"} - } - }, - }, - Port: 9443, - }) - // 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. @@ -145,6 +134,20 @@ func main() { 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"} + } + }, + }, + Port: 9443, + }) + mgr, err := ctrl.NewManager(restConfig, ctrl.Options{ Scheme: scheme, Metrics: metrics.Options{ From af22e3afef6af1e51da556d3948f01c1ae89fa85 Mon Sep 17 00:00:00 2001 From: Andrey Lebedev Date: Thu, 17 Sep 2026 00:05:26 +0200 Subject: [PATCH 4/4] Enable tls-profiles bundle annotation and drop kube-rbac-proxy Set `features.operators.openshift.io/tls-profiles` to `true` now that the operator honors the cluster TLS security profile, updating both the source base and the generated `ClusterServiceVersion`. Remove the leftover `kube-rbac-proxy` plumbing from the downstream bundle build: the operator serves its metrics natively and no longer runs the proxy sidecar. Drop `KUBE_RBAC_PROXY_IMAGE_PULLSPEC` from `container_digest.sh` and its required-variable check, image `sed` replacements, and orphaned `relatedImages` entry from `update_bundle.sh`. Co-Authored-By: Claude --- bundle-hack/container_digest.sh | 4 ---- bundle-hack/update_bundle.sh | 8 +------- .../aws-load-balancer-operator.clusterserviceversion.yaml | 2 +- .../aws-load-balancer-operator.clusterserviceversion.yaml | 2 +- 4 files changed, 3 insertions(+), 13 deletions(-) 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 db4cff3e3..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" 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"