Skip to content

Commit 622e335

Browse files
DavidHurtaclaude
andcommitted
pkg/cvo/metrics: Authenticate clients using mTLS
In OpenShift, core operators SHOULD require authentication and they SHOULD support TLS client certificate authentication [1]. They also SHOULD support local authorization and SHOULD allow the well-known metrics scraping identity [1]. To achieve this, an operator must be able to verify a client's certificate. To do this, the certificate can be verified using the certificate authority (CA) bundle located at the client-ca-file key of the kube-system/extension-apiserver-authentication ConfigMap [2]. Guarantee failed connections when the config from the GetConfigForClient method is nil to ensure connections are only using the TLS config from the serving cert controller. [1]: https://github.com/openshift/enhancements/blob/master/CONVENTIONS.md#metrics [2]: https://rhobs-handbook.netlify.app/products/openshiftmonitoring/collecting_metrics.md/#exposing-metrics-for-prometheus Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 2a432a6 commit 622e335

1 file changed

Lines changed: 67 additions & 26 deletions

File tree

pkg/cvo/metrics.go

Lines changed: 67 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"k8s.io/apimachinery/pkg/labels"
2020
"k8s.io/apimachinery/pkg/util/sets"
2121
"k8s.io/apiserver/pkg/server/dynamiccertificates"
22+
"k8s.io/client-go/kubernetes"
2223
authenticationclientsetv1 "k8s.io/client-go/kubernetes/typed/authentication/v1"
2324
"k8s.io/client-go/rest"
2425
"k8s.io/client-go/tools/cache"
@@ -252,35 +253,93 @@ func RunMetrics(runContext context.Context, shutdownContext context.Context, lis
252253
resultChannelCount := 0
253254

254255
// Create a dynamic serving cert/key controller to watch for serving certificate changes from files.
255-
servingCertController, err := dynamiccertificates.NewDynamicServingContentFromFiles("metrics-serving-cert", certFile, keyFile)
256+
servingContentController, err := dynamiccertificates.NewDynamicServingContentFromFiles("metrics-serving-cert", certFile, keyFile)
256257
if err != nil {
257258
return fmt.Errorf("failed to create serving certificate controller: %w", err)
258259
}
259-
if err := servingCertController.RunOnce(metricsContext); err != nil {
260+
if err := servingContentController.RunOnce(metricsContext); err != nil {
260261
return fmt.Errorf("failed to initialize serving content controller: %w", err)
261262
}
262263

263264
// Start the serving cert controller to begin watching the cert and key files
264265
resultChannelCount++
265266
go func() {
266-
servingCertController.Run(metricsContext, 1)
267+
servingContentController.Run(metricsContext, 1)
267268
resultChannel <- asyncResult{name: "serving content controller"}
268269
}()
269270

270-
// Create TLS config using the controllers. The config uses callbacks to dynamically
271-
// fetch the latest certificates and CA bundles on each connection, so no server
272-
// restart is needed when certificates change.
273-
tlsConfig, err := makeTLSConfig(servingCertController)
271+
// Create a dynamic CA controller to watch for client CA changes from a ConfigMap.
272+
kubeClient, err := kubernetes.NewForConfig(restConfig)
274273
if err != nil {
275-
return fmt.Errorf("failed to create TLS config: %w", err)
274+
return fmt.Errorf("failed to create kube client: %w", err)
275+
}
276+
277+
clientCAController, err := dynamiccertificates.NewDynamicCAFromConfigMapController(
278+
"metrics-client-ca",
279+
"kube-system",
280+
"extension-apiserver-authentication",
281+
"client-ca-file",
282+
kubeClient)
283+
if err != nil {
284+
return fmt.Errorf("failed to create client CA controller: %w", err)
285+
}
286+
287+
if err := clientCAController.RunOnce(metricsContext); err != nil {
288+
return fmt.Errorf("failed to initialize client CA controller: %w", err)
276289
}
277290

278291
client, err := authenticationclientsetv1.NewForConfig(restConfig)
279292
if err != nil {
280293
return fmt.Errorf("failed to create config: %w", err)
281294
}
282295

296+
// Start the client CA controller to begin watching the ConfigMap
297+
resultChannelCount++
298+
go func() {
299+
clientCAController.Run(metricsContext, 1)
300+
resultChannel <- asyncResult{name: "client CA from ConfigMap controller"}
301+
}()
302+
303+
servingCertController := dynamiccertificates.NewDynamicServingCertificateController(
304+
crypto.SecureTLSConfig(&tls.Config{
305+
ClientAuth: tls.RequireAndVerifyClientCert,
306+
}),
307+
clientCAController,
308+
servingContentController,
309+
nil,
310+
nil,
311+
)
312+
if err := servingCertController.RunOnce(); err != nil {
313+
return fmt.Errorf("failed to initialize serving certificate controller: %w", err)
314+
}
315+
316+
// Register listeners so servingCertController is notified when certificates change.
317+
if clientCAController != nil {
318+
clientCAController.AddListener(servingCertController)
319+
}
320+
servingContentController.AddListener(servingCertController)
321+
322+
resultChannelCount++
323+
go func() {
324+
servingCertController.Run(1, metricsContext.Done())
325+
resultChannel <- asyncResult{name: "serving certification controller"}
326+
}()
327+
283328
server := createHttpServer(metricsContext, client, disableMetricsAuth)
329+
tlsConfig := crypto.SecureTLSConfig(&tls.Config{
330+
GetConfigForClient: func(clientHello *tls.ClientHelloInfo) (*tls.Config, error) {
331+
config, err := servingCertController.GetConfigForClient(clientHello)
332+
if err != nil {
333+
return nil, err
334+
}
335+
if config == nil {
336+
// To ensure we rather safely fail connections when the desired config is nil. Safety over availability.
337+
err := fmt.Errorf("serving certificate controller returned nil TLS configuration")
338+
return nil, err
339+
}
340+
return config, nil
341+
},
342+
})
284343

285344
resultChannelCount++
286345
go func() {
@@ -659,21 +718,3 @@ func mostRecentTimestamp(cv *configv1.ClusterVersion) int64 {
659718
}
660719
return latest.Unix()
661720
}
662-
663-
func makeTLSConfig(servingCertController dynamiccertificates.CertKeyContentProvider) (*tls.Config, error) {
664-
_, err := tls.X509KeyPair(servingCertController.CurrentCertKeyContent())
665-
if err != nil {
666-
return nil, fmt.Errorf("failed to create X509 key pair: %w", err)
667-
}
668-
tlsConfig := crypto.SecureTLSConfig(&tls.Config{
669-
GetCertificate: func(_ *tls.ClientHelloInfo) (*tls.Certificate, error) {
670-
cert, err := tls.X509KeyPair(servingCertController.CurrentCertKeyContent())
671-
if err != nil {
672-
klog.Errorf("Failed to load current serving certificate, rejecting connection: %v", err)
673-
return nil, fmt.Errorf("invalid serving certificate: %w", err)
674-
}
675-
return &cert, nil
676-
},
677-
})
678-
return tlsConfig, nil
679-
}

0 commit comments

Comments
 (0)