diff --git a/tls/config.go b/tls/config.go new file mode 100644 index 0000000000..dcb0bfadde --- /dev/null +++ b/tls/config.go @@ -0,0 +1,176 @@ +/* +Copyright 2026 The Knative Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package tls + +import ( + cryptotls "crypto/tls" + "fmt" + "os" + "strings" +) + +// Environment variable name suffixes for TLS configuration. +// Use with a prefix to namespace them, e.g. "WEBHOOK_" + MinVersionEnvKey +// reads the WEBHOOK_TLS_MIN_VERSION variable. +const ( + MinVersionEnvKey = "TLS_MIN_VERSION" + MaxVersionEnvKey = "TLS_MAX_VERSION" + CipherSuitesEnvKey = "TLS_CIPHER_SUITES" + CurvePreferencesEnvKey = "TLS_CURVE_PREFERENCES" +) + +// Config holds parsed TLS configuration values that can be used +// to build a *crypto/tls.Config. +type Config struct { + MinVersion uint16 + MaxVersion uint16 + CipherSuites []uint16 + CurvePreferences []cryptotls.CurveID +} + +// NewConfigFromEnv reads TLS configuration from environment variables and +// returns a Config. The prefix is prepended to each standard env-var suffix; +// for example with prefix "WEBHOOK_" the function reads +// WEBHOOK_TLS_MIN_VERSION, WEBHOOK_TLS_MAX_VERSION, etc. +// Fields whose corresponding env var is unset are left at their zero value. +func NewConfigFromEnv(prefix string) (*Config, error) { + var cfg Config + + if v := os.Getenv(prefix + MinVersionEnvKey); v != "" { + ver, err := ParseVersion(v) + if err != nil { + return nil, fmt.Errorf("invalid %s%s %q: %w", prefix, MinVersionEnvKey, v, err) + } + cfg.MinVersion = ver + } + + if v := os.Getenv(prefix + MaxVersionEnvKey); v != "" { + ver, err := ParseVersion(v) + if err != nil { + return nil, fmt.Errorf("invalid %s%s %q: %w", prefix, MaxVersionEnvKey, v, err) + } + cfg.MaxVersion = ver + } + + if v := os.Getenv(prefix + CipherSuitesEnvKey); v != "" { + suites, err := ParseCipherSuites(v) + if err != nil { + return nil, fmt.Errorf("invalid %s%s: %w", prefix, CipherSuitesEnvKey, err) + } + cfg.CipherSuites = suites + } + + if v := os.Getenv(prefix + CurvePreferencesEnvKey); v != "" { + curves, err := ParseCurvePreferences(v) + if err != nil { + return nil, fmt.Errorf("invalid %s%s: %w", prefix, CurvePreferencesEnvKey, err) + } + cfg.CurvePreferences = curves + } + + return &cfg, nil +} + +// TLSConfig constructs a *crypto/tls.Config from the parsed configuration. +// The caller typically adds additional fields such as GetCertificate. +func (c *Config) TLSConfig() *cryptotls.Config { + //nolint:gosec // Min version is caller-configurable; default is TLS 1.3. + return &cryptotls.Config{ + MinVersion: c.MinVersion, + MaxVersion: c.MaxVersion, + CipherSuites: c.CipherSuites, + CurvePreferences: c.CurvePreferences, + } +} + +// ParseVersion converts a TLS version string to the corresponding +// crypto/tls constant. Accepted values are "1.2" and "1.3". +func ParseVersion(v string) (uint16, error) { + switch v { + case "1.2": + return cryptotls.VersionTLS12, nil + case "1.3": + return cryptotls.VersionTLS13, nil + default: + return 0, fmt.Errorf("unsupported TLS version %q: must be %q or %q", v, "1.2", "1.3") + } +} + +// ParseCipherSuites parses a comma-separated list of TLS cipher-suite names +// (e.g. "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384") +// into a slice of cipher-suite IDs. Names must match those returned by +// crypto/tls.CipherSuiteName. +func ParseCipherSuites(s string) ([]uint16, error) { + lookup := cipherSuiteLookup() + parts := strings.Split(s, ",") + suites := make([]uint16, 0, len(parts)) + + for _, name := range parts { + name = strings.TrimSpace(name) + if name == "" { + continue + } + id, ok := lookup[name] + if !ok { + return nil, fmt.Errorf("unknown cipher suite %q", name) + } + suites = append(suites, id) + } + + return suites, nil +} + +// ParseCurvePreferences parses a comma-separated list of elliptic-curve names +// (e.g. "X25519,CurveP256") into a slice of crypto/tls.CurveID values. +// Both Go constant names (CurveP256) and standard names (P-256) are accepted. +func ParseCurvePreferences(s string) ([]cryptotls.CurveID, error) { + parts := strings.Split(s, ",") + curves := make([]cryptotls.CurveID, 0, len(parts)) + + for _, name := range parts { + name = strings.TrimSpace(name) + if name == "" { + continue + } + id, ok := curvesByName[name] + if !ok { + return nil, fmt.Errorf("unknown curve %q", name) + } + curves = append(curves, id) + } + + return curves, nil +} + +func cipherSuiteLookup() map[string]uint16 { + m := make(map[string]uint16) + for _, cs := range cryptotls.CipherSuites() { + m[cs.Name] = cs.ID + } + return m +} + +var curvesByName = map[string]cryptotls.CurveID{ + "CurveP256": cryptotls.CurveP256, + "CurveP384": cryptotls.CurveP384, + "CurveP521": cryptotls.CurveP521, + "X25519": cryptotls.X25519, + "X25519MLKEM768": cryptotls.X25519MLKEM768, + "P-256": cryptotls.CurveP256, + "P-384": cryptotls.CurveP384, + "P-521": cryptotls.CurveP521, +} diff --git a/tls/config_test.go b/tls/config_test.go new file mode 100644 index 0000000000..9cb3a32e8e --- /dev/null +++ b/tls/config_test.go @@ -0,0 +1,384 @@ +/* +Copyright 2026 The Knative Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package tls + +import ( + cryptotls "crypto/tls" + "testing" +) + +func TestParseVersion(t *testing.T) { + tests := []struct { + name string + input string + want uint16 + wantErr bool + }{ + {name: "TLS 1.2", input: "1.2", want: cryptotls.VersionTLS12}, + {name: "TLS 1.3", input: "1.3", want: cryptotls.VersionTLS13}, + {name: "unsupported version", input: "1.0", wantErr: true}, + {name: "unsupported version 1.1", input: "1.1", wantErr: true}, + {name: "trailing space", input: "1.2 ", wantErr: true}, + {name: "empty string", input: "", wantErr: true}, + {name: "garbage", input: "abc", wantErr: true}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got, err := ParseVersion(tc.input) + if tc.wantErr { + if err == nil { + t.Fatalf("ParseVersion(%q) = %d, want error", tc.input, got) + } + return + } + if err != nil { + t.Fatalf("ParseVersion(%q) unexpected error: %v", tc.input, err) + } + if got != tc.want { + t.Errorf("ParseVersion(%q) = %d, want %d", tc.input, got, tc.want) + } + }) + } +} + +func TestParseCipherSuites(t *testing.T) { + tests := []struct { + name string + input string + want []uint16 + wantErr bool + }{ + { + name: "single suite", + input: "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + want: []uint16{cryptotls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256}, + }, + { + name: "multiple suites", + input: "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", + want: []uint16{ + cryptotls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + cryptotls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + }, + }, + { + name: "whitespace trimmed", + input: " TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 , TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 ", + want: []uint16{ + cryptotls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + cryptotls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + }, + }, + { + name: "empty parts skipped", + input: "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,,", + want: []uint16{cryptotls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256}, + }, + { + name: "unknown suite", + input: "DOES_NOT_EXIST", + wantErr: true, + }, + { + name: "empty string", + want: []uint16{}, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got, err := ParseCipherSuites(tc.input) + if tc.wantErr { + if err == nil { + t.Fatalf("ParseCipherSuites(%q) = %v, want error", tc.input, got) + } + return + } + if err != nil { + t.Fatalf("ParseCipherSuites(%q) unexpected error: %v", tc.input, err) + } + if len(got) != len(tc.want) { + t.Fatalf("ParseCipherSuites(%q) returned %d suites, want %d", tc.input, len(got), len(tc.want)) + } + for i := range tc.want { + if got[i] != tc.want[i] { + t.Errorf("ParseCipherSuites(%q)[%d] = %d, want %d", tc.input, i, got[i], tc.want[i]) + } + } + }) + } +} + +func TestParseCurvePreferences(t *testing.T) { + tests := []struct { + name string + input string + want []cryptotls.CurveID + wantErr bool + }{ + { + name: "Go constant name X25519", + input: "X25519", + want: []cryptotls.CurveID{cryptotls.X25519}, + }, + { + name: "Go constant name CurveP256", + input: "CurveP256", + want: []cryptotls.CurveID{cryptotls.CurveP256}, + }, + { + name: "standard name P-256", + input: "P-256", + want: []cryptotls.CurveID{cryptotls.CurveP256}, + }, + { + name: "multiple curves with mixed naming", + input: "X25519,P-256,CurveP384", + want: []cryptotls.CurveID{ + cryptotls.X25519, + cryptotls.CurveP256, + cryptotls.CurveP384, + }, + }, + { + name: "whitespace trimmed", + input: " X25519 , CurveP256 ", + want: []cryptotls.CurveID{ + cryptotls.X25519, + cryptotls.CurveP256, + }, + }, + { + name: "all curves by standard name", + input: "P-256,P-384,P-521,X25519", + want: []cryptotls.CurveID{ + cryptotls.CurveP256, + cryptotls.CurveP384, + cryptotls.CurveP521, + cryptotls.X25519, + }, + }, + { + name: "post-quantum hybrid X25519MLKEM768", + input: "X25519MLKEM768", + want: []cryptotls.CurveID{cryptotls.X25519MLKEM768}, + }, + { + name: "unknown curve", + input: "CurveP128", + wantErr: true, + }, + { + name: "empty string", + want: []cryptotls.CurveID{}, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got, err := ParseCurvePreferences(tc.input) + if tc.wantErr { + if err == nil { + t.Fatalf("ParseCurvePreferences(%q) = %v, want error", tc.input, got) + } + return + } + if err != nil { + t.Fatalf("ParseCurvePreferences(%q) unexpected error: %v", tc.input, err) + } + if len(got) != len(tc.want) { + t.Fatalf("ParseCurvePreferences(%q) returned %d curves, want %d", tc.input, len(got), len(tc.want)) + } + for i := range tc.want { + if got[i] != tc.want[i] { + t.Errorf("ParseCurvePreferences(%q)[%d] = %d, want %d", tc.input, i, got[i], tc.want[i]) + } + } + }) + } +} + +func TestNewConfigFromEnv(t *testing.T) { + t.Run("no env vars set returns zero value", func(t *testing.T) { + cfg, err := NewConfigFromEnv("") + if err != nil { + t.Fatal("unexpected error:", err) + } + if cfg.MinVersion != 0 { + t.Errorf("MinVersion = %d, want 0", cfg.MinVersion) + } + if cfg.MaxVersion != 0 { + t.Errorf("MaxVersion = %d, want 0", cfg.MaxVersion) + } + if cfg.CipherSuites != nil { + t.Errorf("CipherSuites = %v, want nil", cfg.CipherSuites) + } + if cfg.CurvePreferences != nil { + t.Errorf("CurvePreferences = %v, want nil", cfg.CurvePreferences) + } + }) + + t.Run("min version from env", func(t *testing.T) { + t.Setenv(MinVersionEnvKey, "1.2") + cfg, err := NewConfigFromEnv("") + if err != nil { + t.Fatal("unexpected error:", err) + } + if cfg.MinVersion != cryptotls.VersionTLS12 { + t.Errorf("MinVersion = %d, want %d", cfg.MinVersion, cryptotls.VersionTLS12) + } + }) + + t.Run("max version from env", func(t *testing.T) { + t.Setenv(MaxVersionEnvKey, "1.3") + cfg, err := NewConfigFromEnv("") + if err != nil { + t.Fatal("unexpected error:", err) + } + if cfg.MaxVersion != cryptotls.VersionTLS13 { + t.Errorf("MaxVersion = %d, want %d", cfg.MaxVersion, cryptotls.VersionTLS13) + } + }) + + t.Run("cipher suites from env", func(t *testing.T) { + t.Setenv(CipherSuitesEnvKey, "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256") + cfg, err := NewConfigFromEnv("") + if err != nil { + t.Fatal("unexpected error:", err) + } + if len(cfg.CipherSuites) != 1 || cfg.CipherSuites[0] != cryptotls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 { + t.Errorf("CipherSuites = %v, want [%d]", cfg.CipherSuites, cryptotls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256) + } + }) + + t.Run("curve preferences from env", func(t *testing.T) { + t.Setenv(CurvePreferencesEnvKey, "X25519,CurveP256") + cfg, err := NewConfigFromEnv("") + if err != nil { + t.Fatal("unexpected error:", err) + } + if len(cfg.CurvePreferences) != 2 { + t.Fatalf("CurvePreferences has %d entries, want 2", len(cfg.CurvePreferences)) + } + if cfg.CurvePreferences[0] != cryptotls.X25519 { + t.Errorf("CurvePreferences[0] = %d, want %d", cfg.CurvePreferences[0], cryptotls.X25519) + } + if cfg.CurvePreferences[1] != cryptotls.CurveP256 { + t.Errorf("CurvePreferences[1] = %d, want %d", cfg.CurvePreferences[1], cryptotls.CurveP256) + } + }) + + t.Run("prefix is prepended to env key", func(t *testing.T) { + t.Setenv("WEBHOOK_TLS_MIN_VERSION", "1.2") + cfg, err := NewConfigFromEnv("WEBHOOK_") + if err != nil { + t.Fatal("unexpected error:", err) + } + if cfg.MinVersion != cryptotls.VersionTLS12 { + t.Errorf("MinVersion = %d, want %d", cfg.MinVersion, cryptotls.VersionTLS12) + } + }) + + t.Run("all env vars set", func(t *testing.T) { + t.Setenv(MinVersionEnvKey, "1.2") + t.Setenv(MaxVersionEnvKey, "1.3") + t.Setenv(CipherSuitesEnvKey, "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384") + t.Setenv(CurvePreferencesEnvKey, "X25519,P-256") + + cfg, err := NewConfigFromEnv("") + if err != nil { + t.Fatal("unexpected error:", err) + } + if cfg.MinVersion != cryptotls.VersionTLS12 { + t.Errorf("MinVersion = %d, want %d", cfg.MinVersion, cryptotls.VersionTLS12) + } + if cfg.MaxVersion != cryptotls.VersionTLS13 { + t.Errorf("MaxVersion = %d, want %d", cfg.MaxVersion, cryptotls.VersionTLS13) + } + if len(cfg.CipherSuites) != 2 { + t.Fatalf("CipherSuites has %d entries, want 2", len(cfg.CipherSuites)) + } + if len(cfg.CurvePreferences) != 2 { + t.Fatalf("CurvePreferences has %d entries, want 2", len(cfg.CurvePreferences)) + } + }) + + t.Run("invalid min version", func(t *testing.T) { + t.Setenv(MinVersionEnvKey, "1.0") + _, err := NewConfigFromEnv("") + if err == nil { + t.Fatal("expected error for invalid min version") + } + }) + + t.Run("invalid max version", func(t *testing.T) { + t.Setenv(MaxVersionEnvKey, "bad") + _, err := NewConfigFromEnv("") + if err == nil { + t.Fatal("expected error for invalid max version") + } + }) + + t.Run("invalid cipher suite", func(t *testing.T) { + t.Setenv(CipherSuitesEnvKey, "NOT_A_REAL_CIPHER") + _, err := NewConfigFromEnv("") + if err == nil { + t.Fatal("expected error for invalid cipher suite") + } + }) + + t.Run("invalid curve", func(t *testing.T) { + t.Setenv(CurvePreferencesEnvKey, "NotACurve") + _, err := NewConfigFromEnv("") + if err == nil { + t.Fatal("expected error for invalid curve") + } + }) +} + +func TestConfig_TLSConfig(t *testing.T) { + cfg := &Config{ + MinVersion: cryptotls.VersionTLS12, + MaxVersion: cryptotls.VersionTLS13, + CipherSuites: []uint16{ + cryptotls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + }, + CurvePreferences: []cryptotls.CurveID{ + cryptotls.X25519, + cryptotls.CurveP256, + }, + } + + tc := cfg.TLSConfig() + + if tc.MinVersion != cryptotls.VersionTLS12 { + t.Errorf("MinVersion = %d, want %d", tc.MinVersion, cryptotls.VersionTLS12) + } + if tc.MaxVersion != cryptotls.VersionTLS13 { + t.Errorf("MaxVersion = %d, want %d", tc.MaxVersion, cryptotls.VersionTLS13) + } + if len(tc.CipherSuites) != 1 || tc.CipherSuites[0] != cryptotls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 { + t.Errorf("CipherSuites = %v, want [%d]", tc.CipherSuites, cryptotls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256) + } + if len(tc.CurvePreferences) != 2 { + t.Fatalf("CurvePreferences has %d entries, want 2", len(tc.CurvePreferences)) + } + if tc.CurvePreferences[0] != cryptotls.X25519 { + t.Errorf("CurvePreferences[0] = %d, want %d", tc.CurvePreferences[0], cryptotls.X25519) + } +} diff --git a/webhook/env.go b/webhook/env.go index e622f5f97b..e1508513c7 100644 --- a/webhook/env.go +++ b/webhook/env.go @@ -72,6 +72,8 @@ func SecretNameFromEnv(defaultSecretName string) string { return secret } +// Deprecated: Use knative.dev/pkg/tls.NewConfigFromEnv or knative.dev/pkg/tls.ParseVersion instead. +// TLS configuration is now read automatically inside webhook.New via the shared tls package. func TLSMinVersionFromEnv(defaultTLSMinVersion uint16) uint16 { switch tlsMinVersion := os.Getenv(tlsMinVersionEnvKey); tlsMinVersion { case "1.2": diff --git a/webhook/webhook.go b/webhook/webhook.go index 378c0b2adb..d4cad301b7 100644 --- a/webhook/webhook.go +++ b/webhook/webhook.go @@ -34,6 +34,7 @@ import ( "knative.dev/pkg/network" "knative.dev/pkg/network/handlers" "knative.dev/pkg/observability/semconv" + knativetls "knative.dev/pkg/tls" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" "go.opentelemetry.io/otel/attribute" @@ -182,11 +183,33 @@ func New( logger := logging.FromContext(ctx) - defaultTLSMinVersion := uint16(tls.VersionTLS13) + tlsCfg, err := knativetls.NewConfigFromEnv("WEBHOOK_") + if err != nil { + return nil, fmt.Errorf("reading TLS configuration from environment: %w", err) + } + + // Replace the TLS configuration with the one from the environment if not set. + // Default to TLS 1.3 as the minimum version when neither the caller nor the + // environment specifies one. if opts.TLSMinVersion == 0 { - opts.TLSMinVersion = TLSMinVersionFromEnv(defaultTLSMinVersion) - } else if opts.TLSMinVersion != tls.VersionTLS12 && opts.TLSMinVersion != tls.VersionTLS13 { - return nil, fmt.Errorf("unsupported TLS version: %d", opts.TLSMinVersion) + if tlsCfg.MinVersion != 0 { + opts.TLSMinVersion = tlsCfg.MinVersion + } else { + opts.TLSMinVersion = tls.VersionTLS13 + } + } + if opts.TLSMaxVersion == 0 && tlsCfg.MaxVersion != 0 { + opts.TLSMaxVersion = tlsCfg.MaxVersion + } + if opts.TLSCipherSuites == nil && len(tlsCfg.CipherSuites) > 0 { + opts.TLSCipherSuites = tlsCfg.CipherSuites + } + if opts.TLSCurvePreferences == nil && len(tlsCfg.CurvePreferences) > 0 { + opts.TLSCurvePreferences = tlsCfg.CurvePreferences + } + + if opts.TLSMinVersion != 0 && opts.TLSMinVersion != tls.VersionTLS12 && opts.TLSMinVersion != tls.VersionTLS13 { + return nil, fmt.Errorf("unsupported TLS minimum version %d: must be TLS 1.2 or TLS 1.3", opts.TLSMinVersion) } syncCtx, cancel := context.WithCancel(context.Background()) diff --git a/webhook/webhook_test.go b/webhook/webhook_test.go index d4bb521dff..46f2e17552 100644 --- a/webhook/webhook_test.go +++ b/webhook/webhook_test.go @@ -242,6 +242,147 @@ func TestTLSCurvePreferencesWebhookOption(t *testing.T) { }) } +func TestTLSConfigFromEnvironment(t *testing.T) { + t.Run("env min version used when opts min version is zero", func(t *testing.T) { + t.Setenv("WEBHOOK_TLS_MIN_VERSION", "1.2") + opts := newDefaultOptions() + wh, err := newAdmissionControllerWebhook(t, opts) + if err != nil { + t.Fatal("Unexpected error", err) + } + if wh.tlsConfig == nil { + t.Fatal("Expected tlsConfig to be set") + } + if wh.tlsConfig.MinVersion != tls.VersionTLS12 { + t.Errorf("Expected MinVersion from env to be TLS 1.2, got %d", wh.tlsConfig.MinVersion) + } + }) + + t.Run("opts min version takes precedence over env", func(t *testing.T) { + t.Setenv("WEBHOOK_TLS_MIN_VERSION", "1.2") + opts := newDefaultOptions() + opts.TLSMinVersion = tls.VersionTLS13 + wh, err := newAdmissionControllerWebhook(t, opts) + if err != nil { + t.Fatal("Unexpected error", err) + } + if wh.tlsConfig == nil { + t.Fatal("Expected tlsConfig to be set") + } + if wh.tlsConfig.MinVersion != tls.VersionTLS13 { + t.Errorf("Expected MinVersion from opts (TLS 1.3), got %d", wh.tlsConfig.MinVersion) + } + }) + + t.Run("env max version used when opts max version is zero", func(t *testing.T) { + t.Setenv("WEBHOOK_TLS_MAX_VERSION", "1.3") + opts := newDefaultOptions() + wh, err := newAdmissionControllerWebhook(t, opts) + if err != nil { + t.Fatal("Unexpected error", err) + } + if wh.tlsConfig == nil { + t.Fatal("Expected tlsConfig to be set") + } + if wh.tlsConfig.MaxVersion != tls.VersionTLS13 { + t.Errorf("Expected MaxVersion from env to be TLS 1.3, got %d", wh.tlsConfig.MaxVersion) + } + }) + + t.Run("opts max version takes precedence over env", func(t *testing.T) { + t.Setenv("WEBHOOK_TLS_MAX_VERSION", "1.2") + opts := newDefaultOptions() + opts.TLSMaxVersion = tls.VersionTLS13 + wh, err := newAdmissionControllerWebhook(t, opts) + if err != nil { + t.Fatal("Unexpected error", err) + } + if wh.tlsConfig == nil { + t.Fatal("Expected tlsConfig to be set") + } + if wh.tlsConfig.MaxVersion != tls.VersionTLS13 { + t.Errorf("Expected MaxVersion from opts (TLS 1.3), got %d", wh.tlsConfig.MaxVersion) + } + }) + + t.Run("env cipher suites used when opts cipher suites is nil", func(t *testing.T) { + t.Setenv("WEBHOOK_TLS_CIPHER_SUITES", "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256") + opts := newDefaultOptions() + wh, err := newAdmissionControllerWebhook(t, opts) + if err != nil { + t.Fatal("Unexpected error", err) + } + if wh.tlsConfig == nil { + t.Fatal("Expected tlsConfig to be set") + } + if len(wh.tlsConfig.CipherSuites) != 1 || wh.tlsConfig.CipherSuites[0] != tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 { + t.Errorf("Expected CipherSuites from env, got %v", wh.tlsConfig.CipherSuites) + } + }) + + t.Run("opts cipher suites take precedence over env", func(t *testing.T) { + t.Setenv("WEBHOOK_TLS_CIPHER_SUITES", "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256") + opts := newDefaultOptions() + opts.TLSCipherSuites = []uint16{tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384} + wh, err := newAdmissionControllerWebhook(t, opts) + if err != nil { + t.Fatal("Unexpected error", err) + } + if wh.tlsConfig == nil { + t.Fatal("Expected tlsConfig to be set") + } + if len(wh.tlsConfig.CipherSuites) != 1 || wh.tlsConfig.CipherSuites[0] != tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 { + t.Errorf("Expected CipherSuites from opts, got %v", wh.tlsConfig.CipherSuites) + } + }) + + t.Run("env curve preferences used when opts curve preferences is nil", func(t *testing.T) { + t.Setenv("WEBHOOK_TLS_CURVE_PREFERENCES", "X25519,CurveP256") + opts := newDefaultOptions() + wh, err := newAdmissionControllerWebhook(t, opts) + if err != nil { + t.Fatal("Unexpected error", err) + } + if wh.tlsConfig == nil { + t.Fatal("Expected tlsConfig to be set") + } + if len(wh.tlsConfig.CurvePreferences) != 2 { + t.Fatalf("Expected 2 curve preferences from env, got %d", len(wh.tlsConfig.CurvePreferences)) + } + if wh.tlsConfig.CurvePreferences[0] != tls.X25519 { + t.Errorf("Expected CurvePreferences[0] = X25519, got %d", wh.tlsConfig.CurvePreferences[0]) + } + if wh.tlsConfig.CurvePreferences[1] != tls.CurveP256 { + t.Errorf("Expected CurvePreferences[1] = CurveP256, got %d", wh.tlsConfig.CurvePreferences[1]) + } + }) + + t.Run("opts curve preferences take precedence over env", func(t *testing.T) { + t.Setenv("WEBHOOK_TLS_CURVE_PREFERENCES", "X25519") + opts := newDefaultOptions() + opts.TLSCurvePreferences = []tls.CurveID{tls.CurveP384} + wh, err := newAdmissionControllerWebhook(t, opts) + if err != nil { + t.Fatal("Unexpected error", err) + } + if wh.tlsConfig == nil { + t.Fatal("Expected tlsConfig to be set") + } + if len(wh.tlsConfig.CurvePreferences) != 1 || wh.tlsConfig.CurvePreferences[0] != tls.CurveP384 { + t.Errorf("Expected CurvePreferences from opts, got %v", wh.tlsConfig.CurvePreferences) + } + }) + + t.Run("invalid env TLS config returns error", func(t *testing.T) { + t.Setenv("WEBHOOK_TLS_MIN_VERSION", "bad") + opts := newDefaultOptions() + _, err := newAdmissionControllerWebhook(t, opts) + if err == nil { + t.Fatal("Expected error for invalid env TLS min version") + } + }) +} + func TestTLSConfigCombinedOptions(t *testing.T) { opts := newDefaultOptions() t.Run("when all TLS options are configured together", func(t *testing.T) {