Skip to content
Open
3 changes: 2 additions & 1 deletion argocd-operator/common/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
4 changes: 4 additions & 0 deletions argocd-operator/controllers/argoutil/redis.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
71 changes: 69 additions & 2 deletions argocd-operator/controllers/argoutil/redis_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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`),
},
{
Expand Down Expand Up @@ -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 ",
},
},
{
Expand All @@ -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",
},
},
}
Expand Down Expand Up @@ -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"])
}
}
})
}
}

36 changes: 36 additions & 0 deletions argocd-operator/controllers/argoutil/tls.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"fmt"
"math"
"math/big"
"strings"
"sync"
"time"

Expand Down Expand Up @@ -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
Expand Down
51 changes: 51 additions & 0 deletions argocd-operator/controllers/argoutil/tls_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
})
}
}
5 changes: 5 additions & 0 deletions build/redis/haproxy.cfg.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
37 changes: 1 addition & 36 deletions controllers/consoleplugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down
Loading