From 67fdae85222d11d4a8b45d046c1f34a1c339a4b3 Mon Sep 17 00:00:00 2001 From: Marcus Kok Date: Fri, 17 Jul 2026 11:20:23 -0400 Subject: [PATCH 1/2] PROJQUAY-10703: feat(tls): propagate PQC groups for Modern profiles --- controllers/quay/tls.go | 15 ++++++++++++-- controllers/quay/tls_test.go | 13 ++++++++++++ pkg/context/context.go | 1 + pkg/kustomize/kustomize.go | 5 +++++ pkg/kustomize/kustomize_test.go | 20 +++++++++++++++++++ .../tls_security_profile/chainsaw-test.yaml | 13 ++++++++++++ 6 files changed, 65 insertions(+), 2 deletions(-) diff --git a/controllers/quay/tls.go b/controllers/quay/tls.go index 8827c35c1..08f3b1268 100644 --- a/controllers/quay/tls.go +++ b/controllers/quay/tls.go @@ -18,8 +18,9 @@ import ( // checkTLSSecurityProfile reads the cluster-wide TLS security profile from the // OpenShift APIServer resource and populates the QuayRegistryContext with the -// corresponding SSL_PROTOCOLS and SSL_CIPHERS values. If the user has already -// set these values in config.yaml, this function is a no-op. +// corresponding SSL_PROTOCOLS, SSL_CIPHERS, SSL_CIPHERSUITES, and +// SSL_ECDH_CURVES values. If the user has already set any TLS values in +// config.yaml, this function is a no-op. func (r *QuayRegistryReconciler) checkTLSSecurityProfile( ctx context.Context, qctx *quaycontext.QuayRegistryContext, @@ -63,12 +64,22 @@ func (r *QuayRegistryReconciler) checkTLSSecurityProfile( qctx.SSLProtocols = protocols qctx.SSLCiphers = ciphers qctx.SSLCiphersuites = ciphersuites + qctx.SSLECDHCurves = tlsECDHCurves(apiServer.Spec.TLSSecurityProfile) return nil } // translateTLSProfile converts an OpenShift TLSSecurityProfile into // space-separated protocol versions (nginx format) and colon-separated cipher // names (OpenSSL format). +const modernSSLECDHCurves = "X25519MLKEM768:X25519:prime256v1" + +func tlsECDHCurves(profile *configv1.TLSSecurityProfile) string { + if profile != nil && profile.Type == configv1.TLSProfileModernType { + return modernSSLECDHCurves + } + return "" +} + func translateTLSProfile(profile *configv1.TLSSecurityProfile) (protocols, ciphers, ciphersuites string) { translate := func(spec *configv1.TLSProfileSpec) (string, string, string) { tls12, tls13 := splitCiphers(spec.Ciphers) diff --git a/controllers/quay/tls_test.go b/controllers/quay/tls_test.go index 6165ab0f2..47dbc78e3 100644 --- a/controllers/quay/tls_test.go +++ b/controllers/quay/tls_test.go @@ -212,6 +212,10 @@ func TestCheckTLSSecurityProfile_UserOverride(t *testing.T) { name: "user set SSL_CIPHERSUITES", configYAML: "SSL_CIPHERSUITES:\n- TLS_AES_128_GCM_SHA256", }, + { + name: "user set SSL_ECDH_CURVES", + configYAML: "SSL_ECDH_CURVES:\n- X25519", + }, } { t.Run(tt.name, func(t *testing.T) { qctx := quaycontext.NewQuayRegistryContext() @@ -232,6 +236,9 @@ func TestCheckTLSSecurityProfile_UserOverride(t *testing.T) { if qctx.SSLCiphers != "" { t.Errorf("SSLCiphers should be empty, got %q", qctx.SSLCiphers) } + if qctx.SSLECDHCurves != "" { + t.Errorf("SSLECDHCurves should be empty, got %q", qctx.SSLECDHCurves) + } }) } } @@ -304,6 +311,9 @@ func TestCheckTLSSecurityProfile_WithAPIServer(t *testing.T) { if qctx.SSLCiphersuites == "" { t.Error("expected non-empty SSLCiphersuites for Modern profile") } + if qctx.SSLECDHCurves != modernSSLECDHCurves { + t.Errorf("SSLECDHCurves = %q, want %q", qctx.SSLECDHCurves, modernSSLECDHCurves) + } } func TestCheckTLSSecurityProfile_NilProfile(t *testing.T) { @@ -339,4 +349,7 @@ func TestCheckTLSSecurityProfile_NilProfile(t *testing.T) { if qctx.SSLProtocols != "TLSv1.2 TLSv1.3" { t.Errorf("SSLProtocols = %q, want %q", qctx.SSLProtocols, "TLSv1.2 TLSv1.3") } + if qctx.SSLECDHCurves != "" { + t.Errorf("SSLECDHCurves should be empty for Intermediate profile, got %q", qctx.SSLECDHCurves) + } } diff --git a/pkg/context/context.go b/pkg/context/context.go index 5ae30e4ec..8235abd49 100644 --- a/pkg/context/context.go +++ b/pkg/context/context.go @@ -22,6 +22,7 @@ type QuayRegistryContext struct { SSLProtocols string // e.g. "TLSv1.2 TLSv1.3" (nginx format) SSLCiphers string // e.g. "ECDHE-RSA-AES128-GCM-SHA256:..." (TLS 1.2 ciphers, OpenSSL format) SSLCiphersuites string // e.g. "TLS_AES_128_GCM_SHA256:..." (TLS 1.3 ciphersuites, OpenSSL format) + SSLECDHCurves string // e.g. "X25519MLKEM768:X25519:prime256v1" (OpenSSL supported groups) // Object Storage SupportsObjectStorage bool diff --git a/pkg/kustomize/kustomize.go b/pkg/kustomize/kustomize.go index 4e7e750d1..63358f29d 100644 --- a/pkg/kustomize/kustomize.go +++ b/pkg/kustomize/kustomize.go @@ -1007,6 +1007,11 @@ func Inflate( parsedUserConfig["SSL_CIPHERSUITES"] = strings.Split(ctx.SSLCiphersuites, ":") } } + if ctx.SSLECDHCurves != "" { + if _, ok := parsedUserConfig["SSL_ECDH_CURVES"]; !ok { + parsedUserConfig["SSL_ECDH_CURVES"] = strings.Split(ctx.SSLECDHCurves, ":") + } + } programmaticBootstrapEnabled := ProgrammaticBootstrapEnabled(parsedUserConfig) if programmaticBootstrapEnabled { diff --git a/pkg/kustomize/kustomize_test.go b/pkg/kustomize/kustomize_test.go index b41f742f7..930e85a2a 100644 --- a/pkg/kustomize/kustomize_test.go +++ b/pkg/kustomize/kustomize_test.go @@ -953,6 +953,26 @@ func TestInflate(t *testing.T) { } } +func TestInflateInjectsSSLECDHCurves(t *testing.T) { + log := testlogr.NewTestLogger(t) + test := inflateTests[0] + test.ctx.SSLECDHCurves = "X25519MLKEM768:X25519:prime256v1" + + pieces, err := Inflate(&test.ctx, test.quayRegistry, test.configBundle, log, false) + assert.NoError(t, err) + + for _, obj := range pieces { + objectMeta, _ := meta.Accessor(obj) + if strings.Contains(objectMeta.GetName(), configSecretPrefix) { + configBundle := obj.(*corev1.Secret) + config := decode(configBundle.Data["config.yaml"]).(map[string]interface{}) + assert.Equal(t, []interface{}{"X25519MLKEM768", "X25519", "prime256v1"}, config["SSL_ECDH_CURVES"]) + return + } + } + t.Fatal("generated Quay config secret not found") +} + func TestInflatePushgatewayURLInjected(t *testing.T) { log := testlogr.NewTestLogger(t) ctx := quaycontext.QuayRegistryContext{} diff --git a/test/chainsaw/tls_security_profile/chainsaw-test.yaml b/test/chainsaw/tls_security_profile/chainsaw-test.yaml index 9f6369d64..7aa388d32 100644 --- a/test/chainsaw/tls_security_profile/chainsaw-test.yaml +++ b/test/chainsaw/tls_security_profile/chainsaw-test.yaml @@ -85,3 +85,16 @@ spec: echo "FAIL: SSL_CIPHERS not found in Quay config (TLS is unmanaged, should be injected)" exit 1 fi + + PROFILE_TYPE=$(kubectl get apiserver cluster -o jsonpath='{.spec.tlsSecurityProfile.type}' 2>/dev/null || echo "") + if [ "$PROFILE_TYPE" = "Modern" ]; then + echo "$CONFIG" | grep -q "SSL_ECDH_CURVES" || { + echo "FAIL: SSL_ECDH_CURVES not found for Modern profile" + exit 1 + } + echo "$CONFIG" | grep -q "X25519MLKEM768" || { + echo "FAIL: Modern profile did not inject X25519MLKEM768" + exit 1 + } + echo "PASS: Modern profile injected PQC supported groups" + fi From ba2e978970e551d9adb6f4c1152b53e7522aecd5 Mon Sep 17 00:00:00 2001 From: Marcus Kok Date: Fri, 17 Jul 2026 12:46:29 -0400 Subject: [PATCH 2/2] test(tls): address CodeRabbit review findings --- controllers/quay/tls.go | 3 +++ pkg/kustomize/kustomize_test.go | 2 +- test/chainsaw/tls_security_profile/chainsaw-test.yaml | 6 +++++- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/controllers/quay/tls.go b/controllers/quay/tls.go index 08f3b1268..8360f310c 100644 --- a/controllers/quay/tls.go +++ b/controllers/quay/tls.go @@ -43,6 +43,9 @@ func (r *QuayRegistryReconciler) checkTLSSecurityProfile( if _, ok := config["SSL_CIPHERSUITES"]; ok { return nil } + if _, ok := config["SSL_ECDH_CURVES"]; ok { + return nil + } // Try to read the APIServer "cluster" resource. var apiServer configv1.APIServer diff --git a/pkg/kustomize/kustomize_test.go b/pkg/kustomize/kustomize_test.go index 930e85a2a..ed910785f 100644 --- a/pkg/kustomize/kustomize_test.go +++ b/pkg/kustomize/kustomize_test.go @@ -959,7 +959,7 @@ func TestInflateInjectsSSLECDHCurves(t *testing.T) { test.ctx.SSLECDHCurves = "X25519MLKEM768:X25519:prime256v1" pieces, err := Inflate(&test.ctx, test.quayRegistry, test.configBundle, log, false) - assert.NoError(t, err) + require.NoError(t, err) for _, obj := range pieces { objectMeta, _ := meta.Accessor(obj) diff --git a/test/chainsaw/tls_security_profile/chainsaw-test.yaml b/test/chainsaw/tls_security_profile/chainsaw-test.yaml index 7aa388d32..1c2f6e7b0 100644 --- a/test/chainsaw/tls_security_profile/chainsaw-test.yaml +++ b/test/chainsaw/tls_security_profile/chainsaw-test.yaml @@ -86,7 +86,11 @@ spec: exit 1 fi - PROFILE_TYPE=$(kubectl get apiserver cluster -o jsonpath='{.spec.tlsSecurityProfile.type}' 2>/dev/null || echo "") + if ! PROFILE_TYPE=$(kubectl get apiserver cluster \ + -o jsonpath='{.spec.tlsSecurityProfile.type}'); then + echo "FAIL: unable to read apiserver cluster TLS profile" + exit 1 + fi if [ "$PROFILE_TYPE" = "Modern" ]; then echo "$CONFIG" | grep -q "SSL_ECDH_CURVES" || { echo "FAIL: SSL_ECDH_CURVES not found for Modern profile"