diff --git a/deploy/deployment.yaml b/deploy/deployment.yaml index 9373553..84c9deb 100644 --- a/deploy/deployment.yaml +++ b/deploy/deployment.yaml @@ -22,6 +22,12 @@ spec: env: - name: INSECURE value: "true" + - name: TLSMINVERSION + value: "1.3" + - name: TLSMAXVERSION + value: "1.3" + - name: TLSCIPHERSUITES + value: "" volumeMounts: - mountPath: "/etc/gitops/ssl" name: backend-ssl diff --git a/pkg/cmd/root.go b/pkg/cmd/root.go index d488e7a..88e7394 100644 --- a/pkg/cmd/root.go +++ b/pkg/cmd/root.go @@ -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" @@ -22,14 +23,155 @@ import ( ) const ( - portFlag = "port" - insecureFlag = "insecure" - tlsCertFlag = "tls-cert" - tlsKeyFlag = "tls-key" - noTLSFlag = "no-tls" - enableHTTP2 = "enable-http2" + portFlag = "port" + insecureFlag = "insecure" + tlsCertFlag = "tls-cert" + tlsKeyFlag = "tls-key" + noTLSFlag = "no-tls" + enableHTTP2 = "enable-http2" + tlsMinVersionFlag = "tlsminversion" + tlsMaxVersionFlag = "tlsmaxversion" + tlsCipherSuitesFlag = "tlsciphersuites" ) +type TLSConfig struct { + MinVersion string + MaxVersion string + Ciphers string +} + +// tlsVersionMap maps version strings to tls version constants. +// TLS 1.0 is not supported as it is considered insecure. +var tlsVersionMap = map[string]uint16{ + "1.1": tls.VersionTLS11, + "tls1.1": tls.VersionTLS11, + "1.2": tls.VersionTLS12, + "tls1.2": tls.VersionTLS12, + "1.3": tls.VersionTLS13, + "tls1.3": tls.VersionTLS13, +} + +// 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 + } + } + 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. +// Returns 0 if the string is empty (meaning "use default"). +func ParseTLSVersion(version string) (uint16, error) { + if version == "" { + return 0, nil + } + v, ok := tlsVersionMap[strings.ToLower(strings.TrimSpace(version))] + if !ok { + return 0, fmt.Errorf("unsupported TLS version: %q (supported: 1.1, 1.2, 1.3)", version) + } + return v, nil +} + +// ParseTLSCiphers parses a colon-separated list of cipher suite names into cipher suite IDs. +// Only secure cipher suites (from tls.CipherSuites()) are allowed. +// Returns nil if the input is empty. +func ParseTLSCiphers(ciphers string) ([]uint16, error) { + if ciphers == "" { + return nil, nil + } + // Build lookup map from Go's secure cipher suites only + cipherMap := make(map[string]uint16) + for _, cs := range tls.CipherSuites() { + cipherMap[cs.Name] = cs.ID + } + var result []uint16 + for _, name := range strings.Split(ciphers, ":") { + name = strings.TrimSpace(name) + if name == "" { + continue + } + id, ok := cipherMap[name] + if !ok { + return nil, fmt.Errorf("unsupported TLS cipher suite: %q", name) + } + result = append(result, id) + } + return result, nil +} + +// ValidateTLSConfig validates the TLS configuration parameters. +// It checks that: +// - The minimum TLS version is not greater than the maximum TLS version +// - All configured cipher suites are compatible with the minimum TLS version +func ValidateTLSConfig(minVersion, maxVersion uint16, cipherSuites []uint16) error { + if minVersion != 0 && maxVersion != 0 && minVersion > maxVersion { + return fmt.Errorf("minimum TLS version (%s) cannot be higher than maximum TLS version (%s)", + TLSVersionName(minVersion), TLSVersionName(maxVersion)) + } + if len(cipherSuites) > 0 && minVersion != 0 { + availableCiphers := tls.CipherSuites() + for _, cipherID := range cipherSuites { + for _, cs := range availableCiphers { + if cs.ID == cipherID { + supported := false + for _, v := range cs.SupportedVersions { + if v == minVersion { + supported = true + break + } + } + if !supported { + return fmt.Errorf("cipher suite %s is not supported by minimum TLS version %s", + cs.Name, TLSVersionName(minVersion)) + } + break + } + } + } + } + + return nil +} + +func buildTLSConfig() (*tls.Config, error) { + cfg := TLSConfig{ + MinVersion: viper.GetString(tlsMinVersionFlag), + MaxVersion: viper.GetString(tlsMaxVersionFlag), + Ciphers: viper.GetString(tlsCipherSuitesFlag), + } + + tlsCfg := &tls.Config{} //nolint:gosec + minVer, err := ParseTLSVersion(cfg.MinVersion) + if err != nil { + return nil, fmt.Errorf("invalid --%s: %w", tlsMinVersionFlag, err) + } + tlsCfg.MinVersion = minVer + maxVer, err := ParseTLSVersion(cfg.MaxVersion) + if err != nil { + return nil, fmt.Errorf("invalid --%s: %w", tlsMaxVersionFlag, err) + } + tlsCfg.MaxVersion = maxVer + ciphers, err := ParseTLSCiphers(cfg.Ciphers) + if err != nil { + return nil, fmt.Errorf("invalid --%s: %w", tlsCipherSuitesFlag, err) + } + if len(ciphers) > 0 && minVer == tls.VersionTLS13 { + log.Printf( + "%s has no effect when minimum TLS version is 1.3 because TLS 1.3 cipher suites are not configurable; ignoring", + tlsCipherSuitesFlag, + ) + ciphers = nil + } + if err := ValidateTLSConfig(minVer, maxVer, ciphers); err != nil { + return nil, err + } + tlsCfg.CipherSuites = ciphers + + return tlsCfg, nil +} + func init() { cobra.OnInitialize(initConfig) if err := argoV1aplha1.AddToScheme(scheme.Scheme); err != nil { @@ -79,6 +221,11 @@ func makeHTTPCmd() *cobra.Command { log.Println("TLS connections disabled") return server.ListenAndServe() } + tlsConfig, err := buildTLSConfig() + if err != nil { + return err + } + server.TLSConfig = tlsConfig log.Printf("Using TLS from %q and %q", viper.GetString(tlsCertFlag), viper.GetString(tlsKeyFlag)) return server.ListenAndServeTLS(viper.GetString(tlsCertFlag), viper.GetString(tlsKeyFlag)) }, @@ -125,6 +272,26 @@ func makeHTTPCmd() *cobra.Command { "enable HTTP/2 for the server", ) logIfError(viper.BindPFlag(enableHTTP2, cmd.Flags().Lookup(enableHTTP2))) + cmd.Flags().String( + tlsMinVersionFlag, + "", + "minimum supported TLS version (1.2, 1.3)", + ) + logIfError(viper.BindPFlag(tlsMinVersionFlag, cmd.Flags().Lookup(tlsMinVersionFlag))) + + cmd.Flags().String( + tlsMaxVersionFlag, + "", + "maximum supported TLS version (1.2, 1.3)", + ) + logIfError(viper.BindPFlag(tlsMaxVersionFlag, cmd.Flags().Lookup(tlsMaxVersionFlag))) + + cmd.Flags().String( + tlsCipherSuitesFlag, + "", + "comma-separated list of TLS cipher suites", + ) + logIfError(viper.BindPFlag(tlsCipherSuitesFlag, cmd.Flags().Lookup(tlsCipherSuitesFlag))) return cmd }