Skip to content
Merged
2 changes: 2 additions & 0 deletions deploy/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ spec:
value: "1.3"
- name: TLS_CIPHER_SUITES
value: ""
- name: TLS_CURVE_PREFERENCES
value: "X25519MLKEM768:X25519:CurveP256"
Comment thread
akhilnittala marked this conversation as resolved.
Resolved
volumeMounts:
- mountPath: "/etc/gitops/ssl"
name: backend-ssl
Expand Down
9 changes: 9 additions & 0 deletions pkg/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"log"
"net/http"
"strings"

argoV1aplha1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
"github.com/prometheus/client_golang/prometheus/promhttp"
Expand Down Expand Up @@ -44,6 +45,7 @@ func logIfError(e error) {
}

func initConfig() {
viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
viper.AutomaticEnv()
}

Expand Down Expand Up @@ -150,6 +152,13 @@ func makeHTTPCmd() *cobra.Command {
"comma-separated list of TLS cipher suites",
)
logIfError(viper.BindPFlag(tlsCipherSuitesFlag, cmd.Flags().Lookup(tlsCipherSuitesFlag)))

cmd.Flags().String(
tlsCurvePreferencesFlag,
"",
"colon-separated list of TLS curve preferences (e.g. X25519MLKEM768:X25519:CurveP256)",
)
logIfError(viper.BindPFlag(tlsCurvePreferencesFlag, cmd.Flags().Lookup(tlsCurvePreferencesFlag)))
return cmd
}

Expand Down
90 changes: 76 additions & 14 deletions pkg/cmd/tls.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,25 @@ import (
"crypto/tls"
"fmt"
"log"
"maps"
"slices"
"strings"

"github.com/spf13/viper"
)

const (
tlsMinVersionFlag = "tls-min-version"
tlsMaxVersionFlag = "tls-max-version"
tlsCipherSuitesFlag = "tls-cipher-suites"
tlsMinVersionFlag = "tls-min-version"
tlsMaxVersionFlag = "tls-max-version"
tlsCipherSuitesFlag = "tls-cipher-suites"
tlsCurvePreferencesFlag = "tls-curve-preferences"
)

type TLSConfig struct {
MinVersion string
MaxVersion string
Ciphers string
MinVersion string
MaxVersion string
Ciphers string
CurvePreferences string
}

