diff --git a/argocd-operator/common/defaults.go b/argocd-operator/common/defaults.go index 24f4ed13a88..39f51697bd0 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. + // 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:e11f034e651603f10a365e5ad5a0321825e18eded9620e40c4f4d6ae58419bfe" // 3.0.8-alpine + 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 e1fbdfade74..cc474acbc3c 100644 --- a/argocd-operator/controllers/argoutil/redis.go +++ b/argocd-operator/controllers/argoutil/redis.go @@ -203,6 +203,10 @@ func GetRedisHAProxyConfig(cr *argoproj.ArgoCD, useTLSForRedis bool, centralTLSC vars["TLSCiphers"] = strings.Join(centralTLSConfigProfile.Ciphers, ":") } + if curves := MapOpenShiftTLSGroupsToOpenSSL(centralTLSConfigProfile.CurvePreferences, "Redis HAProxy"); 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..1b32b54c601 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,60 @@ 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 including PQC OpenSSL groups", + useTLS: true, + centralTLSConfigProfile: tlsprofile.TLSConfigProfile{ + MinVersion: configv1.VersionTLS12, + CurvePreferences: []configv1.TLSGroup{ + configv1.TLSGroupX25519MLKEM768, + configv1.TLSGroupX25519, + configv1.TLSGroupSecP256r1, + configv1.TLSGroupSecP384r1, + }, + }, + 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: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", + }, + }, + { + 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: []configv1.TLSGroup{ + configv1.TLSGroupX25519, + configv1.TLSGroupSecP256r1, + }, + }, + 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:secp256r1", + "ssl-default-server-curves X25519:secp256r1", + }, + notExpectedInOutput: []string{ + "ssl-default-bind-ciphers ", + "ssl-default-server-ciphers ", }, }, { @@ -98,6 +156,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 +205,14 @@ func TestGetRedisHAProxyConfigRenderedTLSValues(t *testing.T) { } else { assert.Empty(t, capturedVars["TLSCiphers"]) } + expectedCurves := MapOpenShiftTLSGroupsToOpenSSL(tt.centralTLSConfigProfile.CurvePreferences, "Redis HAProxy") + if expectedCurves != "" { + assert.Equal(t, expectedCurves, capturedVars["TLSCurves"]) + } else { + assert.Empty(t, capturedVars["TLSCurves"]) + } } }) } } + diff --git a/argocd-operator/controllers/argoutil/tls.go b/argocd-operator/controllers/argoutil/tls.go index caa6e9eb28b..373478d1a0a 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,41 @@ 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: {}, +} + +// 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 "" + } + 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..d31fe3da52d 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 TestMapOpenShiftTLSGroupsToOpenSSL(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 := MapOpenShiftTLSGroupsToOpenSSL(tt.input, "test") + if got != tt.expected { + t.Fatalf("expected %s got %s", tt.expected, got) + } + }) + } +} 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 diff --git a/controllers/consoleplugin.go b/controllers/consoleplugin.go index 16accd185ec..f24257ebe0c 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.MapOpenShiftTLSGroupsToOpenSSL(r.CentralTLSProfile.CurvePreferences, "gitops-plugin httpd"); groups != "" { httpdConfigBase += fmt.Sprintf("\n\tSSLOpenSSLConfCmd Groups %s", groups) } // Close VirtualHost