From 60cf2ba0ad3d07f00ababe510dd962beabd115be Mon Sep 17 00:00:00 2001 From: Evgeny Slutsky Date: Mon, 4 May 2026 16:44:25 +0200 Subject: [PATCH 1/5] 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 e9bdf7af50..208774c83a 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 d65e6234ec38debcf1886c231da6ece17e7287f4 Mon Sep 17 00:00:00 2001 From: Evgeny Slutsky Date: Mon, 4 May 2026 16:44:58 +0200 Subject: [PATCH 2/5] 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 208774c83a..dccd784adc 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 @@ -503,6 +508,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 08527714cd7da329856b60e5b391469a6c681a42 Mon Sep 17 00:00:00 2001 From: Evgeny Slutsky Date: Mon, 4 May 2026 16:45:20 +0200 Subject: [PATCH 3/5] 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 dccd784adc..290b7a16b2 100644 --- a/pkg/components/controllers.go +++ b/pkg/components/controllers.go @@ -528,6 +528,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. @@ -619,6 +627,7 @@ func generateIngressParams(cfg *config.Config) (assets.RenderParams, error) { "RouterCiphers": RouterCiphers, "RouterCiphersSuites": RouterCiphersSuites, "RouterSSLMinVersion": RouterSSLMinVersion, + "RouterTLSCurves": tlsCurves, "RouterAllowWildcardRoutes": RouterAllowWildcardRoutes, "ClientCAMapName": clientCAMapName, "ClientAuthPolicy": clientAuthPolicy, From 9e1e64a61da02a85d98cd532f03f0615d51a498b Mon Sep 17 00:00:00 2001 From: Evgeny Slutsky Date: Wed, 27 May 2026 12:52:14 +0200 Subject: [PATCH 4/5] add test for ingress Post Quantum curves support Signed-off-by: Evgeny Slutsky --- test/suites/optional/tls-scanner.robot | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/test/suites/optional/tls-scanner.robot b/test/suites/optional/tls-scanner.robot index 1a26973871..b29f18b3d3 100644 --- a/test/suites/optional/tls-scanner.robot +++ b/test/suites/optional/tls-scanner.robot @@ -49,6 +49,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 *** Check Required Scanner Variables @@ -123,3 +127,19 @@ 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. + ${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 a9aaf1e343500651461c3fceadfb05af790a4abd Mon Sep 17 00:00:00 2001 From: Evgeny Slutsky Date: Mon, 8 Jun 2026 12:22:01 +0200 Subject: [PATCH 5/5] address PR review comments Signed-off-by: Evgeny Slutsky --- pkg/components/controllers.go | 10 +- pkg/components/controllers_test.go | 139 +++++++++++++++++++++++++ test/suites/optional/tls-scanner.robot | 4 + 3 files changed, 148 insertions(+), 5 deletions(-) create mode 100644 pkg/components/controllers_test.go diff --git a/pkg/components/controllers.go b/pkg/components/controllers.go index 290b7a16b2..fdde5b9c6b 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 } @@ -467,7 +467,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" @@ -512,7 +512,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) { @@ -532,7 +532,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 b29f18b3d3..367ca307c8 100644 --- a/test/suites/optional/tls-scanner.robot +++ b/test/suites/optional/tls-scanner.robot @@ -133,6 +133,10 @@ Verify ML-KEM Post Quantum Curve Negotiation ... 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}