Skip to content

Commit f62171b

Browse files
authored
feat: add shared tls package for reading TLS config from environment (#3324)
* 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> * fix: address review feedback on tls package Reduce the public API surface of the tls package by unexporting ParseVersion, ParseCipherSuites, and ParseCurvePreferences since they are implementation details of NewConfigFromEnv. Also validate that TLS max version is not smaller than min version in webhook.New(), document the Options TLS field precedence (programmatic > env vars > defaults), and broaden TestConfig_TLSConfig to exercise the full NewConfigFromEnv → TLSConfig path. Signed-off-by: Mikhail Fedosin <mfedosin@redhat.com> --------- Signed-off-by: Mikhail Fedosin <mfedosin@redhat.com>
1 parent b239e96 commit f62171b

File tree

5 files changed

+790
-7
lines changed

5 files changed

+790
-7
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)