Skip to content

Commit 3bab9a7

Browse files
committed
feat(metrics): use cluster tls settings
Signed-off-by: Calum Murray <[email protected]>
1 parent 8613e58 commit 3bab9a7

7 files changed

Lines changed: 354 additions & 3 deletions

File tree

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ func-test: deploy
7575
unit-test:
7676
@echo "Executing unit tests"
7777
go clean -testcache
78-
go test -v ./controllers/...
78+
go test -v ./controllers/... ./pkg/...
7979

8080
build: $(SOURCES)
8181
go build $(GOBUILDFLAGS) -ldflags="$(GOLDFLAGS)" -o ./update-service-operator ./

bundle/manifests/update-service-operator.clusterserviceversion.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ spec:
124124
- apiGroups:
125125
- config.openshift.io
126126
resources:
127+
- apiservers
127128
- images
128129
verbs:
129130
- get

config/rbac/role.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ rules:
2626
- apiGroups:
2727
- config.openshift.io
2828
resources:
29+
- apiservers
2930
- images
3031
verbs:
3132
- get

controllers/updateservice_controller.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ type UpdateServiceReconciler struct {
5858
// +kubebuilder:rbac:groups="apps",resources=deployments,verbs=get;list;watch
5959
// +kubebuilder:rbac:groups=networking.k8s.io,resources=networkpolicies,verbs=get;list;watch
6060
// +kubebuilder:rbac:groups="policy",resources=poddisruptionbudgets,verbs=get;list;watch
61-
// +kubebuilder:rbac:groups=config.openshift.io,resources=images,verbs=get;list;watch
61+
// +kubebuilder:rbac:groups=config.openshift.io,resources=apiservers;images,verbs=get;list;watch
6262
// +kubebuilder:rbac:groups=route.openshift.io,resources=routes,verbs=get;list;watch
6363
// +kubebuilder:rbac:groups=updateservice.operator.openshift.io,resources=*,verbs=get;list;watch
6464
// +kubebuilder:rbac:groups="",resources=pods;services;services/finalizers;endpoints;persistentvolumeclaims;events;configmaps;secrets,verbs=create;delete;get;list;patch;update;watch,namespace=openshift-update-service

main.go

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package main
22

33
import (
4+
"context"
5+
"crypto/tls"
46
"flag"
57
"fmt"
68
"os"
@@ -18,12 +20,14 @@ import (
1820

1921
updateservicev1 "github.com/openshift/cincinnati-operator/api/v1"
2022
"github.com/openshift/cincinnati-operator/controllers"
23+
"github.com/openshift/cincinnati-operator/pkg/tlsconfig"
2124
"github.com/openshift/cincinnati-operator/version"
2225

2326
// to ensure that exec-entrypoint and run can make use of them.
2427
_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
2528
ctrl "sigs.k8s.io/controller-runtime"
2629
"sigs.k8s.io/controller-runtime/pkg/cache"
30+
"sigs.k8s.io/controller-runtime/pkg/client"
2731
"sigs.k8s.io/controller-runtime/pkg/log/zap"
2832
// +kubebuilder:scaffold:imports
2933
)
@@ -86,12 +90,31 @@ func main() {
8690
log.Error(err, "POD_NAMESPACE must be set; unable to start manager")
8791
os.Exit(1)
8892
}
93+
94+
restConfig := ctrl.GetConfigOrDie()
95+
96+
// Read the cluster's TLS security profile from ApiServer configuration
97+
// and apply it to the metrics server. Falls back to Intermediate if unavailable.
98+
var tlsProfile *configv1.TLSSecurityProfile
99+
c, err := client.New(restConfig, client.Options{Scheme: scheme})
100+
if err != nil {
101+
log.Error(err, "Failed to create client for reading ApiServer TLS profile, using Intermediate default")
102+
}
103+
if c != nil {
104+
tlsProfile, err = tlsconfig.FetchAPIServerProfile(context.Background(), c)
105+
if err != nil {
106+
log.Error(err, "Failed to read ApiServer TLS profile, using Intermediate default")
107+
}
108+
}
109+
tlsOpt := tlsconfig.FromProfile(tlsProfile)
110+
89111
options := ctrl.Options{
90112
Scheme: scheme,
91113
Metrics: metricsserver.Options{
92114
BindAddress: metricsAddr,
93115
SecureServing: true,
94116
CertDir: "/etc/metrics-certs",
117+
TLSOpts: []func(*tls.Config){tlsOpt},
95118
// TODO: add FilterProvider: filters.WithAuthenticationAndAuthorization when upgrading controller-runtime to v0.19+
96119
},
97120
LeaderElection: enableLeaderElection,
@@ -104,7 +127,7 @@ func main() {
104127
},
105128
}
106129

107-
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), options)
130+
mgr, err := ctrl.NewManager(restConfig, options)
108131
if err != nil {
109132
log.Error(err, "unable to start manager")
110133
os.Exit(1)

pkg/tlsconfig/tlsconfig.go

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
// Package tlsconfig converts OpenShift TLS security profiles
2+
// (configv1.TLSSecurityProfile) into Go crypto/tls configuration.
3+
package tlsconfig
4+
5+
import (
6+
"context"
7+
"crypto/tls"
8+
"fmt"
9+
"strings"
10+
11+
configv1 "github.com/openshift/api/config/v1"
12+
"sigs.k8s.io/controller-runtime/pkg/client"
13+
logf "sigs.k8s.io/controller-runtime/pkg/log"
14+
)
15+
16+
var log = logf.Log.WithName("tlsconfig")
17+
18+
// TODO: Once openshift/api is bumped to a version that includes the Groups
19+
// field on TLSProfileSpec (added upstream for OCP 4.20), wire up
20+
// tls.Config.CurvePreferences from the profile's Groups to support
21+
// post-quantum key agreement (e.g. X25519MLKEM768).
22+
23+
// openSSLToGoCipherSuiteID maps OpenSSL cipher suite names used by OpenShift
24+
// TLS profiles to Go crypto/tls cipher suite IDs.
25+
//
26+
// TLS 1.3 ciphers (TLS_AES_128_GCM_SHA256, TLS_AES_256_GCM_SHA384,
27+
// TLS_CHACHA20_POLY1305_SHA256) are not included because Go's crypto/tls
28+
// always enables them when TLS 1.3 is negotiated — the CipherSuites field
29+
// on tls.Config only controls TLS 1.2 and below.
30+
//
31+
// Ciphers not supported by Go (e.g., DHE-RSA-*) are omitted from this map
32+
// and logged as warnings at startup.
33+
var openSSLToGoCipherSuiteID = map[string]uint16{
34+
"ECDHE-ECDSA-AES128-GCM-SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
35+
"ECDHE-RSA-AES128-GCM-SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
36+
"ECDHE-ECDSA-AES256-GCM-SHA384": tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
37+
"ECDHE-RSA-AES256-GCM-SHA384": tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
38+
"ECDHE-ECDSA-CHACHA20-POLY1305": tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
39+
"ECDHE-RSA-CHACHA20-POLY1305": tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,
40+
"ECDHE-ECDSA-AES128-SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256,
41+
"ECDHE-RSA-AES128-SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256,
42+
"ECDHE-ECDSA-AES128-SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
43+
"ECDHE-RSA-AES128-SHA": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
44+
"ECDHE-ECDSA-AES256-SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
45+
"ECDHE-RSA-AES256-SHA": tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
46+
"AES128-GCM-SHA256": tls.TLS_RSA_WITH_AES_128_GCM_SHA256,
47+
"AES256-GCM-SHA384": tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
48+
"AES128-SHA256": tls.TLS_RSA_WITH_AES_128_CBC_SHA256,
49+
"AES128-SHA": tls.TLS_RSA_WITH_AES_128_CBC_SHA,
50+
"AES256-SHA": tls.TLS_RSA_WITH_AES_256_CBC_SHA,
51+
"DES-CBC3-SHA": tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA,
52+
}
53+
54+
// tlsVersionMap maps OpenShift TLS protocol version constants to Go crypto/tls version constants.
55+
var tlsVersionMap = map[configv1.TLSProtocolVersion]uint16{
56+
configv1.VersionTLS10: tls.VersionTLS10,
57+
configv1.VersionTLS11: tls.VersionTLS11,
58+
configv1.VersionTLS12: tls.VersionTLS12,
59+
configv1.VersionTLS13: tls.VersionTLS13,
60+
}
61+
62+
// EffectiveProfile resolves a TLS security profile to a concrete TLSProfileSpec.
63+
// Returns the Intermediate profile when the input is nil or references an unknown type.
64+
func EffectiveProfile(profile *configv1.TLSSecurityProfile) *configv1.TLSProfileSpec {
65+
if profile == nil {
66+
return configv1.TLSProfiles[configv1.TLSProfileIntermediateType]
67+
}
68+
if profile.Type == configv1.TLSProfileCustomType && profile.Custom != nil {
69+
return &profile.Custom.TLSProfileSpec
70+
}
71+
if spec, ok := configv1.TLSProfiles[profile.Type]; ok {
72+
return spec
73+
}
74+
return configv1.TLSProfiles[configv1.TLSProfileIntermediateType]
75+
}
76+
77+
// FromProfile returns a function that configures a tls.Config based on an
78+
// OpenShift TLS security profile. Cipher suites not supported by Go's
79+
// crypto/tls are skipped with a warning log.
80+
func FromProfile(profile *configv1.TLSSecurityProfile) func(*tls.Config) {
81+
spec := EffectiveProfile(profile)
82+
83+
minVersion := uint16(tls.VersionTLS12)
84+
if v, ok := tlsVersionMap[spec.MinTLSVersion]; ok {
85+
minVersion = v
86+
}
87+
88+
var cipherSuites []uint16
89+
for _, cipher := range spec.Ciphers {
90+
if id, ok := openSSLToGoCipherSuiteID[cipher]; ok {
91+
cipherSuites = append(cipherSuites, id)
92+
} else if !strings.HasPrefix(cipher, "TLS_") {
93+
// TLS 1.3 ciphers (TLS_* prefix) are always enabled by Go and
94+
// don't need to be in CipherSuites. Anything else is genuinely
95+
// unsupported by Go's crypto/tls.
96+
log.Info("Skipping cipher suite not supported by Go's crypto/tls", "cipher", cipher)
97+
}
98+
}
99+
100+
return func(cfg *tls.Config) {
101+
cfg.MinVersion = minVersion
102+
if len(cipherSuites) > 0 {
103+
cfg.CipherSuites = cipherSuites
104+
}
105+
}
106+
}
107+
108+
// FetchAPIServerProfile reads the TLS security profile from the cluster's
109+
// ApiServer configuration (apiserver.config.openshift.io/cluster).
110+
func FetchAPIServerProfile(ctx context.Context, reader client.Reader) (*configv1.TLSSecurityProfile, error) {
111+
apiServer := &configv1.APIServer{}
112+
if err := reader.Get(ctx, client.ObjectKey{Name: "cluster"}, apiServer); err != nil {
113+
return nil, fmt.Errorf("getting apiserver config: %w", err)
114+
}
115+
return apiServer.Spec.TLSSecurityProfile, nil
116+
}

0 commit comments

Comments
 (0)