// tlsVersionMap maps version strings to tls version constants.
Expand All @@ -34,12 +38,16 @@ var tlsVersionMap = map[string]uint16{

// TLSVersionName returns a human-readable name for a TLS version constant.
func TLSVersionName(version uint16) string {
for name, v := range tlsVersionMap {
if v == version {
return name
}
switch version {
case tls.VersionTLS11:
return "1.1"
case tls.VersionTLS12:
return "1.2"
case tls.VersionTLS13:
return "1.3"
default:
return fmt.Sprintf("unknown (%d)", version)
}
return fmt.Sprintf("unknown (%d)", version)
}

// ParseTLSVersion parses a TLS version string (e.g. "1.2", "1.3", "TLS1.2") into a tls version constant.
Expand Down Expand Up @@ -82,6 +90,53 @@ func ParseTLSCiphers(ciphers string) ([]uint16, error) {
return result, nil
}

var allowedCurveNames = func() map[string]tls.CurveID {
curves := []tls.CurveID{
tls.X25519MLKEM768,
tls.X25519,
tls.CurveP256,
tls.CurveP384,
tls.CurveP521,
}
allowed := make(map[string]tls.CurveID, len(curves))
for _, curve := range curves {
allowed[curve.String()] = curve
}
return allowed
}()

func parseCurvePreferences(names []string) ([]tls.CurveID, error) {
if len(names) == 0 {
return nil, nil
}
curves := make([]tls.CurveID, 0, len(names))
for _, name := range names {
if id, ok := allowedCurveNames[name]; ok {
curves = append(curves, id)
} else {
return nil, fmt.Errorf("unknown curve: %q (supported: %v)", name, slices.Sorted(maps.Keys(allowedCurveNames)))
}
}
return curves, nil
}

// ParseTLSCurvePreferences parses a colon-separated list of TLS curve preference names.
// Returns nil if the input is empty.
func ParseTLSCurvePreferences(curves string) ([]tls.CurveID, error) {
if curves == "" {
return nil, nil
}
var names []string
for _, name := range strings.Split(curves, ":") {
name = strings.TrimSpace(name)
if name == "" {
continue
}
names = append(names, name)
}
return parseCurvePreferences(names)
}

// ValidateTLSConfig validates the TLS configuration parameters.
// It checks that:
// - The minimum TLS version is not greater than the maximum TLS version
Expand Down Expand Up @@ -118,9 +173,10 @@ func ValidateTLSConfig(minVersion, maxVersion uint16, cipherSuites []uint16) err

func buildTLSConfig() (*tls.Config, error) {
cfg := TLSConfig{
MinVersion: viper.GetString(tlsMinVersionFlag),
MaxVersion: viper.GetString(tlsMaxVersionFlag),
Ciphers: viper.GetString(tlsCipherSuitesFlag),
MinVersion: viper.GetString(tlsMinVersionFlag),
MaxVersion: viper.GetString(tlsMaxVersionFlag),
Ciphers: viper.GetString(tlsCipherSuitesFlag),
CurvePreferences: viper.GetString(tlsCurvePreferencesFlag),
}

tlsCfg := &tls.Config{} //nolint:gosec
Expand Down Expand Up @@ -150,5 +206,11 @@ func buildTLSConfig() (*tls.Config, error) {
}
tlsCfg.CipherSuites = ciphers

curvePrefs, err := ParseTLSCurvePreferences(cfg.CurvePreferences)
if err != nil {
return nil, fmt.Errorf("invalid --%s: %w", tlsCurvePreferencesFlag, err)
}
tlsCfg.CurvePreferences = curvePrefs

return tlsCfg, nil
}
90 changes: 82 additions & 8 deletions pkg/cmd/tls_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,18 +205,80 @@ func TestValidateTLSConfig(t *testing.T) {
}
}

func TestParseTLSCurvePreferences(t *testing.T) {
tests := []struct {
name string
input string
expected []tls.CurveID
expectErr bool
}{
{
name: "empty",
input: "",
expected: nil,
},
{
name: "single curve",
input: tls.X25519.String(),
expected: []tls.CurveID{tls.X25519},
},
{
name: "multiple curves",
input: tls.X25519MLKEM768.String() + ":" +
tls.X25519.String() + ":" +
tls.CurveP256.String(),
expected: []tls.CurveID{
tls.X25519MLKEM768,
tls.X25519,
tls.CurveP256,
},
},
{
name: "post-quantum curve",
input: tls.X25519MLKEM768.String(),
expected: []tls.CurveID{tls.X25519MLKEM768},
},
{
name: "invalid curve",
input: "INVALID_CURVE",
expectErr: true,
},
{
name: "ignore empty entries",
input: ":" + tls.X25519.String() + "::",
expected: []tls.CurveID{tls.X25519},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
curves, err := ParseTLSCurvePreferences(tt.input)

if tt.expectErr {
require.Error(t, err)
return
}

require.NoError(t, err)
assert.Equal(t, tt.expected, curves)
})
}
}

func TestBuildTLSConfig(t *testing.T) {
validCipher := tls.CipherSuites()[3]

tests := []struct {
name string
minVersion string
maxVersion string
ciphers string
expectedMin uint16
expectedMax uint16
expectedSuite []uint16
expectErr bool
name string
minVersion string
maxVersion string
ciphers string
curves string
expectedMin uint16
expectedMax uint16
expectedSuite []uint16
expectedCurves []tls.CurveID
expectErr bool
}{
{
name: "defaults",
Expand All @@ -234,6 +296,11 @@ func TestBuildTLSConfig(t *testing.T) {
validCipher.ID,
},
},
{
name: "valid curves",
curves: tls.X25519.String() + ":" + tls.CurveP256.String(),
expectedCurves: []tls.CurveID{tls.X25519, tls.CurveP256},
},
{
name: "invalid min version",
minVersion: "1.0",
Expand All @@ -249,6 +316,11 @@ func TestBuildTLSConfig(t *testing.T) {
ciphers: "BAD_CIPHER",
expectErr: true,
},
{
name: "invalid curve",
curves: "BAD_CURVE",
expectErr: true,
},
{
name: "min greater than max",
minVersion: "1.3",
Expand All @@ -271,6 +343,7 @@ func TestBuildTLSConfig(t *testing.T) {
viper.Set(tlsMinVersionFlag, tt.minVersion)
viper.Set(tlsMaxVersionFlag, tt.maxVersion)
viper.Set(tlsCipherSuitesFlag, tt.ciphers)
viper.Set(tlsCurvePreferencesFlag, tt.curves)

cfg, err := buildTLSConfig()

Expand All @@ -283,6 +356,7 @@ func TestBuildTLSConfig(t *testing.T) {
assert.Equal(t, tt.expectedMin, cfg.MinVersion)
assert.Equal(t, tt.expectedMax, cfg.MaxVersion)
assert.Equal(t, tt.expectedSuite, cfg.CipherSuites)
assert.Equal(t, tt.expectedCurves, cfg.CurvePreferences)
})
}
}
Loading