From 98e10b432be849040fdf186c654e6c1eeef2729c Mon Sep 17 00:00:00 2001 From: akhil nittala Date: Fri, 9 Oct 2026 21:51:10 +0530 Subject: [PATCH 1/5] feat(tls): Configure TLS Curve Preferences for Redis HA Proxy Signed-off-by: akhil nittala --- argocd-operator/common/defaults.go | 5 +- argocd-operator/controllers/argoutil/redis.go | 38 +++++++ .../controllers/argoutil/redis_test.go | 107 +++++++++++++++++- build/redis/haproxy.cfg.tpl | 5 + 4 files changed, 151 insertions(+), 4 deletions(-) diff --git a/argocd-operator/common/defaults.go b/argocd-operator/common/defaults.go index 24f4ed13a88..21f80bf127f 100644 --- a/argocd-operator/common/defaults.go +++ b/argocd-operator/common/defaults.go @@ -197,10 +197,11 @@ const ( ArgoCDDefaultRedisHAReplicas = int32(3) // ArgoCDDefaultRedisHAProxyImage is the default Redis HAProxy image to use when not specified. - ArgoCDDefaultRedisHAProxyImage = "public.ecr.aws/docker/library/haproxy" + // haproxytech builds use AWS-LC, which supports post-quantum TLS groups (e.g. X25519MLKEM768). + ArgoCDDefaultRedisHAProxyImage = "docker.io/haproxytech/haproxy-alpine" // ArgoCDDefaultRedisHAProxyVersion is the default Redis HAProxy image tag to use when not specified. - ArgoCDDefaultRedisHAProxyVersion = "sha256:e11f034e651603f10a365e5ad5a0321825e18eded9620e40c4f4d6ae58419bfe" // 3.0.8-alpine + ArgoCDDefaultRedisHAProxyVersion = "sha256:4b2da4adb652487a1aa1bb7fdacf5df1088d2c7d21302fcce738812d0d2fc453" // 3.3.6 // ArgoCDDefaultRedisImage is the Redis container image to use when not specified. ArgoCDDefaultRedisImage = "public.ecr.aws/docker/library/redis" diff --git a/argocd-operator/controllers/argoutil/redis.go b/argocd-operator/controllers/argoutil/redis.go index e1fbdfade74..724206d7b2a 100644 --- a/argocd-operator/controllers/argoutil/redis.go +++ b/argocd-operator/controllers/argoutil/redis.go @@ -186,6 +186,40 @@ func GetRedisInitScript(cr *argoproj.ArgoCD, useTLSForRedis bool) string { return script } +// haproxySupportedCurves maps OpenShift TLS group names to HAProxy/AWS-LC curve names. +// Classical groups use NIST aliases (secp256r1 -> P-256). Post-quantum hybrid groups +// are passed through for HAProxy 3.3+ with AWS-LC. +var haproxySupportedCurves = map[string]string{ + // Classical curves (OpenShift IANA name -> HAProxy NIST name) + "X25519": "X25519", + "secp256r1": "P-256", + "secp384r1": "P-384", + "secp521r1": "P-521", + "P-256": "P-256", + "P-384": "P-384", + "P-521": "P-521", + // Post-quantum hybrid groups (same name in OpenShift and HAProxy/AWS-LC) + "X25519MLKEM768": "X25519MLKEM768", + "SecP256r1MLKEM768": "SecP256r1MLKEM768", + "SecP384r1MLKEM1024": "SecP384r1MLKEM1024", +} + +// MapCurvePreferencesToHAProxyCurves converts OpenShift TLS group names to a +// colon-separated HAProxy curves list. Classical OpenShift names are converted +// to NIST aliases (e.g. secp256r1 -> P-256). Unknown groups are skipped. +func MapCurvePreferencesToHAProxyCurves(groups []string) string { + if len(groups) == 0 { + return "" + } + curves := make([]string, 0, len(groups)) + for _, group := range groups { + if curve, ok := haproxySupportedCurves[group]; ok { + curves = append(curves, curve) + } + } + return strings.Join(curves, ":") +} + // GetRedisHAProxyConfig will load the Redis HA Proxy configuration from a template on disk for the given ArgoCD. // If an error occurs, an empty string value will be returned. func GetRedisHAProxyConfig(cr *argoproj.ArgoCD, useTLSForRedis bool, centralTLSConfigProfile tlsprofile.TLSConfigProfile) string { @@ -203,6 +237,10 @@ func GetRedisHAProxyConfig(cr *argoproj.ArgoCD, useTLSForRedis bool, centralTLSC vars["TLSCiphers"] = strings.Join(centralTLSConfigProfile.Ciphers, ":") } + if curves := MapCurvePreferencesToHAProxyCurves(centralTLSConfigProfile.CurvePreferences); curves != "" { + vars["TLSCurves"] = curves + } + script, err := loadTemplateFile(path, vars) if err != nil { log.Error(err, "unable to load redis haproxy configuration") diff --git a/argocd-operator/controllers/argoutil/redis_test.go b/argocd-operator/controllers/argoutil/redis_test.go index 085ff30a7e1..d77ad0d9b3d 100644 --- a/argocd-operator/controllers/argoutil/redis_test.go +++ b/argocd-operator/controllers/argoutil/redis_test.go @@ -15,8 +15,8 @@ import ( "github.com/argoproj-labs/gitops-operator/argocd-operator/pkg/tlsprofile" ) -// TestGetRedisHAProxyConfigRenderedTLSValues verifies that TLS minVersion and ciphers -// are correctly rendered in the final HAProxy configuration template output. +// TestGetRedisHAProxyConfigRenderedTLSValues verifies that TLS minVersion, ciphers, +// and curve preferences are correctly rendered in the final HAProxy configuration. func TestGetRedisHAProxyConfigRenderedTLSValues(t *testing.T) { tests := []struct { name string @@ -44,6 +44,10 @@ func TestGetRedisHAProxyConfigRenderedTLSValues(t *testing.T) { "ssl-default-bind-ciphersuites ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256", "ssl-default-server-ciphersuites ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256", }, + notExpectedInOutput: []string{ + "ssl-default-bind-curves", + "ssl-default-server-curves", + }, validatePattern: regexp.MustCompile(`ssl-min-ver\s+TLSv1\.2`), }, { @@ -84,6 +88,62 @@ func TestGetRedisHAProxyConfigRenderedTLSValues(t *testing.T) { "ssl-default-server-ciphers ", "ssl-default-bind-ciphersuites", "ssl-default-server-ciphersuites", + "ssl-default-bind-curves", + "ssl-default-server-curves", + }, + }, + { + name: "TLS with curve preferences maps OpenShift groups to HAProxy names", + useTLS: true, + centralTLSConfigProfile: tlsprofile.TLSConfigProfile{ + MinVersion: configv1.VersionTLS12, + CurvePreferences: []string{ + "X25519MLKEM768", + "X25519", + "secp256r1", + "secp384r1", + }, + }, + expectedInOutput: []string{ + "ssl-default-bind-options ssl-min-ver TLSv1.2", + "ssl-default-server-options ssl-min-ver TLSv1.2", + "ssl-default-bind-curves X25519MLKEM768:X25519:P-256:P-384", + "ssl-default-server-curves X25519MLKEM768:X25519:P-256:P-384", + }, + notExpectedInOutput: []string{ + "ssl-default-bind-ciphers ", + "ssl-default-server-ciphers ", + "ssl-default-bind-ciphersuites", + "ssl-default-server-ciphersuites", + "secp256r1", + "secp384r1", + }, + }, + { + name: "TLS 1.3 with ciphers and curve preferences", + useTLS: true, + centralTLSConfigProfile: tlsprofile.TLSConfigProfile{ + MinVersion: configv1.VersionTLS13, + Ciphers: []string{ + "TLS_AES_128_GCM_SHA256", + "TLS_AES_256_GCM_SHA384", + }, + CurvePreferences: []string{ + "X25519", + "secp256r1", + }, + }, + expectedInOutput: []string{ + "ssl-default-bind-options ssl-min-ver TLSv1.3", + "ssl-default-server-options ssl-min-ver TLSv1.3", + "ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384", + "ssl-default-server-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384", + "ssl-default-bind-curves X25519:P-256", + "ssl-default-server-curves X25519:P-256", + }, + notExpectedInOutput: []string{ + "ssl-default-bind-ciphers ", + "ssl-default-server-ciphers ", }, }, { @@ -98,6 +158,8 @@ func TestGetRedisHAProxyConfigRenderedTLSValues(t *testing.T) { "ssl-default-server-ciphers", "ssl-default-bind-ciphersuites", "ssl-default-server-ciphersuites", + "ssl-default-bind-curves", + "ssl-default-server-curves", }, }, } @@ -145,7 +207,48 @@ func TestGetRedisHAProxyConfigRenderedTLSValues(t *testing.T) { } else { assert.Empty(t, capturedVars["TLSCiphers"]) } + expectedCurves := MapCurvePreferencesToHAProxyCurves(tt.centralTLSConfigProfile.CurvePreferences) + if expectedCurves != "" { + assert.Equal(t, expectedCurves, capturedVars["TLSCurves"]) + } else { + assert.Empty(t, capturedVars["TLSCurves"]) + } } }) } } + +func TestMapCurvePreferencesToHAProxyCurves(t *testing.T) { + tests := []struct { + name string + input []string + expected string + }{ + { + name: "empty", + input: nil, + expected: "", + }, + { + name: "classical OpenShift groups", + input: []string{"X25519", "secp256r1", "secp384r1", "secp521r1"}, + expected: "X25519:P-256:P-384:P-521", + }, + { + name: "maps OpenShift groups including PQC", + input: []string{"X25519MLKEM768", "X25519", "SecP256r1MLKEM768", "secp256r1"}, + expected: "X25519MLKEM768:X25519:SecP256r1MLKEM768:P-256", + }, + { + name: "skips unknown groups", + input: []string{"UnknownGroup", "X25519"}, + expected: "X25519", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.expected, MapCurvePreferencesToHAProxyCurves(tt.input)) + }) + } +} diff --git a/build/redis/haproxy.cfg.tpl b/build/redis/haproxy.cfg.tpl index 0c4e975c71b..4d78d7401d4 100644 --- a/build/redis/haproxy.cfg.tpl +++ b/build/redis/haproxy.cfg.tpl @@ -22,6 +22,11 @@ global ssl-default-server-ciphersuites {{.TLSCiphers}} {{- end}} {{- end}} + +{{- if .TLSCurves}} + ssl-default-bind-curves {{.TLSCurves}} + ssl-default-server-curves {{.TLSCurves}} +{{- end}} {{- end}} defaults REDIS From a838539f74836bb7ce425ad6185bb0a3d4861b06 Mon Sep 17 00:00:00 2001 From: akhil nittala Date: Tue, 13 Oct 2026 23:31:10 +0530 Subject: [PATCH 2/5] feat(tls): Configure TLS Curve Preferences for Redis HA Proxy Signed-off-by: akhil nittala --- argocd-operator/common/defaults.go | 6 ++-- argocd-operator/controllers/argoutil/redis.go | 34 ++++++++----------- .../controllers/argoutil/redis_test.go | 18 +++++----- 3 files changed, 25 insertions(+), 33 deletions(-) diff --git a/argocd-operator/common/defaults.go b/argocd-operator/common/defaults.go index 21f80bf127f..39f51697bd0 100644 --- a/argocd-operator/common/defaults.go +++ b/argocd-operator/common/defaults.go @@ -197,11 +197,11 @@ const ( ArgoCDDefaultRedisHAReplicas = int32(3) // ArgoCDDefaultRedisHAProxyImage is the default Redis HAProxy image to use when not specified. - // haproxytech builds use AWS-LC, which supports post-quantum TLS groups (e.g. X25519MLKEM768). - ArgoCDDefaultRedisHAProxyImage = "docker.io/haproxytech/haproxy-alpine" + // Alpine 3.22+ ships OpenSSL 3.5+, which supports post-quantum TLS groups (e.g. X25519MLKEM768). + ArgoCDDefaultRedisHAProxyImage = "public.ecr.aws/docker/library/haproxy" // ArgoCDDefaultRedisHAProxyVersion is the default Redis HAProxy image tag to use when not specified. - ArgoCDDefaultRedisHAProxyVersion = "sha256:4b2da4adb652487a1aa1bb7fdacf5df1088d2c7d21302fcce738812d0d2fc453" // 3.3.6 + ArgoCDDefaultRedisHAProxyVersion = "sha256:96088e311f0f8c13d0fa197255d47fedb4c35090049319187b0038b848d86f82" // 3.3-alpine (3.3.16, OpenSSL 3.5) // ArgoCDDefaultRedisImage is the Redis container image to use when not specified. ArgoCDDefaultRedisImage = "public.ecr.aws/docker/library/redis" diff --git a/argocd-operator/controllers/argoutil/redis.go b/argocd-operator/controllers/argoutil/redis.go index 724206d7b2a..4970d83372d 100644 --- a/argocd-operator/controllers/argoutil/redis.go +++ b/argocd-operator/controllers/argoutil/redis.go @@ -186,35 +186,29 @@ func GetRedisInitScript(cr *argoproj.ArgoCD, useTLSForRedis bool) string { return script } -// haproxySupportedCurves maps OpenShift TLS group names to HAProxy/AWS-LC curve names. -// Classical groups use NIST aliases (secp256r1 -> P-256). Post-quantum hybrid groups -// are passed through for HAProxy 3.3+ with AWS-LC. -var haproxySupportedCurves = map[string]string{ - // Classical curves (OpenShift IANA name -> HAProxy NIST name) - "X25519": "X25519", - "secp256r1": "P-256", - "secp384r1": "P-384", - "secp521r1": "P-521", - "P-256": "P-256", - "P-384": "P-384", - "P-521": "P-521", - // Post-quantum hybrid groups (same name in OpenShift and HAProxy/AWS-LC) - "X25519MLKEM768": "X25519MLKEM768", - "SecP256r1MLKEM768": "SecP256r1MLKEM768", - "SecP384r1MLKEM1024": "SecP384r1MLKEM1024", +// haproxySupportedCurves lists OpenShift TLS group names accepted by HAProxy +// when linked against OpenSSL 3.5+ (public.ecr.aws/docker/library/haproxy alpine). +// OpenShift IANA names match OpenSSL group names, including post-quantum hybrids. +var haproxySupportedCurves = map[string]struct{}{ + "X25519": {}, + "secp256r1": {}, + "secp384r1": {}, + "secp521r1": {}, + "X25519MLKEM768": {}, + "SecP256r1MLKEM768": {}, + "SecP384r1MLKEM1024": {}, } // MapCurvePreferencesToHAProxyCurves converts OpenShift TLS group names to a -// colon-separated HAProxy curves list. Classical OpenShift names are converted -// to NIST aliases (e.g. secp256r1 -> P-256). Unknown groups are skipped. +// colon-separated HAProxy curves list for OpenSSL 3.5+. Unknown groups are skipped. func MapCurvePreferencesToHAProxyCurves(groups []string) string { if len(groups) == 0 { return "" } curves := make([]string, 0, len(groups)) for _, group := range groups { - if curve, ok := haproxySupportedCurves[group]; ok { - curves = append(curves, curve) + if _, ok := haproxySupportedCurves[group]; ok { + curves = append(curves, group) } } return strings.Join(curves, ":") diff --git a/argocd-operator/controllers/argoutil/redis_test.go b/argocd-operator/controllers/argoutil/redis_test.go index d77ad0d9b3d..1b554754a59 100644 --- a/argocd-operator/controllers/argoutil/redis_test.go +++ b/argocd-operator/controllers/argoutil/redis_test.go @@ -93,7 +93,7 @@ func TestGetRedisHAProxyConfigRenderedTLSValues(t *testing.T) { }, }, { - name: "TLS with curve preferences maps OpenShift groups to HAProxy names", + name: "TLS with curve preferences including PQC OpenSSL groups", useTLS: true, centralTLSConfigProfile: tlsprofile.TLSConfigProfile{ MinVersion: configv1.VersionTLS12, @@ -107,16 +107,14 @@ func TestGetRedisHAProxyConfigRenderedTLSValues(t *testing.T) { expectedInOutput: []string{ "ssl-default-bind-options ssl-min-ver TLSv1.2", "ssl-default-server-options ssl-min-ver TLSv1.2", - "ssl-default-bind-curves X25519MLKEM768:X25519:P-256:P-384", - "ssl-default-server-curves X25519MLKEM768:X25519:P-256:P-384", + "ssl-default-bind-curves X25519MLKEM768:X25519:secp256r1:secp384r1", + "ssl-default-server-curves X25519MLKEM768:X25519:secp256r1:secp384r1", }, notExpectedInOutput: []string{ "ssl-default-bind-ciphers ", "ssl-default-server-ciphers ", "ssl-default-bind-ciphersuites", "ssl-default-server-ciphersuites", - "secp256r1", - "secp384r1", }, }, { @@ -138,8 +136,8 @@ func TestGetRedisHAProxyConfigRenderedTLSValues(t *testing.T) { "ssl-default-server-options ssl-min-ver TLSv1.3", "ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384", "ssl-default-server-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384", - "ssl-default-bind-curves X25519:P-256", - "ssl-default-server-curves X25519:P-256", + "ssl-default-bind-curves X25519:secp256r1", + "ssl-default-server-curves X25519:secp256r1", }, notExpectedInOutput: []string{ "ssl-default-bind-ciphers ", @@ -232,12 +230,12 @@ func TestMapCurvePreferencesToHAProxyCurves(t *testing.T) { { name: "classical OpenShift groups", input: []string{"X25519", "secp256r1", "secp384r1", "secp521r1"}, - expected: "X25519:P-256:P-384:P-521", + expected: "X25519:secp256r1:secp384r1:secp521r1", }, { - name: "maps OpenShift groups including PQC", + name: "OpenShift groups including PQC", input: []string{"X25519MLKEM768", "X25519", "SecP256r1MLKEM768", "secp256r1"}, - expected: "X25519MLKEM768:X25519:SecP256r1MLKEM768:P-256", + expected: "X25519MLKEM768:X25519:SecP256r1MLKEM768:secp256r1", }, { name: "skips unknown groups", From 248fd05373d61dfee925ba5ba3d577026ebb5be7 Mon Sep 17 00:00:00 2001 From: akhil nittala Date: Wed, 14 Oct 2026 13:29:04 +0530 Subject: [PATCH 3/5] feat(tls): Configure TLS Curve Preferences for Redis HA Proxy Signed-off-by: akhil nittala --- argocd-operator/controllers/argoutil/redis.go | 33 ++++++++------ .../controllers/argoutil/redis_test.go | 43 ++++++++++++------- 2 files changed, 48 insertions(+), 28 deletions(-) diff --git a/argocd-operator/controllers/argoutil/redis.go b/argocd-operator/controllers/argoutil/redis.go index 4970d83372d..898cf54faa7 100644 --- a/argocd-operator/controllers/argoutil/redis.go +++ b/argocd-operator/controllers/argoutil/redis.go @@ -8,6 +8,7 @@ import ( "strings" "text/template" + configv1 "github.com/openshift/api/config/v1" corev1 "k8s.io/api/core/v1" argoproj "github.com/argoproj-labs/gitops-operator/argocd-operator/api/v1beta1" @@ -186,30 +187,36 @@ func GetRedisInitScript(cr *argoproj.ArgoCD, useTLSForRedis bool) string { return script } -// haproxySupportedCurves lists OpenShift TLS group names accepted by HAProxy -// when linked against OpenSSL 3.5+ (public.ecr.aws/docker/library/haproxy alpine). +// haproxySupportedCurves lists OpenShift TLS groups accepted by HAProxy when +// linked against OpenSSL 3.5+ (public.ecr.aws/docker/library/haproxy alpine). // OpenShift IANA names match OpenSSL group names, including post-quantum hybrids. -var haproxySupportedCurves = map[string]struct{}{ - "X25519": {}, - "secp256r1": {}, - "secp384r1": {}, - "secp521r1": {}, - "X25519MLKEM768": {}, - "SecP256r1MLKEM768": {}, - "SecP384r1MLKEM1024": {}, +var haproxySupportedCurves = map[configv1.TLSGroup]struct{}{ + configv1.TLSGroupX25519: {}, + configv1.TLSGroupSecP256r1: {}, + configv1.TLSGroupSecP384r1: {}, + configv1.TLSGroupSecP521r1: {}, + configv1.TLSGroupX25519MLKEM768: {}, + configv1.TLSGroupSecP256r1MLKEM768: {}, + configv1.TLSGroupSecP384r1MLKEM1024: {}, } -// MapCurvePreferencesToHAProxyCurves converts OpenShift TLS group names to a +// MapCurvePreferencesToHAProxyCurves converts OpenShift TLS groups to a // colon-separated HAProxy curves list for OpenSSL 3.5+. Unknown groups are skipped. -func MapCurvePreferencesToHAProxyCurves(groups []string) string { +func MapCurvePreferencesToHAProxyCurves(groups []configv1.TLSGroup) string { if len(groups) == 0 { return "" } curves := make([]string, 0, len(groups)) + skipped := make([]configv1.TLSGroup, 0) for _, group := range groups { if _, ok := haproxySupportedCurves[group]; ok { - curves = append(curves, group) + curves = append(curves, string(group)) + continue } + skipped = append(skipped, group) + } + if len(skipped) > 0 { + log.Info("skipping unsupported TLS curve preference groups for Redis HAProxy", "groups", skipped) } return strings.Join(curves, ":") } diff --git a/argocd-operator/controllers/argoutil/redis_test.go b/argocd-operator/controllers/argoutil/redis_test.go index 1b554754a59..fd3d9881951 100644 --- a/argocd-operator/controllers/argoutil/redis_test.go +++ b/argocd-operator/controllers/argoutil/redis_test.go @@ -97,11 +97,11 @@ func TestGetRedisHAProxyConfigRenderedTLSValues(t *testing.T) { useTLS: true, centralTLSConfigProfile: tlsprofile.TLSConfigProfile{ MinVersion: configv1.VersionTLS12, - CurvePreferences: []string{ - "X25519MLKEM768", - "X25519", - "secp256r1", - "secp384r1", + CurvePreferences: []configv1.TLSGroup{ + configv1.TLSGroupX25519MLKEM768, + configv1.TLSGroupX25519, + configv1.TLSGroupSecP256r1, + configv1.TLSGroupSecP384r1, }, }, expectedInOutput: []string{ @@ -126,9 +126,9 @@ func TestGetRedisHAProxyConfigRenderedTLSValues(t *testing.T) { "TLS_AES_128_GCM_SHA256", "TLS_AES_256_GCM_SHA384", }, - CurvePreferences: []string{ - "X25519", - "secp256r1", + CurvePreferences: []configv1.TLSGroup{ + configv1.TLSGroupX25519, + configv1.TLSGroupSecP256r1, }, }, expectedInOutput: []string{ @@ -219,7 +219,7 @@ func TestGetRedisHAProxyConfigRenderedTLSValues(t *testing.T) { func TestMapCurvePreferencesToHAProxyCurves(t *testing.T) { tests := []struct { name string - input []string + input []configv1.TLSGroup expected string }{ { @@ -228,18 +228,31 @@ func TestMapCurvePreferencesToHAProxyCurves(t *testing.T) { expected: "", }, { - name: "classical OpenShift groups", - input: []string{"X25519", "secp256r1", "secp384r1", "secp521r1"}, + name: "classical OpenShift groups", + input: []configv1.TLSGroup{ + configv1.TLSGroupX25519, + configv1.TLSGroupSecP256r1, + configv1.TLSGroupSecP384r1, + configv1.TLSGroupSecP521r1, + }, expected: "X25519:secp256r1:secp384r1:secp521r1", }, { - name: "OpenShift groups including PQC", - input: []string{"X25519MLKEM768", "X25519", "SecP256r1MLKEM768", "secp256r1"}, + name: "OpenShift groups including PQC", + input: []configv1.TLSGroup{ + configv1.TLSGroupX25519MLKEM768, + configv1.TLSGroupX25519, + configv1.TLSGroupSecP256r1MLKEM768, + configv1.TLSGroupSecP256r1, + }, expected: "X25519MLKEM768:X25519:SecP256r1MLKEM768:secp256r1", }, { - name: "skips unknown groups", - input: []string{"UnknownGroup", "X25519"}, + name: "skips unknown groups", + input: []configv1.TLSGroup{ + configv1.TLSGroup("UnknownGroup"), + configv1.TLSGroupX25519, + }, expected: "X25519", }, } From cc9cec1d5ec49e352b4d0c06219288ead9ccfbc3 Mon Sep 17 00:00:00 2001 From: akhil nittala Date: Thu, 15 Oct 2026 08:39:24 +0530 Subject: [PATCH 4/5] feat(tls): configure TLS Curve preferences for PQC Signed-off-by: akhil nittala --- argocd-operator/controllers/argoutil/redis.go | 37 +------------- .../controllers/argoutil/redis_test.go | 49 +----------------- argocd-operator/controllers/argoutil/tls.go | 35 +++++++++++++ .../controllers/argoutil/tls_test.go | 51 +++++++++++++++++++ controllers/consoleplugin.go | 37 +------------- 5 files changed, 89 insertions(+), 120 deletions(-) diff --git a/argocd-operator/controllers/argoutil/redis.go b/argocd-operator/controllers/argoutil/redis.go index 898cf54faa7..bae54941736 100644 --- a/argocd-operator/controllers/argoutil/redis.go +++ b/argocd-operator/controllers/argoutil/redis.go @@ -8,7 +8,6 @@ import ( "strings" "text/template" - configv1 "github.com/openshift/api/config/v1" corev1 "k8s.io/api/core/v1" argoproj "github.com/argoproj-labs/gitops-operator/argocd-operator/api/v1beta1" @@ -187,40 +186,6 @@ func GetRedisInitScript(cr *argoproj.ArgoCD, useTLSForRedis bool) string { return script } -// haproxySupportedCurves lists OpenShift TLS groups accepted by HAProxy when -// linked against OpenSSL 3.5+ (public.ecr.aws/docker/library/haproxy alpine). -// OpenShift IANA names match OpenSSL group names, including post-quantum hybrids. -var haproxySupportedCurves = map[configv1.TLSGroup]struct{}{ - configv1.TLSGroupX25519: {}, - configv1.TLSGroupSecP256r1: {}, - configv1.TLSGroupSecP384r1: {}, - configv1.TLSGroupSecP521r1: {}, - configv1.TLSGroupX25519MLKEM768: {}, - configv1.TLSGroupSecP256r1MLKEM768: {}, - configv1.TLSGroupSecP384r1MLKEM1024: {}, -} - -// MapCurvePreferencesToHAProxyCurves converts OpenShift TLS groups to a -// colon-separated HAProxy curves list for OpenSSL 3.5+. Unknown groups are skipped. -func MapCurvePreferencesToHAProxyCurves(groups []configv1.TLSGroup) string { - if len(groups) == 0 { - return "" - } - curves := make([]string, 0, len(groups)) - skipped := make([]configv1.TLSGroup, 0) - for _, group := range groups { - if _, ok := haproxySupportedCurves[group]; ok { - curves = append(curves, string(group)) - continue - } - skipped = append(skipped, group) - } - if len(skipped) > 0 { - log.Info("skipping unsupported TLS curve preference groups for Redis HAProxy", "groups", skipped) - } - return strings.Join(curves, ":") -} - // GetRedisHAProxyConfig will load the Redis HA Proxy configuration from a template on disk for the given ArgoCD. // If an error occurs, an empty string value will be returned. func GetRedisHAProxyConfig(cr *argoproj.ArgoCD, useTLSForRedis bool, centralTLSConfigProfile tlsprofile.TLSConfigProfile) string { @@ -238,7 +203,7 @@ func GetRedisHAProxyConfig(cr *argoproj.ArgoCD, useTLSForRedis bool, centralTLSC vars["TLSCiphers"] = strings.Join(centralTLSConfigProfile.Ciphers, ":") } - if curves := MapCurvePreferencesToHAProxyCurves(centralTLSConfigProfile.CurvePreferences); curves != "" { + if curves := MapTLSGroups(centralTLSConfigProfile.CurvePreferences, "Redis HAProxy"); curves != "" { vars["TLSCurves"] = curves } diff --git a/argocd-operator/controllers/argoutil/redis_test.go b/argocd-operator/controllers/argoutil/redis_test.go index fd3d9881951..fd0a8e6fdee 100644 --- a/argocd-operator/controllers/argoutil/redis_test.go +++ b/argocd-operator/controllers/argoutil/redis_test.go @@ -205,7 +205,7 @@ func TestGetRedisHAProxyConfigRenderedTLSValues(t *testing.T) { } else { assert.Empty(t, capturedVars["TLSCiphers"]) } - expectedCurves := MapCurvePreferencesToHAProxyCurves(tt.centralTLSConfigProfile.CurvePreferences) + expectedCurves := MapTLSGroups(tt.centralTLSConfigProfile.CurvePreferences, "Redis HAProxy") if expectedCurves != "" { assert.Equal(t, expectedCurves, capturedVars["TLSCurves"]) } else { @@ -216,50 +216,3 @@ func TestGetRedisHAProxyConfigRenderedTLSValues(t *testing.T) { } } -func TestMapCurvePreferencesToHAProxyCurves(t *testing.T) { - tests := []struct { - name string - input []configv1.TLSGroup - expected string - }{ - { - name: "empty", - input: nil, - expected: "", - }, - { - name: "classical OpenShift groups", - input: []configv1.TLSGroup{ - configv1.TLSGroupX25519, - configv1.TLSGroupSecP256r1, - configv1.TLSGroupSecP384r1, - configv1.TLSGroupSecP521r1, - }, - expected: "X25519:secp256r1:secp384r1:secp521r1", - }, - { - name: "OpenShift groups including PQC", - input: []configv1.TLSGroup{ - configv1.TLSGroupX25519MLKEM768, - configv1.TLSGroupX25519, - configv1.TLSGroupSecP256r1MLKEM768, - configv1.TLSGroupSecP256r1, - }, - expected: "X25519MLKEM768:X25519:SecP256r1MLKEM768:secp256r1", - }, - { - name: "skips unknown groups", - input: []configv1.TLSGroup{ - configv1.TLSGroup("UnknownGroup"), - configv1.TLSGroupX25519, - }, - expected: "X25519", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - assert.Equal(t, tt.expected, MapCurvePreferencesToHAProxyCurves(tt.input)) - }) - } -} diff --git a/argocd-operator/controllers/argoutil/tls.go b/argocd-operator/controllers/argoutil/tls.go index caa6e9eb28b..d8a52ae14c9 100644 --- a/argocd-operator/controllers/argoutil/tls.go +++ b/argocd-operator/controllers/argoutil/tls.go @@ -25,6 +25,7 @@ import ( "fmt" "math" "math/big" + "strings" "sync" "time" @@ -196,6 +197,40 @@ func MapCipherSuites(names []string) []string { return out } +// supportedOpenSSLTLSGroups lists OpenShift TLS groups whose IANA names are +// accepted by OpenSSL 3.5+ (HAProxy alpine, httpd SSLOpenSSLConfCmd Groups, etc.). +var supportedOpenSSLTLSGroups = map[configv1.TLSGroup]struct{}{ + configv1.TLSGroupX25519: {}, + configv1.TLSGroupSecP256r1: {}, + configv1.TLSGroupSecP384r1: {}, + configv1.TLSGroupSecP521r1: {}, + configv1.TLSGroupX25519MLKEM768: {}, + configv1.TLSGroupSecP256r1MLKEM768: {}, + configv1.TLSGroupSecP384r1MLKEM1024: {}, +} + +// MapTLSGroups converts OpenShift TLS groups to a colon-separated OpenSSL Groups +// string. Unknown groups are skipped and logged. component is included in the +// log message to identify the caller (e.g. "Redis HAProxy", "gitops-plugin httpd"). +func MapTLSGroups(groups []configv1.TLSGroup, component string) string { + if len(groups) == 0 { + return "" + } + names := make([]string, 0, len(groups)) + skipped := make([]configv1.TLSGroup, 0) + for _, group := range groups { + if _, ok := supportedOpenSSLTLSGroups[group]; ok { + names = append(names, string(group)) + continue + } + skipped = append(skipped, group) + } + if len(skipped) > 0 { + log.Info("skipping unsupported TLS curve preference groups", "component", component, "groups", skipped) + } + return strings.Join(names, ":") +} + func AgentTLSProtocolVersionString(v configv1.TLSProtocolVersion) string { version := TLSProtocolVersionString(v) //Agent will not support 1.0 tls version diff --git a/argocd-operator/controllers/argoutil/tls_test.go b/argocd-operator/controllers/argoutil/tls_test.go index bfa88b3d44c..736efcc7d6b 100644 --- a/argocd-operator/controllers/argoutil/tls_test.go +++ b/argocd-operator/controllers/argoutil/tls_test.go @@ -115,3 +115,54 @@ func TestAgentTLSProtocolVersionString(t *testing.T) { } } } + +func TestMapTLSGroups(t *testing.T) { + tests := []struct { + name string + input []configv1.TLSGroup + expected string + }{ + { + name: "empty", + input: nil, + expected: "", + }, + { + name: "classical OpenShift groups", + input: []configv1.TLSGroup{ + configv1.TLSGroupX25519, + configv1.TLSGroupSecP256r1, + configv1.TLSGroupSecP384r1, + configv1.TLSGroupSecP521r1, + }, + expected: "X25519:secp256r1:secp384r1:secp521r1", + }, + { + name: "OpenShift groups including PQC", + input: []configv1.TLSGroup{ + configv1.TLSGroupX25519MLKEM768, + configv1.TLSGroupX25519, + configv1.TLSGroupSecP256r1MLKEM768, + configv1.TLSGroupSecP256r1, + }, + expected: "X25519MLKEM768:X25519:SecP256r1MLKEM768:secp256r1", + }, + { + name: "skips unknown groups", + input: []configv1.TLSGroup{ + configv1.TLSGroup("UnknownGroup"), + configv1.TLSGroupX25519, + }, + expected: "X25519", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := MapTLSGroups(tt.input, "test") + if got != tt.expected { + t.Fatalf("expected %s got %s", tt.expected, got) + } + }) + } +} diff --git a/controllers/consoleplugin.go b/controllers/consoleplugin.go index 16accd185ec..9e7fa568608 100644 --- a/controllers/consoleplugin.go +++ b/controllers/consoleplugin.go @@ -13,7 +13,6 @@ import ( argocommon "github.com/argoproj-labs/gitops-operator/argocd-operator/common" argocdutil "github.com/argoproj-labs/gitops-operator/argocd-operator/controllers/argoutil" - configv1 "github.com/openshift/api/config/v1" consolev1 "github.com/openshift/api/console/v1" pipelinesv1alpha1 "github.com/redhat-developer/gitops-operator/api/v1alpha1" "github.com/redhat-developer/gitops-operator/controllers/util" @@ -251,40 +250,6 @@ func securityContextForPlugin() *corev1.SecurityContext { } } -// allowedHttpdGroups lists TLS groups supported by OpenSSL via SSLOpenSSLConfCmd Groups. -// OpenShift TLSGroup string values are directly usable as OpenSSL group names (OpenSSL >= 3.5). -var allowedHttpdGroups = map[configv1.TLSGroup]struct{}{ - configv1.TLSGroupX25519: {}, - configv1.TLSGroupSecP256r1: {}, - configv1.TLSGroupSecP384r1: {}, - configv1.TLSGroupSecP521r1: {}, - configv1.TLSGroupX25519MLKEM768: {}, - configv1.TLSGroupSecP256r1MLKEM768: {}, - configv1.TLSGroupSecP384r1MLKEM1024: {}, -} - -// tlsGroupsForHttpd converts OpenShift TLS groups to a colon-separated OpenSSL -// Groups string for Apache's SSLOpenSSLConfCmd directive. Unknown groups are -// filtered out so forward-compatible profile values do not break httpd. -func tlsGroupsForHttpd(groups []configv1.TLSGroup) string { - if len(groups) == 0 { - return "" - } - names := make([]string, 0, len(groups)) - skipped := make([]configv1.TLSGroup, 0) - for _, g := range groups { - if _, ok := allowedHttpdGroups[g]; ok { - names = append(names, string(g)) - continue - } - skipped = append(skipped, g) - } - if len(skipped) > 0 { - logs.Info("skipping unsupported TLS curve preference groups for gitops-plugin httpd", "groups", skipped) - } - return strings.Join(names, ":") -} - // buildHttpdConfig generates httpd.conf with dynamic TLS settings func (r *ReconcileGitopsService) buildHttpdConfig() string { minVersionTLS := string(r.CentralTLSProfile.MinVersion) @@ -310,7 +275,7 @@ ServerRoot "/etc/httpd" if minVersionTLS != "VersionTLS13" && strings.Join(r.CentralTLSProfile.Ciphers, ":") != "" { httpdConfigBase += fmt.Sprintf("\n\tSSLCipherSuite %s", strings.Join(r.CentralTLSProfile.Ciphers, ":")) } - if groups := tlsGroupsForHttpd(r.CentralTLSProfile.CurvePreferences); groups != "" { + if groups := argocdutil.MapTLSGroups(r.CentralTLSProfile.CurvePreferences, "gitops-plugin httpd"); groups != "" { httpdConfigBase += fmt.Sprintf("\n\tSSLOpenSSLConfCmd Groups %s", groups) } // Close VirtualHost From d6ecd948ece91f6ed150269a9a08f985f1bb2017 Mon Sep 17 00:00:00 2001 From: akhil nittala Date: Thu, 15 Oct 2026 13:43:12 +0530 Subject: [PATCH 5/5] feat(tls): configure TLS Curve preferences for PQC Signed-off-by: akhil nittala --- argocd-operator/controllers/argoutil/redis.go | 2 +- argocd-operator/controllers/argoutil/redis_test.go | 2 +- argocd-operator/controllers/argoutil/tls.go | 9 +++++---- argocd-operator/controllers/argoutil/tls_test.go | 4 ++-- controllers/consoleplugin.go | 2 +- 5 files changed, 10 insertions(+), 9 deletions(-) diff --git a/argocd-operator/controllers/argoutil/redis.go b/argocd-operator/controllers/argoutil/redis.go index bae54941736..cc474acbc3c 100644 --- a/argocd-operator/controllers/argoutil/redis.go +++ b/argocd-operator/controllers/argoutil/redis.go @@ -203,7 +203,7 @@ func GetRedisHAProxyConfig(cr *argoproj.ArgoCD, useTLSForRedis bool, centralTLSC vars["TLSCiphers"] = strings.Join(centralTLSConfigProfile.Ciphers, ":") } - if curves := MapTLSGroups(centralTLSConfigProfile.CurvePreferences, "Redis HAProxy"); curves != "" { + if curves := MapOpenShiftTLSGroupsToOpenSSL(centralTLSConfigProfile.CurvePreferences, "Redis HAProxy"); curves != "" { vars["TLSCurves"] = curves } diff --git a/argocd-operator/controllers/argoutil/redis_test.go b/argocd-operator/controllers/argoutil/redis_test.go index fd0a8e6fdee..1b32b54c601 100644 --- a/argocd-operator/controllers/argoutil/redis_test.go +++ b/argocd-operator/controllers/argoutil/redis_test.go @@ -205,7 +205,7 @@ func TestGetRedisHAProxyConfigRenderedTLSValues(t *testing.T) { } else { assert.Empty(t, capturedVars["TLSCiphers"]) } - expectedCurves := MapTLSGroups(tt.centralTLSConfigProfile.CurvePreferences, "Redis HAProxy") + expectedCurves := MapOpenShiftTLSGroupsToOpenSSL(tt.centralTLSConfigProfile.CurvePreferences, "Redis HAProxy") if expectedCurves != "" { assert.Equal(t, expectedCurves, capturedVars["TLSCurves"]) } else { diff --git a/argocd-operator/controllers/argoutil/tls.go b/argocd-operator/controllers/argoutil/tls.go index d8a52ae14c9..373478d1a0a 100644 --- a/argocd-operator/controllers/argoutil/tls.go +++ b/argocd-operator/controllers/argoutil/tls.go @@ -209,10 +209,11 @@ var supportedOpenSSLTLSGroups = map[configv1.TLSGroup]struct{}{ configv1.TLSGroupSecP384r1MLKEM1024: {}, } -// MapTLSGroups converts OpenShift TLS groups to a colon-separated OpenSSL Groups -// string. Unknown groups are skipped and logged. component is included in the -// log message to identify the caller (e.g. "Redis HAProxy", "gitops-plugin httpd"). -func MapTLSGroups(groups []configv1.TLSGroup, component string) string { +// MapOpenShiftTLSGroupsToOpenSSL converts OpenShift TLS groups to a +// colon-separated OpenSSL Groups string. Unknown groups are skipped and logged. +// component is included in the log message to identify the caller +// (e.g. "Redis HAProxy", "gitops-plugin httpd"). +func MapOpenShiftTLSGroupsToOpenSSL(groups []configv1.TLSGroup, component string) string { if len(groups) == 0 { return "" } diff --git a/argocd-operator/controllers/argoutil/tls_test.go b/argocd-operator/controllers/argoutil/tls_test.go index 736efcc7d6b..d31fe3da52d 100644 --- a/argocd-operator/controllers/argoutil/tls_test.go +++ b/argocd-operator/controllers/argoutil/tls_test.go @@ -116,7 +116,7 @@ func TestAgentTLSProtocolVersionString(t *testing.T) { } } -func TestMapTLSGroups(t *testing.T) { +func TestMapOpenShiftTLSGroupsToOpenSSL(t *testing.T) { tests := []struct { name string input []configv1.TLSGroup @@ -159,7 +159,7 @@ func TestMapTLSGroups(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := MapTLSGroups(tt.input, "test") + got := MapOpenShiftTLSGroupsToOpenSSL(tt.input, "test") if got != tt.expected { t.Fatalf("expected %s got %s", tt.expected, got) } diff --git a/controllers/consoleplugin.go b/controllers/consoleplugin.go index 9e7fa568608..f24257ebe0c 100644 --- a/controllers/consoleplugin.go +++ b/controllers/consoleplugin.go @@ -275,7 +275,7 @@ ServerRoot "/etc/httpd" if minVersionTLS != "VersionTLS13" && strings.Join(r.CentralTLSProfile.Ciphers, ":") != "" { httpdConfigBase += fmt.Sprintf("\n\tSSLCipherSuite %s", strings.Join(r.CentralTLSProfile.Ciphers, ":")) } - if groups := argocdutil.MapTLSGroups(r.CentralTLSProfile.CurvePreferences, "gitops-plugin httpd"); groups != "" { + if groups := argocdutil.MapOpenShiftTLSGroupsToOpenSSL(r.CentralTLSProfile.CurvePreferences, "gitops-plugin httpd"); groups != "" { httpdConfigBase += fmt.Sprintf("\n\tSSLOpenSSLConfCmd Groups %s", groups) } // Close VirtualHost