88 "fmt"
99 "net"
1010 "net/http"
11+ "slices"
1112 "time"
1213
1314 "github.com/prometheus/client_golang/prometheus"
@@ -26,6 +27,8 @@ import (
2627 "k8s.io/klog/v2"
2728
2829 configv1 "github.com/openshift/api/config/v1"
30+ configlistersv1 "github.com/openshift/client-go/config/listers/config/v1"
31+ tlsprofile "github.com/openshift/controller-runtime-common/pkg/tls"
2932 "github.com/openshift/library-go/pkg/crypto"
3033
3134 "github.com/openshift/cluster-version-operator/lib/resourcemerge"
@@ -128,6 +131,81 @@ type asyncResult struct {
128131 error error
129132}
130133
134+ // cachedTLSProfile holds the profile spec, apply function, and observed generation.
135+ // The apply function is computed once when the profile changes to avoid
136+ // calling NewTLSConfigFromProfile on every TLS handshake.
137+ // The generation is used to detect when the APIServer resource has been updated.
138+ type cachedTLSProfile struct {
139+ spec configv1.TLSProfileSpec
140+ apply func (* tls.Config )
141+ generation int64
142+ }
143+
144+ // getAPIServerTLSProfile fetches the cluster TLS profile from APIServer resource
145+ // and returns the updated cache. This is called on each TLS handshake.
146+ // On error, returns the last successfully fetched profile if available.
147+ // Returns an error if no valid profile has ever been fetched (fails closed for security).
148+ func getAPIServerTLSProfile (apiServerLister configlistersv1.APIServerLister , lastValidProfile * cachedTLSProfile ) (* cachedTLSProfile , error ) {
149+ apiServer , err := apiServerLister .Get (tlsprofile .APIServerName )
150+ if err != nil {
151+ klog .Errorf ("Failed to get APIServer resource: %v" , err )
152+ return fallbackToCached (lastValidProfile )
153+ }
154+
155+ // Check if the cached profile is still valid based on generation
156+ if lastValidProfile != nil && lastValidProfile .generation == apiServer .Generation {
157+ klog .V (4 ).Info ("Using cached TLS profile (generation unchanged)" )
158+ return lastValidProfile , nil
159+ }
160+
161+ profile , err := tlsprofile .GetTLSProfileSpec (apiServer .Spec .TLSSecurityProfile )
162+ if err != nil {
163+ klog .Errorf ("Failed to resolve TLS profile from APIServer: %v" , err )
164+ return fallbackToCached (lastValidProfile )
165+ }
166+
167+ if lastValidProfile != nil && lastValidProfile .isEqual (& profile ) {
168+ klog .V (4 ).Info ("TLS profile spec unchanged despite generation bump, updating generation" )
169+ return & cachedTLSProfile {
170+ spec : profile ,
171+ apply : lastValidProfile .apply ,
172+ generation : apiServer .Generation ,
173+ }, nil
174+ }
175+
176+ applyTLSProfile , unsupportedCiphers := tlsprofile .NewTLSConfigFromProfile (profile )
177+ if len (unsupportedCiphers ) > 0 {
178+ klog .Warningf ("TLS profile contains unsupported ciphers (will be ignored): %v" , unsupportedCiphers )
179+ }
180+ klog .Infof ("TLS profile changed to: MinTLSVersion=%s, Ciphers=%s" , profile .MinTLSVersion , profile .Ciphers )
181+ return & cachedTLSProfile {
182+ spec : profile ,
183+ apply : applyTLSProfile ,
184+ generation : apiServer .Generation ,
185+ }, nil
186+ }
187+
188+ // fallbackToCached returns the cached profile if available, otherwise returns an error.
189+ func fallbackToCached (lastValidProfile * cachedTLSProfile ) (* cachedTLSProfile , error ) {
190+ if lastValidProfile != nil {
191+ klog .Warningf ("Using last valid TLS profile" )
192+ return lastValidProfile , nil
193+ }
194+ return nil , fmt .Errorf ("no valid TLS profile available" )
195+ }
196+
197+ // isEqual checks if the cached profile matches the given profile spec.
198+ func (c * cachedTLSProfile ) isEqual (profile * configv1.TLSProfileSpec ) bool {
199+ if c == nil && profile == nil {
200+ return true
201+ }
202+ if c == nil || profile == nil {
203+ return false
204+ }
205+ return c .spec .MinTLSVersion == profile .MinTLSVersion &&
206+ slices .Equal (c .spec .Ciphers , profile .Ciphers )
207+ }
208+
131209func createHttpServer (options MetricsOptions , clientCA dynamiccertificates.CAContentProvider ) * http.Server {
132210 if options .DisableAuthentication && options .DisableAuthorization {
133211 handler := http .NewServeMux ()
@@ -270,7 +348,7 @@ type MetricsOptions struct {
270348// Continues serving until runContext.Done() and then attempts a clean
271349// shutdown limited by shutdownContext.Done(). Assumes runContext.Done()
272350// occurs before or simultaneously with shutdownContext.Done().
273- func RunMetrics (runContext context.Context , shutdownContext context.Context , restConfig * rest.Config , options MetricsOptions ) error {
351+ func RunMetrics (runContext context.Context , shutdownContext context.Context , restConfig * rest.Config , apiServerLister configlistersv1. APIServerLister , options MetricsOptions ) error {
274352 if options .ListenAddress == "" {
275353 return errors .New ("listen address is required to serve metrics" )
276354 }
@@ -355,6 +433,7 @@ func RunMetrics(runContext context.Context, shutdownContext context.Context, res
355433 // baseTlSConfig is a template passed to servingCertController,
356434 // which generates updated configs via GetConfigForClient callback on each TLS handshake.
357435 // This enables automatic certificate rotation without server restarts.
436+ // The cluster TLS profile will be applied dynamically in GetConfigForClient.
358437 baseTlSConfig := crypto .SecureTLSConfig (& tls.Config {ClientAuth : clientAuth })
359438 servingCertController := dynamiccertificates .NewDynamicServingCertificateController (
360439 baseTlSConfig ,
@@ -382,6 +461,12 @@ func RunMetrics(runContext context.Context, shutdownContext context.Context, res
382461 }()
383462
384463 server := createHttpServer (options , clientCA )
464+
465+ // lastValidProfile caches the last successfully fetched TLS profile and its apply function.
466+ // On errors, we use this cached value to maintain stability rather than
467+ // constantly switching between profiles on transient errors.
468+ var lastValidProfile * cachedTLSProfile
469+
385470 tlsConfig := crypto .SecureTLSConfig (& tls.Config {
386471 GetConfigForClient : func (clientHello * tls.ClientHelloInfo ) (* tls.Config , error ) {
387472 config , err := servingCertController .GetConfigForClient (clientHello )
@@ -393,6 +478,17 @@ func RunMetrics(runContext context.Context, shutdownContext context.Context, res
393478 err := fmt .Errorf ("serving certificate controller returned nil TLS configuration" )
394479 return nil , err
395480 }
481+
482+ // Fetch cluster TLS profile from APIServer resource (cached via lister, O(1) lookup)
483+ // and apply it to the config. This allows dynamic updates without CVO restart.
484+ // Fail closed if no valid profile is available (security).
485+ profile , err := getAPIServerTLSProfile (apiServerLister , lastValidProfile )
486+ if err != nil {
487+ return nil , fmt .Errorf ("failed to get TLS profile for metrics server: %w" , err )
488+ }
489+ lastValidProfile = profile
490+ profile .apply (config )
491+
396492 return config , nil
397493 },
398494 })
0 commit comments