From 99785b8588cd3d1d99c2533947b525c96ad8bd67 Mon Sep 17 00:00:00 2001 From: Evgeny Slutsky Date: Mon, 4 May 2026 16:44:25 +0200 Subject: [PATCH 1/7] NO-ISSUE: Add FIPS detection for ingress controller Introduce detectFIPS() to check whether the cluster is running in FIPS mode via the FIPS_ENABLED env var or /proc/sys/crypto/fips_enabled. The result is stored in the package-level isFIPSEnabled variable for use by subsequent FIPS-aware configuration logic. Co-Authored-By: Claude Opus 4.6 --- pkg/components/controllers.go | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/pkg/components/controllers.go b/pkg/components/controllers.go index 131ad50472..7b56eca7f2 100644 --- a/pkg/components/controllers.go +++ b/pkg/components/controllers.go @@ -27,6 +27,9 @@ const ( haproxyMaxTimeoutMilliseconds = 2147483647 * time.Millisecond ) +// isFIPSEnabled reports whether the cluster has FIPS enabled. +var isFIPSEnabled = detectFIPS() + var ( tlsVersion13Ciphers = sets.NewString( "TLS_AES_128_GCM_SHA256", @@ -37,6 +40,34 @@ var ( ) ) +// detectFIPS reports whether the cluster is operating in FIPS +// mode by checking the FIPS_ENABLED environment variable if set or +// the /proc/sys/crypto/fips_enabled file otherwise. +func detectFIPS() bool { + if v, ok := os.LookupEnv("FIPS_ENABLED"); ok { + if result, err := strconv.ParseBool(v); err != nil { + klog.Warningf("Failed to parse FIPS_ENABLED environment variable: %v; falling back to procfs", err) + } else { + klog.Infof("Found FIPS_ENABLED environment variable: value=%s, result=%v", v, result) + return result + } + } + + result := false + data, err := os.ReadFile("/proc/sys/crypto/fips_enabled") + if err != nil { + klog.Warningf("Failed to read /proc/sys/crypto/fips_enabled: %v; assuming FIPS is not enabled", err) + return result + } + if len(data) == 0 { + klog.Warningf("Got empty /proc/sys/crypto/fips_enabled; assuming FIPS is not enabled") + return result + } + result = data[0] == '1' + klog.Infof("Read /proc/sys/crypto/fips_enabled: data=%s, result=%v", string(data), result) + return result +} + func startServiceCAController(ctx context.Context, cfg *config.Config, kubeconfigPath string) error { var ( //TODO: fix the rolebinding and sa From 48be008e4371e581c30abd9eede2ec28b355ca22 Mon Sep 17 00:00:00 2001 From: Evgeny Slutsky Date: Mon, 4 May 2026 16:44:58 +0200 Subject: [PATCH 2/7] NO-ISSUE: Filter non-FIPS TLS 1.3 ciphers in ingress router On FIPS-enabled clusters, remove non-FIPS-compliant TLS 1.3 cipher suites (e.g. TLS_CHACHA20_POLY1305_SHA256) from ROUTER_CIPHERSUITES. HAProxy would fail TLS handshakes when a client offers a non-FIPS cipher that is listed in ssl-default-bind-ciphersuites but excluded by the OS FIPS policy. Co-Authored-By: Claude Opus 4.6 --- pkg/components/controllers.go | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/pkg/components/controllers.go b/pkg/components/controllers.go index 7b56eca7f2..8b2d5579ee 100644 --- a/pkg/components/controllers.go +++ b/pkg/components/controllers.go @@ -38,6 +38,11 @@ var ( "TLS_AES_128_CCM_SHA256", "TLS_AES_128_CCM_8_SHA256", ) + + fipsApprovedTLS13Ciphers = sets.NewString( + "TLS_AES_128_GCM_SHA256", + "TLS_AES_256_GCM_SHA384", + ) ) // detectFIPS reports whether the cluster is operating in FIPS @@ -526,6 +531,20 @@ func generateIngressParams(cfg *config.Config) (assets.RenderParams, error) { } } + // On FIPS-enabled clusters, remove non-FIPS-compliant TLS 1.3 cipher + // suites (e.g. TLS_CHACHA20_POLY1305_SHA256). HAProxy would fail TLS + // handshakes when a client offers a non-FIPS cipher first if that cipher + // is listed in ssl-default-bind-ciphersuites but excluded by the OS FIPS policy. + if isFIPSEnabled { + fipsCiphers := tls13Ciphers[:0] + for _, c := range tls13Ciphers { + if fipsApprovedTLS13Ciphers.Has(c) { + fipsCiphers = append(fipsCiphers, c) + } + } + tls13Ciphers = fipsCiphers + } + RouterCiphers := strings.Join(otherCiphers, ":") RouterCiphersSuites := "" if len(tls13Ciphers) != 0 { From 02d9b5f0be63e0c3e9f31b2c679bce121259c732 Mon Sep 17 00:00:00 2001 From: Evgeny Slutsky Date: Mon, 4 May 2026 16:45:20 +0200 Subject: [PATCH 3/7] NO-ISSUE: Configure TLS curves based on FIPS mode for PQC readiness Set ROUTER_CURVES on the ingress router deployment to configure TLS supportedGroups. Non-FIPS clusters use X25519MLKEM768:X25519:P-256:P-384:P-521 (including post-quantum ML-KEM). FIPS clusters use P-256:P-384:P-521 only, since ML-KEM and X25519 are not supported by OpenSSL FIPS 140-3. Co-Authored-By: Claude Opus 4.6 --- assets/components/openshift-router/deployment.yaml | 2 ++ pkg/components/controllers.go | 9 +++++++++ 2 files changed, 11 insertions(+) diff --git a/assets/components/openshift-router/deployment.yaml b/assets/components/openshift-router/deployment.yaml index af1f48f67f..cb2c7b5f31 100644 --- a/assets/components/openshift-router/deployment.yaml +++ b/assets/components/openshift-router/deployment.yaml @@ -58,6 +58,8 @@ spec: value: '{{ .ThreadCount }}' - name: SSL_MIN_VERSION value: '{{ .RouterSSLMinVersion }}' + - name: ROUTER_CURVES + value: '{{ .RouterTLSCurves }}' - name: ROUTER_USE_PROXY_PROTOCOL value: "false" - name: GRACEFUL_SHUTDOWN_DELAY diff --git a/pkg/components/controllers.go b/pkg/components/controllers.go index 8b2d5579ee..3f8d9bc9bd 100644 --- a/pkg/components/controllers.go +++ b/pkg/components/controllers.go @@ -551,6 +551,14 @@ func generateIngressParams(cfg *config.Config) (assets.RenderParams, error) { RouterCiphersSuites = strings.Join(tls13Ciphers, ":") } + // Default TLS supportedGroups (curves) include X25519MLKEM768 for + // post-quantum readiness. In FIPS mode, ML-KEM and X25519 are not + // supported by OpenSSL FIPS 140-3. + tlsCurves := "X25519MLKEM768:X25519:P-256:P-384:P-521" + if isFIPSEnabled { + tlsCurves = "P-256:P-384:P-521" + } + var RouterSSLMinVersion string switch tlsProfileSpec.MinTLSVersion { // TLS 1.0 is not supported, convert to TLS 1.1. @@ -642,6 +650,7 @@ func generateIngressParams(cfg *config.Config) (assets.RenderParams, error) { "RouterCiphers": RouterCiphers, "RouterCiphersSuites": RouterCiphersSuites, "RouterSSLMinVersion": RouterSSLMinVersion, + "RouterTLSCurves": tlsCurves, "RouterAllowWildcardRoutes": RouterAllowWildcardRoutes, "ClientCAMapName": clientCAMapName, "ClientAuthPolicy": clientAuthPolicy, From 8300b1d0781b5ac4a1521ff76dc1786392aa22fa Mon Sep 17 00:00:00 2001 From: Evgeny Slutsky Date: Wed, 27 May 2026 12:52:14 +0200 Subject: [PATCH 4/7] add test for ingress Post Quantum curves support Signed-off-by: Evgeny Slutsky --- pkg/components/controllers.go | 10 +- pkg/components/controllers_test.go | 139 +++++++++++++++++++++++++ test/suites/optional/tls-scanner.robot | 24 +++++ 3 files changed, 168 insertions(+), 5 deletions(-) create mode 100644 pkg/components/controllers_test.go diff --git a/pkg/components/controllers.go b/pkg/components/controllers.go index 3f8d9bc9bd..13489f3bf7 100644 --- a/pkg/components/controllers.go +++ b/pkg/components/controllers.go @@ -68,7 +68,7 @@ func detectFIPS() bool { klog.Warningf("Got empty /proc/sys/crypto/fips_enabled; assuming FIPS is not enabled") return result } - result = data[0] == '1' + result = strings.TrimSpace(string(data)) == "1" klog.Infof("Read /proc/sys/crypto/fips_enabled: data=%s, result=%v", string(data), result) return result } @@ -266,7 +266,7 @@ func startIngressController(ctx context.Context, cfg *config.Config, kubeconfigP return err } - extraParams, err := generateIngressParams(cfg) + extraParams, err := generateIngressParams(cfg, isFIPSEnabled) if err != nil { return err } @@ -490,7 +490,7 @@ func validateClientTLS(patterns []string) error { return nil } -func generateIngressParams(cfg *config.Config) (assets.RenderParams, error) { +func generateIngressParams(cfg *config.Config, fipsEnabled bool) (assets.RenderParams, error) { routerMode := "v4" if cfg.IsIPv6() { routerMode = "v4v6" @@ -535,7 +535,7 @@ func generateIngressParams(cfg *config.Config) (assets.RenderParams, error) { // suites (e.g. TLS_CHACHA20_POLY1305_SHA256). HAProxy would fail TLS // handshakes when a client offers a non-FIPS cipher first if that cipher // is listed in ssl-default-bind-ciphersuites but excluded by the OS FIPS policy. - if isFIPSEnabled { + if fipsEnabled { fipsCiphers := tls13Ciphers[:0] for _, c := range tls13Ciphers { if fipsApprovedTLS13Ciphers.Has(c) { @@ -555,7 +555,7 @@ func generateIngressParams(cfg *config.Config) (assets.RenderParams, error) { // post-quantum readiness. In FIPS mode, ML-KEM and X25519 are not // supported by OpenSSL FIPS 140-3. tlsCurves := "X25519MLKEM768:X25519:P-256:P-384:P-521" - if isFIPSEnabled { + if fipsEnabled { tlsCurves = "P-256:P-384:P-521" } diff --git a/pkg/components/controllers_test.go b/pkg/components/controllers_test.go new file mode 100644 index 0000000000..5e0e700c78 --- /dev/null +++ b/pkg/components/controllers_test.go @@ -0,0 +1,139 @@ +package components + +import ( + "strings" + "testing" + "time" + + configv1 "github.com/openshift/api/config/v1" + "github.com/openshift/microshift/pkg/assets" + "github.com/openshift/microshift/pkg/config" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" +) + +func newTestConfig() *config.Config { + return &config.Config{ + DNS: config.DNS{BaseDomain: "example.com"}, + Network: config.Network{ + ClusterNetwork: []string{"10.42.0.0/16"}, + ServiceNetwork: []string{"10.43.0.0/16"}, + }, + Ingress: config.IngressConfig{ + Ports: config.IngressPortsConfig{ + Http: ptr.To(80), + Https: ptr.To(443), + }, + TuningOptions: config.IngressControllerTuningOptions{ + HeaderBufferBytes: 32768, + HeaderBufferMaxRewriteBytes: 8192, + HealthCheckInterval: &metav1.Duration{Duration: 5 * time.Second}, + ClientTimeout: &metav1.Duration{Duration: 30 * time.Second}, + ClientFinTimeout: &metav1.Duration{Duration: 1 * time.Second}, + ServerTimeout: &metav1.Duration{Duration: 30 * time.Second}, + ServerFinTimeout: &metav1.Duration{Duration: 1 * time.Second}, + TunnelTimeout: &metav1.Duration{Duration: 1 * time.Hour}, + TLSInspectDelay: &metav1.Duration{Duration: 5 * time.Second}, + ThreadCount: 4, + MaxConnections: 50000, + }, + ForwardedHeaderPolicy: "Append", + TLSSecurityProfile: &configv1.TLSSecurityProfile{ + Type: configv1.TLSProfileIntermediateType, + }, + ServingCertificateSecret: "router-certs-default", + DefaultHttpVersionPolicy: 1, + LogEmptyRequests: "Log", + HTTPEmptyRequestsPolicy: "Respond", + AccessLogging: config.AccessLogging{ + Status: config.AccessLoggingDisabled, + }, + }, + } +} + +func requireStringParam(t *testing.T, params assets.RenderParams, key string) string { + t.Helper() + v, ok := params[key] + if !ok { + t.Fatalf("missing param %q", key) + } + s, ok := v.(string) + if !ok { + t.Fatalf("param %q has type %T, want string", key, v) + } + return s +} + +func TestGenerateIngressParamsFIPSCiphers(t *testing.T) { + t.Run("FIPS enabled filters non-FIPS TLS 1.3 ciphers", func(t *testing.T) { + cfg := newTestConfig() + params, err := generateIngressParams(cfg, true) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + cipherSuites := requireStringParam(t, params, "RouterCiphersSuites") + if strings.Contains(cipherSuites, "TLS_CHACHA20_POLY1305_SHA256") { + t.Errorf("FIPS mode should filter out TLS_CHACHA20_POLY1305_SHA256, got: %s", cipherSuites) + } + if !strings.Contains(cipherSuites, "TLS_AES_128_GCM_SHA256") { + t.Errorf("FIPS mode should keep TLS_AES_128_GCM_SHA256, got: %s", cipherSuites) + } + if !strings.Contains(cipherSuites, "TLS_AES_256_GCM_SHA384") { + t.Errorf("FIPS mode should keep TLS_AES_256_GCM_SHA384, got: %s", cipherSuites) + } + }) + + t.Run("non-FIPS keeps all TLS 1.3 ciphers", func(t *testing.T) { + cfg := newTestConfig() + params, err := generateIngressParams(cfg, false) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + cipherSuites := requireStringParam(t, params, "RouterCiphersSuites") + if !strings.Contains(cipherSuites, "TLS_CHACHA20_POLY1305_SHA256") { + t.Errorf("non-FIPS mode should keep TLS_CHACHA20_POLY1305_SHA256, got: %s", cipherSuites) + } + }) +} + +func TestGenerateIngressParamsFIPSCurves(t *testing.T) { + t.Run("FIPS enabled uses only NIST curves", func(t *testing.T) { + cfg := newTestConfig() + params, err := generateIngressParams(cfg, true) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + curves := requireStringParam(t, params, "RouterTLSCurves") + if strings.Contains(curves, "X25519MLKEM768") { + t.Errorf("FIPS mode should exclude X25519MLKEM768, got: %s", curves) + } + for _, c := range strings.Split(curves, ":") { + if c == "X25519" { + t.Errorf("FIPS mode should exclude X25519, got: %s", curves) + } + } + if curves != "P-256:P-384:P-521" { + t.Errorf("FIPS mode curves should be P-256:P-384:P-521, got: %s", curves) + } + }) + + t.Run("non-FIPS includes PQC hybrid curve", func(t *testing.T) { + cfg := newTestConfig() + params, err := generateIngressParams(cfg, false) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + curves := requireStringParam(t, params, "RouterTLSCurves") + if !strings.Contains(curves, "X25519MLKEM768") { + t.Errorf("non-FIPS mode should include X25519MLKEM768, got: %s", curves) + } + if curves != "X25519MLKEM768:X25519:P-256:P-384:P-521" { + t.Errorf("non-FIPS mode curves should be X25519MLKEM768:X25519:P-256:P-384:P-521, got: %s", curves) + } + }) +} diff --git a/test/suites/optional/tls-scanner.robot b/test/suites/optional/tls-scanner.robot index 4add7f2202..1a74acc62c 100644 --- a/test/suites/optional/tls-scanner.robot +++ b/test/suites/optional/tls-scanner.robot @@ -51,6 +51,10 @@ TLS Scanner Host Scan Completes And Produces Artifacts ... Cleanup TLS Scanner Job ... Ensure Cluster Reader Role Deleted +Ingress Router TLS Curves supports ML-KEM Post Quantum Curves + [Documentation] Verify TLS curve negotiation with openssl from inside the router pod. + Verify ML-KEM Post Quantum Curve Negotiation + *** Keywords *** Setup @@ -137,3 +141,23 @@ Cleanup TLS Scanner Job IF '${TLS_SCANNER_DIR}' != '' Run Keyword And Ignore Error Remove Directory ${TLS_SCANNER_DIR} recursive=True END + +Verify ML-KEM Post Quantum Curve Negotiation + [Documentation] Verify X25519MLKEM768 post-quantum hybrid key exchange + ... negotiates successfully via oc exec into the router pod, which + ... has OpenSSL 3.5+ (the host OpenSSL may be too old for ML-KEM). + ... Skipped on FIPS clusters where ML-KEM is not configured. + ${curves}= Oc Get JsonPath deployment openshift-ingress router-default + ... .spec.template.spec.containers[0].env[?(@.name=="ROUTER_CURVES")].value + Skip If "X25519MLKEM768" not in """${curves}""" + ... ROUTER_CURVES does not include X25519MLKEM768 (FIPS mode); skipping ML-KEM test + ${router_ip}= Oc Get JsonPath svc openshift-ingress router-default + ... .spec.clusterIP + ${pod_name}= Oc Get JsonPath pod openshift-ingress ${EMPTY} + ... .items[0].metadata.name + ${output}= Oc Exec ${pod_name} + ... echo Q | openssl s_client -connect ${router_ip}:443 -groups X25519MLKEM768 2>&1 || true + ... ns=openshift-ingress + Should Contain ${output} Negotiated TLS1.3 group: X25519MLKEM768 + ... msg=ML-KEM post-quantum curve X25519MLKEM768 negotiation failed + Log Post-quantum ML-KEM negotiation verified: OK From b86dbab7efe5c955adeaa5c3105b17ba951fdb60 Mon Sep 17 00:00:00 2001 From: Evgeny Slutsky Date: Mon, 6 Jul 2026 15:02:01 +0200 Subject: [PATCH 5/7] Use regex for ML-KEM curve negotiation assertion Replace exact string match with regex (group.*X25519MLKEM768) to make the test more resilient to openssl output format changes. Co-Authored-By: Claude Opus 4.6 --- test/suites/optional/tls-scanner.robot | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/suites/optional/tls-scanner.robot b/test/suites/optional/tls-scanner.robot index 1a74acc62c..1725dcfd30 100644 --- a/test/suites/optional/tls-scanner.robot +++ b/test/suites/optional/tls-scanner.robot @@ -158,6 +158,6 @@ Verify ML-KEM Post Quantum Curve Negotiation ${output}= Oc Exec ${pod_name} ... echo Q | openssl s_client -connect ${router_ip}:443 -groups X25519MLKEM768 2>&1 || true ... ns=openshift-ingress - Should Contain ${output} Negotiated TLS1.3 group: X25519MLKEM768 + Should Match Regexp ${output} group.*X25519MLKEM768 ... msg=ML-KEM post-quantum curve X25519MLKEM768 negotiation failed Log Post-quantum ML-KEM negotiation verified: OK From b20fffc183c4ef57ce44c02f228998a2ccbe4fcb Mon Sep 17 00:00:00 2001 From: Evgeny Slutsky Date: Mon, 6 Jul 2026 15:01:56 +0200 Subject: [PATCH 6/7] Move FIPS detection from package-level init to call site Calling detectFIPS() at the package level triggers I/O (procfs read) on import. Move it to startIngressController where the result is actually needed, avoiding side effects during package initialization. Co-Authored-By: Claude Opus 4.6 --- pkg/components/controllers.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pkg/components/controllers.go b/pkg/components/controllers.go index 13489f3bf7..c8320de3b9 100644 --- a/pkg/components/controllers.go +++ b/pkg/components/controllers.go @@ -27,9 +27,6 @@ const ( haproxyMaxTimeoutMilliseconds = 2147483647 * time.Millisecond ) -// isFIPSEnabled reports whether the cluster has FIPS enabled. -var isFIPSEnabled = detectFIPS() - var ( tlsVersion13Ciphers = sets.NewString( "TLS_AES_128_GCM_SHA256", @@ -266,7 +263,8 @@ func startIngressController(ctx context.Context, cfg *config.Config, kubeconfigP return err } - extraParams, err := generateIngressParams(cfg, isFIPSEnabled) + fipsEnabled := detectFIPS() + extraParams, err := generateIngressParams(cfg, fipsEnabled) if err != nil { return err } From 08f03176a21944f0295b560b74ccc90e8da08107 Mon Sep 17 00:00:00 2001 From: Evgeny Slutsky Date: Mon, 6 Jul 2026 17:45:42 +0200 Subject: [PATCH 7/7] Fix TLSSecurityProfile type mismatch in test Use config.TLSSecurityProfile (local type on main) instead of configv1.TLSSecurityProfile to match the IngressConfig field type after merge with main. Co-Authored-By: Claude Opus 4.6 --- pkg/components/controllers_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/components/controllers_test.go b/pkg/components/controllers_test.go index 5e0e700c78..c15ba684eb 100644 --- a/pkg/components/controllers_test.go +++ b/pkg/components/controllers_test.go @@ -38,7 +38,7 @@ func newTestConfig() *config.Config { MaxConnections: 50000, }, ForwardedHeaderPolicy: "Append", - TLSSecurityProfile: &configv1.TLSSecurityProfile{ + TLSSecurityProfile: &config.TLSSecurityProfile{ Type: configv1.TLSProfileIntermediateType, }, ServingCertificateSecret: "router-certs-default",