Skip to content

Commit 5d0033c

Browse files
committed
TLS 1.3 compliance investigation and remediation
Signed-off-by: Theodor Mihalache <tmihalac@redhat.com>
1 parent 13ed8ba commit 5d0033c

10 files changed

Lines changed: 293 additions & 9 deletions

File tree

cmd/main.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import (
4141
monitoringv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1"
4242
"k8s.io/apimachinery/pkg/labels"
4343
"k8s.io/apimachinery/pkg/runtime"
44+
"k8s.io/apimachinery/pkg/types"
4445
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
4546
"k8s.io/client-go/discovery"
4647
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
@@ -56,6 +57,7 @@ import (
5657

5758
exploitiqv1alpha1 "github.com/RHEcosystemAppEng/exploit-iq-operator/api/v1alpha1"
5859
"github.com/RHEcosystemAppEng/exploit-iq-operator/internal/controller"
60+
"github.com/RHEcosystemAppEng/exploit-iq-operator/internal/tlsconfig"
5961
// +kubebuilder:scaffold:imports
6062
)
6163

@@ -89,6 +91,8 @@ func main() {
8991

9092
var logManagementAddr string
9193

94+
var tlsSecurityProfile string
95+
9296
var tlsOpts []func(*tls.Config)
9397

9498
flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+
@@ -110,6 +114,10 @@ func main() {
110114
flag.StringVar(&metricsCertKey, "metrics-cert-key", "tls.key", "The name of the metrics server key file.")
111115
flag.BoolVar(&enableHTTP2, "enable-http2", false,
112116
"If set, HTTP/2 will be enabled for the metrics and webhook servers")
117+
flag.StringVar(&tlsSecurityProfile, "tls-security-profile", "",
118+
"Override the TLS security profile for the metrics and webhook servers "+
119+
"(Old|Intermediate|Modern). When empty, the operator reads the cluster's "+
120+
"APIServer spec.tlsSecurityProfile; if that is unavailable it defaults to Intermediate (TLS 1.2).")
113121

114122
opts := zap.Options{}
115123
opts.BindFlags(flag.CommandLine)
@@ -218,6 +226,39 @@ func main() {
218226
tlsOpts = append(tlsOpts, disableHTTP2)
219227
}
220228

229+
// Determine the cluster TLS security profile so the metrics and webhook
230+
// servers honour it (OCP 5.0 TLS compliance, TC-5768). Precedence:
231+
// 1. --tls-security-profile flag (explicit override / non-OpenShift),
232+
// 2. the cluster APIServer's spec.tlsSecurityProfile,
233+
// 3. nil -> Intermediate default (TLS 1.2).
234+
// Read once at startup; changing the cluster profile requires an operator
235+
// restart to take effect.
236+
var tlsProfile *configv1.TLSSecurityProfile
237+
238+
switch {
239+
case tlsSecurityProfile != "":
240+
tlsProfile = &configv1.TLSSecurityProfile{Type: configv1.TLSProfileType(tlsSecurityProfile)}
241+
case hasOpenShiftConfig:
242+
profileClient, cErr := client.New(cfg, client.Options{Scheme: scheme})
243+
if cErr != nil {
244+
setupLog.Error(cErr, "unable to create client to read cluster TLS security profile; using default")
245+
} else {
246+
apiServer := &configv1.APIServer{}
247+
if gErr := profileClient.Get(context.Background(), types.NamespacedName{Name: "cluster"}, apiServer); gErr != nil {
248+
setupLog.Error(gErr, "unable to read cluster TLS security profile; using default (Intermediate)")
249+
} else {
250+
tlsProfile = apiServer.Spec.TLSSecurityProfile
251+
}
252+
}
253+
}
254+
255+
// Apply the cluster profile's minimum TLS version (and cipher suites for
256+
// TLS < 1.3) to the metrics and webhook servers. Appended before both
257+
// servers are constructed so each inherits it.
258+
tlsOpts = append(tlsOpts, func(c *tls.Config) {
259+
tlsconfig.Apply(c, tlsProfile)
260+
})
261+
221262
// Create watchers for metrics and webhooks certificates
222263
var metricsCertWatcher, webhookCertWatcher *certwatcher.CertWatcher
223264

config/rbac/role.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ rules:
5555
- apiGroups:
5656
- config.openshift.io
5757
resources:
58+
- apiservers
5859
- clusterversions
5960
- dnses
6061
verbs:

go.mod

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ require (
77
github.com/google/go-cmp v0.7.0
88
github.com/onsi/ginkgo/v2 v2.22.0
99
github.com/onsi/gomega v1.36.1
10-
github.com/openshift/api v0.0.0-20251009160459-595e66a09a84
10+
github.com/openshift/api v0.0.0-20260304172252-b0658d22beea
11+
github.com/openshift/library-go v0.0.0-20260518122146-385e91fd29b1
1112
github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.86.0
1213
github.com/stretchr/testify v1.11.1
1314
go.uber.org/zap v1.27.0

go.sum

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,10 @@ github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg
9393
github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
9494
github.com/onsi/gomega v1.36.1 h1:bJDPBO7ibjxcbHMgSCoo4Yj18UWbKDlLwX1x9sybDcw=
9595
github.com/onsi/gomega v1.36.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
96-
github.com/openshift/api v0.0.0-20251009160459-595e66a09a84 h1:8LC9yrt+LhRgZ1eVaHb2uxyUET/fqpyelSk2hke7wd4=
97-
github.com/openshift/api v0.0.0-20251009160459-595e66a09a84/go.mod h1:SPLf21TYPipzCO67BURkCfK6dcIIxx0oNRVWaOyRcXM=
96+
github.com/openshift/api v0.0.0-20260304172252-b0658d22beea h1:cakCJUhTaFEf67R5PARXWsgZAFSY0OkpIb0Sxo71gkM=
97+
github.com/openshift/api v0.0.0-20260304172252-b0658d22beea/go.mod h1:d5uzF0YN2nQQFA0jIEWzzOZ+edmo6wzlGLvx5Fhz4uY=
98+
github.com/openshift/library-go v0.0.0-20260518122146-385e91fd29b1 h1:PV/TYEV+otsCxgXPakMaK1KXxcmtZhKPkmoSrU2tMF4=
99+
github.com/openshift/library-go v0.0.0-20260518122146-385e91fd29b1/go.mod h1:rYGQrSg+t1JEzeEwg6BJw3loPpXg/n3kgRygUpgxavY=
98100
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
99101
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
100102
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=

internal/controller/exploitiqstack_controller.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ type ExploitIQStackReconciler struct {
7676
// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete
7777
// +kubebuilder:rbac:groups=batch,resources=jobs,verbs=get;list;watch;create;update;patch;delete
7878
// +kubebuilder:rbac:groups=networking.k8s.io,resources=ingresses,verbs=get;list;watch;create;update;patch;delete
79+
// +kubebuilder:rbac:groups=config.openshift.io,resources=apiservers,verbs=get;list;watch
7980
// +kubebuilder:rbac:groups=config.openshift.io,resources=clusterversions,verbs=get;list;watch
8081
// +kubebuilder:rbac:groups=config.openshift.io,resources=dnses,verbs=get;list;watch
8182
// +kubebuilder:rbac:groups=core,resources=configmaps,verbs=get;list;watch;create;update;patch;delete
@@ -219,12 +220,12 @@ func (r *ExploitIQStackReconciler) handleDeletion(
219220
log.Error(err, "failed cleanup", "component", component.Name())
220221

221222
return ctrl.Result{
222-
RequeueAfter: requeueAfterError,
223-
}, fmt.Errorf(
224-
"cleanup %q: %w",
225-
component.Name(),
226-
err,
227-
)
223+
RequeueAfter: requeueAfterError,
224+
}, fmt.Errorf(
225+
"cleanup %q: %w",
226+
component.Name(),
227+
err,
228+
)
228229
}
229230
}
230231
}

internal/controller/platform/detect.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"context"
2121
"fmt"
2222

23+
configv1 "github.com/openshift/api/config/v1"
2324
apierrors "k8s.io/apimachinery/pkg/api/errors"
2425
"k8s.io/apimachinery/pkg/api/meta"
2526
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@@ -45,6 +46,12 @@ type PlatformProfile struct {
4546
HasGatewayAPI bool
4647
HasIngress bool
4748
HasOpenShiftOperator bool
49+
50+
// TLSSecurityProfile is the cluster's configured TLS security profile
51+
// (APIServer spec.tlsSecurityProfile) on OpenShift, or nil when unavailable
52+
// (non-OpenShift, or not readable). Operands use it to honour the cluster TLS
53+
// policy (TC-5768).
54+
TLSSecurityProfile *configv1.TLSSecurityProfile
4855
}
4956

5057
// IsOpenShift returns true if the detected platform is OpenShift.
@@ -106,6 +113,15 @@ func BuildPlatformProfile(ctx context.Context, reader client.Reader) (*PlatformP
106113
profile.HasIngress = detectAPIGroup(ctx, reader, "networking.k8s.io")
107114
profile.HasOpenShiftOperator = detectAPIGroup(ctx, reader, "operator.openshift.io")
108115

116+
// On OpenShift, read the cluster TLS security profile so operands can honour
117+
// it (TC-5768). Best-effort: a read error leaves it nil (default profile).
118+
if detectAPIGroup(ctx, reader, "config.openshift.io") {
119+
apiServer := &configv1.APIServer{}
120+
if err := reader.Get(ctx, client.ObjectKey{Name: "cluster"}, apiServer); err == nil {
121+
profile.TLSSecurityProfile = apiServer.Spec.TLSSecurityProfile
122+
}
123+
}
124+
109125
return profile, nil
110126
}
111127

internal/resources/webapp/deployment.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
exploitiqv1alpha1 "github.com/RHEcosystemAppEng/exploit-iq-operator/api/v1alpha1"
1414
"github.com/RHEcosystemAppEng/exploit-iq-operator/internal/controller/platform"
1515
"github.com/RHEcosystemAppEng/exploit-iq-operator/internal/k8s"
16+
"github.com/RHEcosystemAppEng/exploit-iq-operator/internal/tlsconfig"
1617
)
1718

1819
// Deployment creates a Deployment for the WebApp component.
@@ -215,6 +216,21 @@ func buildWebAppEnv(
215216
corev1.EnvVar{Name: "QUARKUS_HTTP_SSL_CERTIFICATE_RELOAD-PERIOD", Value: "30m"},
216217
corev1.EnvVar{Name: "QUARKUS_OIDC_CLIENT-ID", Value: OAuthClientName},
217218
)
219+
220+
// Honour the cluster TLS security profile on the webapp's Quarkus TLS
221+
// endpoint (TC-5768). Env vars override application.properties in Quarkus,
222+
// and the webapp image does not hard-code these, so they take effect.
223+
protocols, ciphers := tlsconfig.QuarkusSSL(profile.TLSSecurityProfile)
224+
envs = append(envs, corev1.EnvVar{
225+
Name: "QUARKUS_HTTP_SSL_PROTOCOLS",
226+
Value: strings.Join(protocols, ","),
227+
})
228+
if len(ciphers) > 0 {
229+
envs = append(envs, corev1.EnvVar{
230+
Name: "QUARKUS_HTTP_SSL_CIPHER_SUITES",
231+
Value: strings.Join(ciphers, ","),
232+
})
233+
}
218234
}
219235

