diff --git a/cmd/activator/main.go b/cmd/activator/main.go index a2cbaf0f1adb..27d5cfcfa5c2 100644 --- a/cmd/activator/main.go +++ b/cmd/activator/main.go @@ -18,7 +18,6 @@ package main import ( "context" - "crypto/tls" "errors" "fmt" "log" @@ -47,6 +46,7 @@ import ( pkglogging "knative.dev/pkg/logging" "knative.dev/pkg/logging/logkey" pkgnet "knative.dev/pkg/network" + knativetls "knative.dev/pkg/network/tls" k8sruntime "knative.dev/pkg/observability/runtime/k8s" "knative.dev/pkg/signals" "knative.dev/pkg/system" @@ -292,7 +292,7 @@ func main() { "profile": pprof.Server, } - errCh := make(chan error, len(servers)) + errCh := make(chan error, len(servers)+1) for name, server := range servers { go func(name string, s *http.Server) { // Don't forward ErrServerClosed as that indicates we're already shutting down. @@ -306,17 +306,21 @@ func main() { // At this moment activator with TLS does not disable HTTP. // See also https://github.com/knative/serving/issues/12808. if tlsEnabled { - name, server := "https", pkgnet.NewServer(":"+strconv.Itoa(networking.BackendHTTPSPort), ah) - go func(name string, s *http.Server) { - s.TLSConfig = &tls.Config{ - MinVersion: tls.VersionTLS13, - GetCertificate: certCache.GetCertificate, - } + tlsCfg, err := knativetls.DefaultConfigFromEnv("ACTIVATOR_") + if err != nil { + logger.Fatalw("Failed to read TLS configuration from environment", zap.Error(err)) + } + + server := pkgnet.NewServer(":"+strconv.Itoa(networking.BackendHTTPSPort), ah) + servers["https"] = server + go func(s *http.Server) { + s.TLSConfig = tlsCfg + s.TLSConfig.GetCertificate = certCache.GetCertificate // Don't forward ErrServerClosed as that indicates we're already shutting down. if err := s.ListenAndServeTLS("", ""); err != nil && !errors.Is(err, http.ErrServerClosed) { - errCh <- fmt.Errorf("%s server failed: %w", name, err) + errCh <- fmt.Errorf("https server failed: %w", err) } - }(name, server) + }(server) } // Wait for the signal to drain. diff --git a/config/core/configmaps/deployment.yaml b/config/core/configmaps/deployment.yaml index 026760ef1104..9a4197090c3a 100644 --- a/config/core/configmaps/deployment.yaml +++ b/config/core/configmaps/deployment.yaml @@ -22,7 +22,7 @@ metadata: app.kubernetes.io/component: controller app.kubernetes.io/version: devel annotations: - knative.dev/example-checksum: "b99000ec" + knative.dev/example-checksum: "555b4826" data: # This is the Go import path for the binary that is containerized # and substituted here. @@ -92,6 +92,25 @@ data: # If omitted, or empty, no rootCA is added to the golang rootCAs queue-sidecar-rootca: "" + # Sets the minimum TLS version for the queue proxy sidecar's TLS server. + # Accepted values: "1.2", "1.3". Default is "1.3" if not specified. + queue-sidecar-tls-min-version: "" + + # Sets the maximum TLS version for the queue proxy sidecar's TLS server. + # Accepted values: "1.2", "1.3". If omitted, the Go default is used. + queue-sidecar-tls-max-version: "" + + # Sets the cipher suites for the queue proxy sidecar's TLS server. + # Comma-separated list of cipher suite names (e.g. "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"). + # If omitted, the Go default cipher suites are used. + # Note: cipher suites are not configurable in TLS 1.3. + queue-sidecar-tls-cipher-suites: "" + + # Sets the elliptic curve preferences for the queue proxy sidecar's TLS server. + # Comma-separated list of curve names (e.g. "X25519,CurveP256"). + # If omitted, the Go default curves are used. + queue-sidecar-tls-curve-preferences: "" + # If set, it automatically configures pod anti-affinity requirements for all Knative services. # It employs the `preferredDuringSchedulingIgnoredDuringExecution` weighted pod affinity term, # aligning with the Knative revision label. It yields the configuration below in all workloads' deployments: diff --git a/go.mod b/go.mod index 56997267d581..26b5ec44f235 100644 --- a/go.mod +++ b/go.mod @@ -42,7 +42,7 @@ require ( knative.dev/caching v0.0.0-20260120130310-f1fc03b7f5ee knative.dev/hack v0.0.0-20260120115810-bf6758cba446 knative.dev/networking v0.0.0-20260120131110-a7cdca238a0d - knative.dev/pkg v0.0.0-20260120122510-4a022ed9999a + knative.dev/pkg v0.0.0-20260319144603-18c5d580ae64 sigs.k8s.io/randfill v1.0.0 sigs.k8s.io/yaml v1.6.0 ) diff --git a/go.sum b/go.sum index fec3e3a65da5..dfb8c7f2d52a 100644 --- a/go.sum +++ b/go.sum @@ -550,8 +550,8 @@ knative.dev/hack v0.0.0-20260120115810-bf6758cba446 h1:Y8raYHIuAL9/gUKGYD9/dD+Eq knative.dev/hack v0.0.0-20260120115810-bf6758cba446/go.mod h1:L5RzHgbvam0u8QFHfzCX6MKxu/a/gIGEdaRBqNiVbl0= knative.dev/networking v0.0.0-20260120131110-a7cdca238a0d h1:QQ5QeH5MZlYdu6gRGGjmEBQ/CVoatiXKnVIrUoKlj4U= knative.dev/networking v0.0.0-20260120131110-a7cdca238a0d/go.mod h1:mZD2edO8op3zI6yLOn1yoz8xqbyZfrrDOJCbYElMEKs= -knative.dev/pkg v0.0.0-20260120122510-4a022ed9999a h1:9f29OTA7w/iVIX6PS6yveVVzNbcUS74eQfchVe8o2/4= -knative.dev/pkg v0.0.0-20260120122510-4a022ed9999a/go.mod h1:Tz3GoxcNC5vH3Zo//cW3mnHL474u+Y1wbsUIZ11p8No= +knative.dev/pkg v0.0.0-20260319144603-18c5d580ae64 h1:TiwrcgUKNePfdAbaJT9W4P57lsKjiZnjJ0wVC6XrL0U= +knative.dev/pkg v0.0.0-20260319144603-18c5d580ae64/go.mod h1:Tz3GoxcNC5vH3Zo//cW3mnHL474u+Y1wbsUIZ11p8No= pgregory.net/rapid v1.1.0 h1:CMa0sjHSru3puNx+J0MIAuiiEV4N0qj8/cMWGBBCsjw= pgregory.net/rapid v1.1.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= sigs.k8s.io/gateway-api v1.1.0 h1:DsLDXCi6jR+Xz8/xd0Z1PYl2Pn0TyaFMOPPZIj4inDM= diff --git a/pkg/deployment/config.go b/pkg/deployment/config.go index 990fcdeff00c..6ebcf3771d23 100644 --- a/pkg/deployment/config.go +++ b/pkg/deployment/config.go @@ -74,6 +74,12 @@ const ( queueSidecarTokenAudiencesKey = "queue-sidecar-token-audiences" queueSidecarRooCAKey = "queue-sidecar-rootca" + // queueSidecar TLS configuration keys. + queueSidecarTLSMinVersionKey = "queue-sidecar-tls-min-version" + queueSidecarTLSMaxVersionKey = "queue-sidecar-tls-max-version" + queueSidecarTLSCipherSuitesKey = "queue-sidecar-tls-cipher-suites" + queueSidecarTLSCurvePreferencesKey = "queue-sidecar-tls-curve-preferences" + defaultAffinityTypeKey = "default-affinity-type" defaultAffinityTypeValue = PreferSpreadRevisionOverNodes @@ -202,6 +208,11 @@ func NewConfigFromMap(configMap map[string]string) (*Config, error) { cm.AsStringSet(queueSidecarTokenAudiencesKey, &nc.QueueSidecarTokenAudiences), cm.AsString(queueSidecarRooCAKey, &nc.QueueSidecarRootCA), + cm.AsString(queueSidecarTLSMinVersionKey, &nc.QueueSidecarTLSMinVersion), + cm.AsString(queueSidecarTLSMaxVersionKey, &nc.QueueSidecarTLSMaxVersion), + cm.AsString(queueSidecarTLSCipherSuitesKey, &nc.QueueSidecarTLSCipherSuites), + cm.AsString(queueSidecarTLSCurvePreferencesKey, &nc.QueueSidecarTLSCurvePreferences), + cm.AsString(RuntimeClassNameKey, &runtimeClassNames), cm.AsBool(podIsAlwaysSchedulableKey, &nc.PodIsAlwaysSchedulable), @@ -308,6 +319,18 @@ type Config struct { // QueueSidecarRootCA is a root certificate to be trusted by the queue proxy sidecar qpoptions. QueueSidecarRootCA string + // QueueSidecarTLSMinVersion is the minimum TLS version for the queue proxy sidecar (e.g. "1.2", "1.3"). + QueueSidecarTLSMinVersion string + + // QueueSidecarTLSMaxVersion is the maximum TLS version for the queue proxy sidecar (e.g. "1.2", "1.3"). + QueueSidecarTLSMaxVersion string + + // QueueSidecarTLSCipherSuites is a comma-separated list of cipher suites for the queue proxy sidecar. + QueueSidecarTLSCipherSuites string + + // QueueSidecarTLSCurvePreferences is a comma-separated list of elliptic curves for the queue proxy sidecar. + QueueSidecarTLSCurvePreferences string + // DefaultAffinityType is a string that controls what affinity rules will be automatically // applied to the PodSpec of all Knative services. DefaultAffinityType AffinityType diff --git a/pkg/deployment/config_test.go b/pkg/deployment/config_test.go index c44a8782290e..7d783ea2829e 100644 --- a/pkg/deployment/config_test.go +++ b/pkg/deployment/config_test.go @@ -472,6 +472,28 @@ kata: podIsAlwaysSchedulableKey: "true", QueueSidecarImageKey: defaultSidecarImage, }, + }, { + name: "controller configuration with queue sidecar TLS settings", + wantConfig: &Config{ + RegistriesSkippingTagResolving: sets.New("kind.local", "ko.local", "dev.local"), + DigestResolutionTimeout: digestResolutionTimeoutDefault, + QueueSidecarImage: defaultSidecarImage, + QueueSidecarCPURequest: &QueueSidecarCPURequestDefault, + QueueSidecarTokenAudiences: sets.New(""), + ProgressDeadline: ProgressDeadlineDefault, + DefaultAffinityType: defaultAffinityTypeValue, + QueueSidecarTLSMinVersion: "1.2", + QueueSidecarTLSMaxVersion: "1.3", + QueueSidecarTLSCipherSuites: "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + QueueSidecarTLSCurvePreferences: "X25519,CurveP256", + }, + data: map[string]string{ + QueueSidecarImageKey: defaultSidecarImage, + queueSidecarTLSMinVersionKey: "1.2", + queueSidecarTLSMaxVersionKey: "1.3", + queueSidecarTLSCipherSuitesKey: "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + queueSidecarTLSCurvePreferencesKey: "X25519,CurveP256", + }, }} for _, tt := range configTests { diff --git a/pkg/queue/sharedmain/main.go b/pkg/queue/sharedmain/main.go index 5307f0ca22a6..d40010ec6d2d 100644 --- a/pkg/queue/sharedmain/main.go +++ b/pkg/queue/sharedmain/main.go @@ -18,7 +18,6 @@ package sharedmain import ( "context" - "crypto/tls" "encoding/json" "errors" "fmt" @@ -31,6 +30,7 @@ import ( netproxy "knative.dev/networking/pkg/http/proxy" pkghandler "knative.dev/pkg/network/handlers" + knativetls "knative.dev/pkg/network/tls" "knative.dev/serving/pkg/activator" "github.com/kelseyhightower/envconfig" @@ -261,23 +261,10 @@ func Main(opts ...Option) error { httpServers["profile"] = pprof.Server } - tlsServers := make(map[string]*http.Server) + var tlsServer *http.Server var certWatcher *certificate.CertWatcher var err error - if tlsEnabled { - tlsServers["main"] = mainServer(":"+env.QueueServingTLSPort, mainHandler) - // Keep admin server on HTTP even with TLS enabled since it's only accessed locally by kubelet - - certWatcher, err = certificate.NewCertWatcher(certPath, keyPath, 1*time.Minute, logger) - if err != nil { - logger.Fatal("failed to create certWatcher", zap.Error(err)) - } - defer certWatcher.Stop() - } - - logger.Info("Starting queue-proxy") - errCh := make(chan error) for name, server := range httpServers { go func(name string, s *http.Server) { @@ -288,20 +275,34 @@ func Main(opts ...Option) error { } }(name, server) } - for name, server := range tlsServers { - go func(name string, s *http.Server) { - logger.Info("Starting tls server ", name, s.Addr) - s.TLSConfig = &tls.Config{ - GetCertificate: certWatcher.GetCertificate, - MinVersion: tls.VersionTLS13, - } + + if tlsEnabled { + tlsServer = mainServer(":"+env.QueueServingTLSPort, mainHandler) + // Keep admin server on HTTP even with TLS enabled since it's only accessed locally by kubelet + + certWatcher, err = certificate.NewCertWatcher(certPath, keyPath, 1*time.Minute, logger) + if err != nil { + logger.Fatal("failed to create certWatcher", zap.Error(err)) + } + defer certWatcher.Stop() + + tlsCfg, err := knativetls.DefaultConfigFromEnv("QUEUE_PROXY_") + if err != nil { + logger.Fatalw("Failed to read TLS configuration from environment", zap.Error(err)) + } + go func() { + logger.Info("Starting tls server main ", tlsServer.Addr) + tlsServer.TLSConfig = tlsCfg + tlsServer.TLSConfig.GetCertificate = certWatcher.GetCertificate // Don't forward ErrServerClosed as that indicates we're already shutting down. - if err := s.ListenAndServeTLS("", ""); err != nil && !errors.Is(err, http.ErrServerClosed) { - errCh <- fmt.Errorf("%s server failed to serve: %w", name, err) + if err := tlsServer.ListenAndServeTLS("", ""); err != nil && !errors.Is(err, http.ErrServerClosed) { + errCh <- fmt.Errorf("main tls server failed to serve: %w", err) } - }(name, server) + }() } + logger.Info("Starting queue-proxy") + // Blocks until we actually receive a TERM signal or one of the servers // exits unexpectedly. We fold both signals together because we only want // to act on the first of those to reach here. @@ -320,10 +321,10 @@ func Main(opts ...Option) error { logger.Errorw("Failed to shutdown server", zap.String("server", name), zap.Error(err)) } } - for name, srv := range tlsServers { - logger.Info("Shutting down server: ", name) - if err := srv.Shutdown(context.Background()); err != nil { - logger.Errorw("Failed to shutdown server", zap.String("server", name), zap.Error(err)) + if tlsServer != nil { + logger.Info("Shutting down server: main tls") + if err := tlsServer.Shutdown(context.Background()); err != nil { + logger.Errorw("Failed to shutdown server", zap.String("server", "main tls"), zap.Error(err)) } } diff --git a/pkg/reconciler/revision/resolve.go b/pkg/reconciler/revision/resolve.go index dc967a00a09d..fc73ce2d058f 100644 --- a/pkg/reconciler/revision/resolve.go +++ b/pkg/reconciler/revision/resolve.go @@ -18,7 +18,6 @@ package revision import ( "context" - "crypto/tls" "crypto/x509" "errors" "fmt" @@ -30,6 +29,7 @@ import ( "github.com/google/go-containerregistry/pkg/v1/remote" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/client-go/kubernetes" + knativetls "knative.dev/pkg/network/tls" ) type digestResolver struct { @@ -43,7 +43,7 @@ const ( // https://kubernetes.io/docs/tasks/tls/managing-tls-in-a-cluster/#trusting-tls-in-a-cluster k8sCertPath = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt" - tlsMinVersionEnvKey = "TAG_TO_DIGEST_TLS_MIN_VERSION" + tlsEnvPrefix = "TAG_TO_DIGEST_" ) // newResolverTransport returns an http.Transport that appends the certs bundle @@ -62,31 +62,20 @@ func newResolverTransport(path string, maxIdleConns, maxIdleConnsPerHost int) (* return nil, errors.New("failed to append k8s cert bundle to cert pool") } + cfg, err := knativetls.DefaultConfigFromEnv(tlsEnvPrefix) + if err != nil { + return nil, err + } + transport := http.DefaultTransport.(*http.Transport).Clone() transport.MaxIdleConns = maxIdleConns transport.MaxIdleConnsPerHost = maxIdleConnsPerHost - //nolint:gosec // quay.io still required 1.2 - bump if they've moved up - transport.TLSClientConfig = &tls.Config{ - MinVersion: tlsMinVersionFromEnv(tls.VersionTLS12), - RootCAs: pool, - } + transport.TLSClientConfig = cfg + transport.TLSClientConfig.RootCAs = pool return transport, nil } -func tlsMinVersionFromEnv(defaultTLSMinVersion uint16) uint16 { - switch tlsMinVersion := os.Getenv(tlsMinVersionEnvKey); tlsMinVersion { - case "1.2": - return tls.VersionTLS12 - case "1.3": - return tls.VersionTLS13 - case "": - return defaultTLSMinVersion - default: - panic(fmt.Sprintf("the environment variable %q has to be either '1.2' or '1.3'", tlsMinVersionEnvKey)) - } -} - // Resolve resolves the image references that use tags to digests. func (r *digestResolver) Resolve( ctx context.Context, diff --git a/pkg/reconciler/revision/resolve_test.go b/pkg/reconciler/revision/resolve_test.go index 1b7f6e73be7c..7b0e02608a11 100644 --- a/pkg/reconciler/revision/resolve_test.go +++ b/pkg/reconciler/revision/resolve_test.go @@ -509,7 +509,7 @@ func TestNewResolverTransport_TLSMinVersion(t *testing.T) { name string envOverride string expectedMinTLS uint16 - expectedPanic bool + wantErr bool }{{ name: "TLS 1.2", envOverride: "1.2", @@ -519,30 +519,38 @@ func TestNewResolverTransport_TLSMinVersion(t *testing.T) { envOverride: "1.3", expectedMinTLS: tls.VersionTLS13, }, { - name: "default TLS 1.2", + name: "default TLS 1.3", envOverride: "", - expectedMinTLS: tls.VersionTLS12, + expectedMinTLS: tls.VersionTLS13, + }, { + name: "invalid version", + envOverride: "1.1", + wantErr: true, }} tmpDir := t.TempDir() for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - t.Setenv(tlsMinVersionEnvKey, tc.envOverride) + t.Setenv(tlsEnvPrefix+"TLS_MIN_VERSION", tc.envOverride) - // noop for this test path, err := writeCertFile(tmpDir, "cert.pem", []byte(certPEM)) if err != nil { t.Fatal("Failed to write cert bundle file:", err) } - // The actual test. - if tr, err := newResolverTransport(path, 100, 100); err != nil { - t.Error("Got unexpected err:", err) - } else if err == nil { - if diff := cmp.Diff(tc.expectedMinTLS, tr.TLSClientConfig.MinVersion); diff != "" { - t.Errorf("expected min TLS version does not match: %s", diff) + tr, err := newResolverTransport(path, 100, 100) + if tc.wantErr { + if err == nil { + t.Fatal("Expected error, got nil") } + return + } + if err != nil { + t.Fatal("Got unexpected err:", err) + } + if diff := cmp.Diff(tc.expectedMinTLS, tr.TLSClientConfig.MinVersion); diff != "" { + t.Errorf("expected min TLS version does not match: %s", diff) } }) } diff --git a/pkg/reconciler/revision/resources/deploy_test.go b/pkg/reconciler/revision/resources/deploy_test.go index 873c7e4d8e52..44abafe49a42 100644 --- a/pkg/reconciler/revision/resources/deploy_test.go +++ b/pkg/reconciler/revision/resources/deploy_test.go @@ -158,6 +158,18 @@ var ( }, { Name: "ROOT_CA", Value: "", + }, { + Name: "QUEUE_PROXY_TLS_MIN_VERSION", + Value: "", + }, { + Name: "QUEUE_PROXY_TLS_MAX_VERSION", + Value: "", + }, { + Name: "QUEUE_PROXY_TLS_CIPHER_SUITES", + Value: "", + }, { + Name: "QUEUE_PROXY_TLS_CURVE_PREFERENCES", + Value: "", }, { Name: "ENABLE_MULTI_CONTAINER_PROBES", Value: "false", diff --git a/pkg/reconciler/revision/resources/queue.go b/pkg/reconciler/revision/resources/queue.go index 23348cf67389..60a2292df044 100644 --- a/pkg/reconciler/revision/resources/queue.go +++ b/pkg/reconciler/revision/resources/queue.go @@ -428,6 +428,18 @@ func makeQueueContainer(rev *v1.Revision, cfg *config.Config) (*corev1.Container }, { Name: "ROOT_CA", Value: cfg.Deployment.QueueSidecarRootCA, + }, { + Name: "QUEUE_PROXY_TLS_MIN_VERSION", + Value: cfg.Deployment.QueueSidecarTLSMinVersion, + }, { + Name: "QUEUE_PROXY_TLS_MAX_VERSION", + Value: cfg.Deployment.QueueSidecarTLSMaxVersion, + }, { + Name: "QUEUE_PROXY_TLS_CIPHER_SUITES", + Value: cfg.Deployment.QueueSidecarTLSCipherSuites, + }, { + Name: "QUEUE_PROXY_TLS_CURVE_PREFERENCES", + Value: cfg.Deployment.QueueSidecarTLSCurvePreferences, }, { Name: "ENABLE_MULTI_CONTAINER_PROBES", Value: strconv.FormatBool(multiContainerProbingEnabled), diff --git a/pkg/reconciler/revision/resources/queue_test.go b/pkg/reconciler/revision/resources/queue_test.go index 1f7f05a4bc61..56ee18165d15 100644 --- a/pkg/reconciler/revision/resources/queue_test.go +++ b/pkg/reconciler/revision/resources/queue_test.go @@ -1122,6 +1122,10 @@ var defaultEnv = map[string]string{ "SYSTEM_NAMESPACE": system.Namespace(), "USER_PORT": strconv.Itoa(v1.DefaultUserPort), "ROOT_CA": "", + "QUEUE_PROXY_TLS_MIN_VERSION": "", + "QUEUE_PROXY_TLS_MAX_VERSION": "", + "QUEUE_PROXY_TLS_CIPHER_SUITES": "", + "QUEUE_PROXY_TLS_CURVE_PREFERENCES": "", "ENABLE_MULTI_CONTAINER_PROBES": "false", "OBSERVABILITY_CONFIG": `{"tracing":{},"metrics":{},"runtime":{},"requestMetrics":{}}`, } diff --git a/vendor/knative.dev/pkg/network/tls/config.go b/vendor/knative.dev/pkg/network/tls/config.go new file mode 100644 index 000000000000..6cd205baeb25 --- /dev/null +++ b/vendor/knative.dev/pkg/network/tls/config.go @@ -0,0 +1,156 @@ +/* +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" +) + +// DefaultConfigFromEnv returns a tls.Config with secure defaults. +// 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. +func DefaultConfigFromEnv(prefix string) (*cryptotls.Config, error) { + cfg := &cryptotls.Config{ + MinVersion: cryptotls.VersionTLS13, + } + + 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 +} + +// 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/vendor/knative.dev/pkg/webhook/env.go b/vendor/knative.dev/pkg/webhook/env.go index e622f5f97b5f..6d3d32203f4a 100644 --- a/vendor/knative.dev/pkg/webhook/env.go +++ b/vendor/knative.dev/pkg/webhook/env.go @@ -72,6 +72,8 @@ func SecretNameFromEnv(defaultSecretName string) string { return secret } +// Deprecated: Use knative.dev/pkg/network/tls.DefaultConfigFromEnv 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/vendor/knative.dev/pkg/webhook/webhook.go b/vendor/knative.dev/pkg/webhook/webhook.go index badc7fef8345..e8895db75e58 100644 --- a/vendor/knative.dev/pkg/webhook/webhook.go +++ b/vendor/knative.dev/pkg/webhook/webhook.go @@ -33,6 +33,7 @@ import ( kubeinformerfactory "knative.dev/pkg/injection/clients/namespacedkube/informers/factory" "knative.dev/pkg/network" "knative.dev/pkg/network/handlers" + knativetls "knative.dev/pkg/network/tls" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" "go.opentelemetry.io/otel/metric" @@ -46,7 +47,15 @@ import ( "knative.dev/pkg/system" ) -// Options contains the configuration for the webhook +// Options contains the configuration for the webhook. +// +// TLS fields (TLSMinVersion, TLSMaxVersion, TLSCipherSuites, TLSCurvePreferences) +// are resolved with the following precedence: +// 1. Values set explicitly in Options (programmatic). +// 2. WEBHOOK_TLS_* environment variables (WEBHOOK_TLS_MIN_VERSION, +// WEBHOOK_TLS_MAX_VERSION, WEBHOOK_TLS_CIPHER_SUITES, WEBHOOK_TLS_CURVE_PREFERENCES). +// 3. Defaults (TLS 1.3 minimum version; zero values for the rest, meaning the +// Go standard library picks its defaults). type Options struct { // TLSMinVersion contains the minimum TLS version that is acceptable to communicate with the API server. // TLS 1.3 is the minimum version if not specified otherwise. @@ -180,11 +189,29 @@ func New( logger := logging.FromContext(ctx) - defaultTLSMinVersion := uint16(tls.VersionTLS13) - 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) + tlsCfg, err := knativetls.DefaultConfigFromEnv("WEBHOOK_") + if err != nil { + return nil, fmt.Errorf("reading TLS configuration from environment: %w", err) + } + + if opts.TLSMinVersion != 0 { + tlsCfg.MinVersion = opts.TLSMinVersion + } + if opts.TLSMaxVersion != 0 { + tlsCfg.MaxVersion = opts.TLSMaxVersion + } + if opts.TLSCipherSuites != nil { + tlsCfg.CipherSuites = opts.TLSCipherSuites + } + if opts.TLSCurvePreferences != nil { + tlsCfg.CurvePreferences = opts.TLSCurvePreferences + } + + if tlsCfg.MinVersion != tls.VersionTLS12 && tlsCfg.MinVersion != tls.VersionTLS13 { + return nil, fmt.Errorf("unsupported TLS minimum version %d: must be TLS 1.2 or TLS 1.3", tlsCfg.MinVersion) + } + if tlsCfg.MaxVersion != 0 && tlsCfg.MinVersion > tlsCfg.MaxVersion { + return nil, fmt.Errorf("TLS minimum version (%#x) is greater than maximum version (%#x)", tlsCfg.MinVersion, tlsCfg.MaxVersion) } syncCtx, cancel := context.WithCancel(context.Background()) @@ -204,42 +231,35 @@ func New( // a new secret informer from it. secretInformer := kubeinformerfactory.Get(ctx).Core().V1().Secrets() - //nolint:gosec // operator configures TLS min version (default is 1.3) - webhook.tlsConfig = &tls.Config{ - MinVersion: opts.TLSMinVersion, - MaxVersion: opts.TLSMaxVersion, - CipherSuites: opts.TLSCipherSuites, - CurvePreferences: opts.TLSCurvePreferences, - - // If we return (nil, error) the client sees - 'tls: internal error" - // If we return (nil, nil) the client sees - 'tls: no certificates configured' - // - // We'll return (nil, nil) when we don't find a certificate - GetCertificate: func(*tls.ClientHelloInfo) (*tls.Certificate, error) { - secret, err := secretInformer.Lister().Secrets(system.Namespace()).Get(opts.SecretName) - if err != nil { - logger.Errorw("failed to fetch secret", zap.Error(err)) - return nil, nil - } - webOpts := GetOptions(ctx) - sKey, sCert := getSecretDataKeyNamesOrDefault(webOpts.ServerPrivateKeyName, webOpts.ServerCertificateName) - serverKey, ok := secret.Data[sKey] - if !ok { - logger.Warn("server key missing") - return nil, nil - } - serverCert, ok := secret.Data[sCert] - if !ok { - logger.Warn("server cert missing") - return nil, nil - } - cert, err := tls.X509KeyPair(serverCert, serverKey) - if err != nil { - return nil, err - } - return &cert, nil - }, + // If we return (nil, error) the client sees - 'tls: internal error' + // If we return (nil, nil) the client sees - 'tls: no certificates configured' + // + // We'll return (nil, nil) when we don't find a certificate + tlsCfg.GetCertificate = func(*tls.ClientHelloInfo) (*tls.Certificate, error) { + secret, err := secretInformer.Lister().Secrets(system.Namespace()).Get(opts.SecretName) + if err != nil { + logger.Errorw("failed to fetch secret", zap.Error(err)) + return nil, nil + } + webOpts := GetOptions(ctx) + sKey, sCert := getSecretDataKeyNamesOrDefault(webOpts.ServerPrivateKeyName, webOpts.ServerCertificateName) + serverKey, ok := secret.Data[sKey] + if !ok { + logger.Warn("server key missing") + return nil, nil + } + serverCert, ok := secret.Data[sCert] + if !ok { + logger.Warn("server cert missing") + return nil, nil + } + cert, err := tls.X509KeyPair(serverCert, serverKey) + if err != nil { + return nil, err + } + return &cert, nil } + webhook.tlsConfig = tlsCfg } webhook.mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { diff --git a/vendor/modules.txt b/vendor/modules.txt index 70ac0f376232..aae6900c11fc 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1536,7 +1536,7 @@ knative.dev/networking/pkg/http/proxy knative.dev/networking/pkg/http/stats knative.dev/networking/pkg/ingress knative.dev/networking/pkg/prober -# knative.dev/pkg v0.0.0-20260120122510-4a022ed9999a +# knative.dev/pkg v0.0.0-20260319144603-18c5d580ae64 ## explicit; go 1.24.0 knative.dev/pkg/apiextensions/storageversion knative.dev/pkg/apiextensions/storageversion/cmd/migrate @@ -1618,6 +1618,7 @@ knative.dev/pkg/logging/logkey knative.dev/pkg/logging/testing knative.dev/pkg/network knative.dev/pkg/network/handlers +knative.dev/pkg/network/tls knative.dev/pkg/observability knative.dev/pkg/observability/attributekey knative.dev/pkg/observability/configmap