-
Notifications
You must be signed in to change notification settings - Fork 26
NE-2739: Honor APIServer tlsAdherence and watch for TLS profile changes #335
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
aa3a6ba
26b6508
2b00963
af22e3a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -34,6 +34,7 @@ rules: | |
| - apiGroups: | ||
| - config.openshift.io | ||
| resources: | ||
| - apiservers | ||
| - infrastructures | ||
| verbs: | ||
| - get | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. startup and watcher use different reads. TLS is selected here, but there is another Get later on around line 361, if adherence profile changes, like from Legacy to Strict the process will run with the old config but treat the new values as the baseline and not restart
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good point, let me try to make
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Addressed. |
||
| 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 | ||
|
Comment on lines
+140
to
+142
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: this could a PR on its own - Webhook TLS fix — apply cluster TLS profile to the webhook server
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The PR aims at fixing all remaining gaps in a single PR. The webook change is in a separate commit for the clear separation and easy of reviewing. |
||
| 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) | ||
| } | ||
|
|
||
|
Comment on lines
-131
to
-138
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. moving this code block above is no-op, what is the motivation behind? |
||
| 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 { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IMHO, we can do it simpler, given that we do not perform any reconcilation on the watched resource. we can indtroduce a TLS profile watch — raw informer, no controller Registered inline in main() after the manager is created, replacing setupTLSProfileWatch: if profile.found {
if err := mgr.Add(manager.RunnableFunc(func(ctx context.Context) error {
inf, err := mgr.GetCache().GetInformer(ctx, &configv1.APIServer{})
if err != nil {
return err
}
if _, err := inf.AddEventHandler(toolscache.ResourceEventHandlerFuncs{
UpdateFunc: func(oldObj, newObj interface{}) {
oldAS := oldObj.(*configv1.APIServer)
newAS := newObj.(*configv1.APIServer)
if oldAS.Spec.TLSAdherence != newAS.Spec.TLSAdherence ||
!reflect.DeepEqual(oldAS.Spec.TLSSecurityProfile, newAS.Spec.TLSSecurityProfile) {
setupLog.Info("cluster TLS configuration changed, exiting to re-apply")
os.Exit(0)
}
},
}); err != nil {
return err
}
<-ctx.Done()
return nil
})); err != nil {
setupLog.Error(err, "unable to set up TLS profile watch")
os.Exit(1)
}
}We need no context wrapping, no code movement setupLog.Info("starting manager")
if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
setupLog.Error(err, "problem running manager")
os.Exit(1)
} |
||
| 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 { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This path treats every read failure as “APIServer unavailable”: it silently applies Intermediate and then profile.found == false disables the watcher permanently. I think we should distinguish transient failures so they can retry or fail startup rather than permanently disabling TLS updates. |
||
| 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 | ||
|
Comment on lines
+394
to
+420
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Propagate non-availability errors from 🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| func getTLSConfigFromProfile(profile *configv1.TLSSecurityProfile) (*tls.Config, error) { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could we do kube rbac proxy clean up in a separate PR, to keep this one focused? It is a leftover of #313 hence, let's not fixed here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This PR is an umbrella for everything we didn't do in #313. I think a separate commit is enough to keep things clean.