Skip to content

Commit 554c259

Browse files
committed
feat: add shared tls package for reading TLS config from environment
Extract TLS configuration parsing into a reusable knative.dev/pkg/tls package so that any Knative component (not just webhooks) can read TLS_MIN_VERSION, TLS_MAX_VERSION, TLS_CIPHER_SUITES, and TLS_CURVE_PREFERENCES from environment variables with an optional prefix. The webhook package is updated to use the new tls package, extending env var support from just WEBHOOK_TLS_MIN_VERSION to all four WEBHOOK_TLS_* variables. Programmatic Options values continue to take precedence over environment variables. Signed-off-by: Mikhail Fedosin <mfedosin@redhat.com>
1 parent b239e96 commit 554c259

File tree

5 files changed

+736
-12
lines changed

5 files changed

+736
-12
lines changed

tls/config.go

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
/*
2+
Copyright 2026 The Knative Authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package tls
18+
19+
import (
20+
cryptotls "crypto/tls"
21+
"fmt"
22+
"os"
23+
"strings"
24+
)
25+
26+
// Environment variable name suffixes for TLS configuration.
27+
// Use with a prefix to namespace them, e.g. "WEBHOOK_" + MinVersionEnvKey
28+
// reads the WEBHOOK_TLS_MIN_VERSION variable.
29+
const (
30+
MinVersionEnvKey = "TLS_MIN_VERSION"
31+
MaxVersionEnvKey = "TLS_MAX_VERSION"
32+
CipherSuitesEnvKey = "TLS_CIPHER_SUITES"
33+
CurvePreferencesEnvKey = "TLS_CURVE_PREFERENCES"
34+
)
35+
36+
// Config holds parsed TLS configuration values that can be used
37+
// to build a *crypto/tls.Config.
38+
type Config struct {
39+
MinVersion uint16
40+
MaxVersion uint16
41+
CipherSuites []uint16
42+
CurvePreferences []cryptotls.CurveID
43+
}
44+
45+
// NewConfigFromEnv reads TLS configuration from environment variables and
46+
// returns a Config. The prefix is prepended to each standard env-var suffix;
47+
// for example with prefix "WEBHOOK_" the function reads
48+
// WEBHOOK_TLS_MIN_VERSION, WEBHOOK_TLS_MAX_VERSION, etc.
49+
// Fields whose corresponding env var is unset are left at their zero value.
50+
func NewConfigFromEnv(prefix string) (*Config, error) {
51+
var cfg Config
52+
53+
if v := os.Getenv(prefix + MinVersionEnvKey); v != "" {
54+
ver, err := ParseVersion(v)
55+
if err != nil {
56+
return nil, fmt.Errorf("invalid %s%s %q: %w", prefix, MinVersionEnvKey, v, err)
57+
}
58+
cfg.MinVersion = ver
59+
}
60+
61+
if v := os.Getenv(prefix + MaxVersionEnvKey); v != "" {
62+
ver, err := ParseVersion(v)
63+
if err != nil {
64+
return nil, fmt.Errorf("invalid %s%s %q: %w", prefix, MaxVersionEnvKey, v, err)
65+
}
66+
cfg.MaxVersion = ver
67+
}
68+
69+
if v := os.Getenv(prefix + CipherSuitesEnvKey); v != "" {
70+
suites, err := ParseCipherSuites(v)
71+
if err != nil {
72+
return nil, fmt.Errorf("invalid %s%s: %w", prefix, CipherSuitesEnvKey, err)
73+
}
74+
cfg.CipherSuites = suites
75+
}
76+
77+
if v := os.Getenv(prefix + CurvePreferencesEnvKey); v != "" {
78+
curves, err := ParseCurvePreferences(v)
79+
if err != nil {
80+
return nil, fmt.Errorf("invalid %s%s: %w", prefix, CurvePreferencesEnvKey, err)
81+
}
82+
cfg.CurvePreferences = curves
83+
}
84+
85+
return &cfg, nil
86+
}
87+
88+
// TLSConfig constructs a *crypto/tls.Config from the parsed configuration.
89+
// The caller typically adds additional fields such as GetCertificate.
90+
func (c *Config) TLSConfig() *cryptotls.Config {
91+
//nolint:gosec // Min version is caller-configurable; default is TLS 1.3.
92+
return &cryptotls.Config{
93+
MinVersion: c.MinVersion,
94+
MaxVersion: c.MaxVersion,
95+
CipherSuites: c.CipherSuites,
96+
CurvePreferences: c.CurvePreferences,
97+
}
98+
}
99+
100+
// ParseVersion converts a TLS version string to the corresponding
101+
// crypto/tls constant. Accepted values are "1.2" and "1.3".
102+
func ParseVersion(v string) (uint16, error) {
103+
switch v {
104+
case "1.2":
105+
return cryptotls.VersionTLS12, nil
106+
case "1.3":
107+
return cryptotls.VersionTLS13, nil
108+
default:
109+
return 0, fmt.Errorf("unsupported TLS version %q: must be %q or %q", v, "1.2", "1.3")
110+
}
111+
}
112+
113+
// ParseCipherSuites parses a comma-separated list of TLS cipher-suite names
114+
// (e.g. "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384")
115+
// into a slice of cipher-suite IDs. Names must match those returned by
116+
// crypto/tls.CipherSuiteName.
117+
func ParseCipherSuites(s string) ([]uint16, error) {
118+
lookup := cipherSuiteLookup()
119+
parts := strings.Split(s, ",")
120+
suites := make([]uint16, 0, len(parts))
121+
122+
for _, name := range parts {
123+
name = strings.TrimSpace(name)
124+
if name == "" {
125+
continue
126+
}
127+
id, ok := lookup[name]
128+
if !ok {
129+
return nil, fmt.Errorf("unknown cipher suite %q", name)
130+
}
131+
suites = append(suites, id)
132+
}
133+
134+
return suites, nil
135+
}
136+
137+
// ParseCurvePreferences parses a comma-separated list of elliptic-curve names
138+
// (e.g. "X25519,CurveP256") into a slice of crypto/tls.CurveID values.
139+
// Both Go constant names (CurveP256) and standard names (P-256) are accepted.
140+
func ParseCurvePreferences(s string) ([]cryptotls.CurveID, error) {
141+
parts := strings.Split(s, ",")
142+
curves := make([]cryptotls.CurveID, 0, len(parts))
143+
144+
for _, name := range parts {
145+
name = strings.TrimSpace(name)
146+
if name == "" {
147+
continue
148+
}
149+
id, ok := curvesByName[name]
150+
if !ok {
151+
return nil, fmt.Errorf("unknown curve %q", name)
152+
}
153+
curves = append(curves, id)
154+
}
155+
156+
return curves, nil
157+
}
158+
159+
func cipherSuiteLookup() map[string]uint16 {
160+
m := make(map[string]uint16)
161+
for _, cs := range cryptotls.CipherSuites() {
162+
m[cs.Name] = cs.ID
163+
}
164+
return m
165+
}
166+
167+
var curvesByName = map[string]cryptotls.CurveID{
168+
"CurveP256": cryptotls.CurveP256,
169+
"CurveP384": cryptotls.CurveP384,
170+
"CurveP521": cryptotls.CurveP521,
171+
"X25519": cryptotls.X25519,
172+
"X25519MLKEM768": cryptotls.X25519MLKEM768,
173+
"P-256": cryptotls.CurveP256,
174+
"P-384": cryptotls.CurveP384,
175+
"P-521": cryptotls.CurveP521,
176+
}

0 commit comments

Comments
 (0)