220236
if profile.IsKubernetes() {

internal/resources/webapp/deployment_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,11 @@ func TestDeployment(t *testing.T) {
194194
assert.Contains(t, envMap, "OPENSHIFT_DOMAIN")
195195
assert.Contains(t, envMap, "QUARKUS_HTTP_SSL_CERTIFICATE_FILES")
196196
assert.Equal(t, "0.0.0.0", envMap["QUARKUS_HTTP_HOST"])
197+
// TC-5768: with no cluster TLS profile the webapp inherits the
198+
// Intermediate default (TLS 1.2+), propagated to Quarkus.
199+
assert.Equal(t, "TLSv1.2,TLSv1.3", envMap["QUARKUS_HTTP_SSL_PROTOCOLS"])
200+
assert.Contains(t, envMap["QUARKUS_HTTP_SSL_CIPHER_SUITES"],
201+
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256")
197202
},
198203
expectError: false,
199204
},

internal/tlsconfig/tlsconfig.go

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
// Package tlsconfig maps the OpenShift cluster TLS security profile
2+
// (Old / Intermediate / Modern / Custom) onto a crypto/tls configuration for
3+
// the operator's served endpoints (metrics and webhook servers), so they
4+
// honour the cluster's configured TLS policy for OCP 5.0 compliance (TC-5768).
5+
package tlsconfig
6+
7+
import (
8+
"crypto/tls"
9+
10+
configv1 "github.com/openshift/api/config/v1"
11+
"github.com/openshift/library-go/pkg/crypto"
12+
)
13+
14+
// Resolve returns the minimum TLS version and Go cipher-suite IDs that satisfy
15+
// the given cluster TLS security profile. A nil profile (no cluster policy
16+
// configured) falls back to the OpenShift default, Intermediate.
17+
//
18+
// The OpenSSL cipher names in the profile are translated to Go cipher IDs via
19+
// library-go's crypto helpers (the same mapping OpenShift itself uses), so it
20+
// stays correct as ciphers evolve. Cipher names Go does not implement are
21+
// dropped by that mapping. For TLS 1.3 (Modern) no cipher suites are returned,
22+
// because Go manages TLS 1.3 cipher suites itself and does not allow configuring
23+
// them.
24+
func Resolve(profile *configv1.TLSSecurityProfile) (minVersion uint16, cipherSuites []uint16) {
25+
spec := specFor(profile)
26+
27+
minVersion, err := crypto.TLSVersion(string(spec.MinTLSVersion))
28+
if err != nil {
29+
minVersion = tls.VersionTLS12
30+
}
31+
32+
if minVersion >= tls.VersionTLS13 {
33+
return minVersion, nil
34+
}
35+
36+
return minVersion, crypto.CipherSuitesOrDie(crypto.OpenSSLToIANACipherSuites(spec.Ciphers))
37+
}
38+
39+
// Apply sets the resolved minimum TLS version (and, for TLS < 1.3, the cipher
40+
// suites) from the cluster TLS security profile onto cfg.
41+
func Apply(cfg *tls.Config, profile *configv1.TLSSecurityProfile) {
42+
minVersion, cipherSuites := Resolve(profile)
43+
cfg.MinVersion = minVersion
44+
if minVersion < tls.VersionTLS13 {
45+
cfg.CipherSuites = cipherSuites
46+
}
47+
}
48+
49+
// protocolOrder lists TLS protocol versions low→high with their JVM/JSSE names,
50+
// used to derive the enabled-protocol set for operands (e.g. Quarkus).
51+
var protocolOrder = []struct {
52+
version uint16
53+
name string
54+
}{
55+
{tls.VersionTLS10, "TLSv1"},
56+
{tls.VersionTLS11, "TLSv1.1"},
57+
{tls.VersionTLS12, "TLSv1.2"},
58+
{tls.VersionTLS13, "TLSv1.3"},
59+
}
60+
61+
// QuarkusSSL returns the TLS protocol names and cipher-suite names (JVM/JSSE
62+
// naming) that honour the given cluster TLS security profile, for configuring a
63+
// Quarkus operand via `quarkus.http.ssl.protocols` / `quarkus.http.ssl.cipher-suites`.
64+
// Protocols are every TLS version >= the profile minimum; ciphers are the
65+
// profile's ciphers mapped to IANA/JVM names (this includes the TLS 1.3 cipher
66+
// names, which the JVM — unlike Go's crypto/tls — accepts in cipher-suites). A
67+
// nil profile yields the Intermediate default, matching the operator's own
68+
// endpoints.
69+
func QuarkusSSL(profile *configv1.TLSSecurityProfile) (protocols []string, ciphers []string) {
70+
minVersion, _ := Resolve(profile)
71+
for _, p := range protocolOrder {
72+
if p.version >= minVersion {
73+
protocols = append(protocols, p.name)
74+
}
75+
}
76+
77+
return protocols, crypto.OpenSSLToIANACipherSuites(specFor(profile).Ciphers)
78+
}
79+
80+
// specFor returns the TLSProfileSpec (ciphers + min version) for the given
81+
// profile: the caller's Custom spec for a Custom profile, the canonical
82+
// definition for a named profile, or the Intermediate default when the profile
83+
// is nil or its type is unset/unknown.
84+
func specFor(profile *configv1.TLSSecurityProfile) configv1.TLSProfileSpec {
85+
if profile != nil {
86+
if profile.Type == configv1.TLSProfileCustomType {
87+
if profile.Custom != nil {
88+
return profile.Custom.TLSProfileSpec
89+
}
90+
} else if spec, ok := configv1.TLSProfiles[profile.Type]; ok {
91+
return *spec
92+
}
93+
}
94+
95+
return *configv1.TLSProfiles[configv1.TLSProfileIntermediateType]
96+
}

0 commit comments

Comments
 (0)