Skip to content

Commit 66f9894

Browse files
andresllhclaude
andauthored
feat(epp): add --tls-min-version and --tls-cipher-suites flags (#2230)
Add CLI flags to configure TLS minimum version and cipher suites on the ext_proc gRPC server. Values are parsed from Go crypto/tls constant names (e.g., VersionTLS12, TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256) and applied to both cert-reload and static-cert TLS config paths. Signed-off-by: Andres Llausas <allausas@redhat.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent eaacbea commit 66f9894

5 files changed

Lines changed: 315 additions & 17 deletions

File tree

cmd/epp/runner/runner.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,6 +461,8 @@ func (r *Runner) setup(ctx context.Context, cfg *rest.Config, opts *runserver.Op
461461
HealthChecking: opts.HealthChecking,
462462
CertPath: opts.CertPath,
463463
EnableCertReload: opts.EnableCertReload,
464+
TLSMinVersion: opts.TLSMinVersionValue(),
465+
TLSCipherSuites: opts.TLSCipherSuiteValues(),
464466
RefreshPrometheusMetricsInterval: opts.RefreshPrometheusMetricsInterval,
465467
MetricsStalenessThreshold: opts.MetricsStalenessThreshold,
466468
Director: director,
@@ -983,6 +985,8 @@ func (r *Runner) runWithFileDiscovery(ctx context.Context, opts *runserver.Optio
983985
HealthChecking: opts.HealthChecking,
984986
CertPath: opts.CertPath,
985987
EnableCertReload: opts.EnableCertReload,
988+
TLSMinVersion: opts.TLSMinVersionValue(),
989+
TLSCipherSuites: opts.TLSCipherSuiteValues(),
986990
RefreshPrometheusMetricsInterval: opts.RefreshPrometheusMetricsInterval,
987991
MetricsStalenessThreshold: opts.MetricsStalenessThreshold,
988992
Director: director,

pkg/epp/server/options.go

Lines changed: 83 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -97,19 +97,23 @@ type Options struct {
9797
//
9898
// Diagnostics.
9999
//
100-
logging.LoggingOptions // Logging configuration.
101-
Tracing bool // Enables emitting traces.
102-
HealthChecking bool // Enables health checking.
103-
MetricsPort int // The metrics port exposed by EPP. (TODO: uint16)
104-
GRPCHealthPort int // The port used for gRPC liveness and readiness probes. (TODO: uint16)
105-
EnablePprof bool // Enables pprof handlers.
106-
CertPath string // The path to the certificate for secure serving.
107-
EnableCertReload bool // Enables certificate reloading of the certificates specified in --cert-path.
108-
SecureServing bool // Enables secure serving.
109-
MetricsEndpointAuth bool // Enables authentication and authorization of the metrics endpoint.
110-
MetricsClientCAFile string // PEM CA that requires a verified client cert on the metrics endpoint.
111-
MetricsCertDir string // Directory with the metrics server certificates that enables metrics TLS.
112-
EnableGRPCStreamMetrics bool // Enables ext_proc gRPC stream metrics (in-flight gauge, hold duration, completions counter by code).
100+
logging.LoggingOptions // Logging configuration.
101+
Tracing bool // Enables emitting traces.
102+
HealthChecking bool // Enables health checking.
103+
MetricsPort int // The metrics port exposed by EPP. (TODO: uint16)
104+
GRPCHealthPort int // The port used for gRPC liveness and readiness probes. (TODO: uint16)
105+
EnablePprof bool // Enables pprof handlers.
106+
CertPath string // The path to the certificate for secure serving.
107+
EnableCertReload bool // Enables certificate reloading of the certificates specified in --cert-path.
108+
SecureServing bool // Enables secure serving.
109+
TLSMinVersion string // Minimum TLS version for secure serving (e.g., VersionTLS12, VersionTLS13).
110+
TLSCipherSuites []string // TLS cipher suites (Go crypto/tls names). Only effective for TLS 1.2 and below.
111+
tlsMinVersionValue uint16 // Parsed TLS min version value.
112+
tlsCipherSuiteValues []uint16 // Parsed TLS cipher suite values.
113+
MetricsEndpointAuth bool // Enables authentication and authorization of the metrics endpoint.
114+
MetricsClientCAFile string // PEM CA that requires a verified client cert on the metrics endpoint.
115+
MetricsCertDir string // Directory with the metrics server certificates that enables metrics TLS.
116+
EnableGRPCStreamMetrics bool // Enables ext_proc gRPC stream metrics (in-flight gauge, hold duration, completions counter by code).
113117
//
114118
// Configuration.
115119
//
@@ -213,6 +217,10 @@ func (opts *Options) AddFlags(fs *pflag.FlagSet) {
213217
fs.BoolVar(&opts.EnableGRPCStreamMetrics, "enable-grpc-stream-metrics", opts.EnableGRPCStreamMetrics,
214218
"Enables ext_proc gRPC stream metrics (in-flight gauge, hold-duration histogram, completions counter by code).")
215219
fs.BoolVar(&opts.SecureServing, "secure-serving", opts.SecureServing, "Enables secure serving.")
220+
fs.StringVar(&opts.TLSMinVersion, "tls-min-version", opts.TLSMinVersion,
221+
"Minimum TLS version for secure serving (e.g., VersionTLS12, VersionTLS13).")
222+
fs.StringSliceVar(&opts.TLSCipherSuites, "tls-cipher-suites", opts.TLSCipherSuites,
223+
"Comma-separated list of TLS cipher suites for secure serving (Go crypto/tls names, e.g., TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256). Only effective for TLS 1.2 and below; TLS 1.3 cipher suites are not configurable.")
216224
fs.BoolVar(&opts.MetricsEndpointAuth, "metrics-endpoint-auth", opts.MetricsEndpointAuth,
217225
"Enables authentication and authorization of the metrics endpoint.")
218226
fs.StringVar(&opts.MetricsClientCAFile, "metrics-client-ca-file", opts.MetricsClientCAFile,
@@ -285,6 +293,21 @@ func (opts *Options) Complete() error {
285293
}
286294
}
287295

296+
if opts.TLSMinVersion != "" {
297+
v, err := parseTLSVersion(opts.TLSMinVersion)
298+
if err != nil {
299+
return fmt.Errorf("invalid tls-min-version %q: %w", opts.TLSMinVersion, err)
300+
}
301+
opts.tlsMinVersionValue = v
302+
}
303+
if len(opts.TLSCipherSuites) > 0 {
304+
suites, err := parseCipherSuites(opts.TLSCipherSuites)
305+
if err != nil {
306+
return fmt.Errorf("invalid tls-cipher-suites: %w", err)
307+
}
308+
opts.tlsCipherSuiteValues = suites
309+
}
310+
288311
// Complete logging options.
289312
return opts.LoggingOptions.Complete()
290313
}
@@ -391,3 +414,50 @@ func removeDuplicatePorts(ports []int) []int {
391414
}
392415
return unique
393416
}
417+
418+
// TLSMinVersionValue returns the parsed uint16 TLS min version.
419+
func (opts *Options) TLSMinVersionValue() uint16 {
420+
return opts.tlsMinVersionValue
421+
}
422+
423+
// TLSCipherSuiteValues returns the parsed uint16 TLS cipher suite IDs.
424+
func (opts *Options) TLSCipherSuiteValues() []uint16 {
425+
return opts.tlsCipherSuiteValues
426+
}
427+
428+
var tlsVersions = map[string]uint16{
429+
"VersionTLS10": tls.VersionTLS10,
430+
"VersionTLS11": tls.VersionTLS11,
431+
"VersionTLS12": tls.VersionTLS12,
432+
"VersionTLS13": tls.VersionTLS13,
433+
}
434+
435+
func parseTLSVersion(s string) (uint16, error) {
436+
if v, ok := tlsVersions[s]; ok {
437+
return v, nil
438+
}
439+
return 0, fmt.Errorf("unknown TLS version %q; supported values: VersionTLS10, VersionTLS11, VersionTLS12, VersionTLS13", s)
440+
}
441+
442+
func parseCipherSuites(names []string) ([]uint16, error) {
443+
byName := make(map[string]uint16)
444+
for _, cs := range tls.CipherSuites() {
445+
byName[cs.Name] = cs.ID
446+
}
447+
for _, cs := range tls.InsecureCipherSuites() {
448+
byName[cs.Name] = cs.ID
449+
}
450+
ids := make([]uint16, 0, len(names))
451+
for _, name := range names {
452+
name = strings.TrimSpace(name)
453+
if name == "" {
454+
continue
455+
}
456+
id, ok := byName[name]
457+
if !ok {
458+
return nil, fmt.Errorf("unknown cipher suite %q", name)
459+
}
460+
ids = append(ids, id)
461+
}
462+
return ids, nil
463+
}

pkg/epp/server/options_test.go

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ limitations under the License.
1717
package server
1818

1919
import (
20+
"crypto/tls"
2021
"os"
2122
"path/filepath"
2223
"strings"
@@ -364,3 +365,129 @@ func TestCompleteMetricsCertFiles(t *testing.T) {
364365
present.MetricsCertDir = dir
365366
require.NoError(t, present.Complete())
366367
}
368+
369+
func TestTLSMinVersionFlag(t *testing.T) {
370+
tests := []struct {
371+
name string
372+
args []string
373+
wantVersion uint16
374+
wantErr bool
375+
}{
376+
{
377+
name: "VersionTLS12",
378+
args: []string{"--tls-min-version", "VersionTLS12"},
379+
wantVersion: tls.VersionTLS12,
380+
},
381+
{
382+
name: "VersionTLS13",
383+
args: []string{"--tls-min-version", "VersionTLS13"},
384+
wantVersion: tls.VersionTLS13,
385+
},
386+
{
387+
name: "not set",
388+
args: []string{},
389+
wantVersion: 0,
390+
},
391+
{
392+
name: "invalid version",
393+
args: []string{"--tls-min-version", "TLS1.2"},
394+
wantErr: true,
395+
},
396+
}
397+
for _, tt := range tests {
398+
t.Run(tt.name, func(t *testing.T) {
399+
fs := pflag.NewFlagSet(tt.name, pflag.ContinueOnError)
400+
opts := NewOptions()
401+
opts.AddFlags(fs)
402+
403+
argv := append([]string{"--pool-name", testPoolName, "--config-file", testConfigFile}, tt.args...)
404+
require.NoError(t, fs.Parse(argv))
405+
406+
err := opts.Complete()
407+
if tt.wantErr {
408+
require.Error(t, err)
409+
return
410+
}
411+
require.NoError(t, err)
412+
require.Equal(t, tt.wantVersion, opts.TLSMinVersionValue())
413+
})
414+
}
415+
}
416+
417+
func TestTLSCipherSuitesFlag(t *testing.T) {
418+
tests := []struct {
419+
name string
420+
args []string
421+
wantSuites []uint16
422+
wantErr bool
423+
}{
424+
{
425+
name: "single cipher",
426+
args: []string{"--tls-cipher-suites", "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"},
427+
wantSuites: []uint16{
428+
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
429+
},
430+
},
431+
{
432+
name: "multiple ciphers",
433+
args: []string{"--tls-cipher-suites", "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"},
434+
wantSuites: []uint16{
435+
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
436+
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
437+
},
438+
},
439+
{
440+
name: "not set",
441+
args: []string{},
442+
wantSuites: nil,
443+
},
444+
{
445+
name: "unknown cipher",
446+
args: []string{"--tls-cipher-suites", "FAKE_CIPHER_SUITE"},
447+
wantErr: true,
448+
},
449+
}
450+
for _, tt := range tests {
451+
t.Run(tt.name, func(t *testing.T) {
452+
fs := pflag.NewFlagSet(tt.name, pflag.ContinueOnError)
453+
opts := NewOptions()
454+
opts.AddFlags(fs)
455+
456+
argv := append([]string{"--pool-name", testPoolName, "--config-file", testConfigFile}, tt.args...)
457+
require.NoError(t, fs.Parse(argv))
458+
459+
err := opts.Complete()
460+
if tt.wantErr {
461+
require.Error(t, err)
462+
return
463+
}
464+
require.NoError(t, err)
465+
require.Equal(t, tt.wantSuites, opts.TLSCipherSuiteValues())
466+
})
467+
}
468+
}
469+
470+
func TestParseTLSVersion(t *testing.T) {
471+
for name, want := range tlsVersions {
472+
got, err := parseTLSVersion(name)
473+
require.NoError(t, err, name)
474+
require.Equal(t, want, got, name)
475+
}
476+
_, err := parseTLSVersion("invalid")
477+
require.Error(t, err)
478+
}
479+
480+
func TestParseCipherSuites(t *testing.T) {
481+
ids, err := parseCipherSuites([]string{
482+
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
483+
"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
484+
})
485+
require.NoError(t, err)
486+
require.Equal(t, []uint16{
487+
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
488+
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
489+
}, ids)
490+
491+
_, err = parseCipherSuites([]string{"BOGUS"})
492+
require.Error(t, err)
493+
}

pkg/epp/server/runserver.go

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ type ExtProcServerRunner struct {
6363
HealthChecking bool
6464
CertPath string
6565
EnableCertReload bool
66+
TLSMinVersion uint16
67+
TLSCipherSuites []uint16
6668
RefreshPrometheusMetricsInterval time.Duration
6769
MetricsStalenessThreshold time.Duration
6870
Director *requestcontrol.Director
@@ -183,17 +185,21 @@ func (r *ExtProcServerRunner) AsRunnable(logger logr.Logger) manager.Runnable {
183185
if err != nil {
184186
return fmt.Errorf("failed to create cert reloader: %w", err)
185187
}
186-
creds = credentials.NewTLS(&tls.Config{
188+
tlsCfg := &tls.Config{
187189
GetCertificate: func(_ *tls.ClientHelloInfo) (*tls.Certificate, error) {
188190
return reloader.Get(), nil
189191
},
190192
NextProtos: []string{"h2"},
191-
})
193+
}
194+
r.applyTLSOverrides(tlsCfg)
195+
creds = credentials.NewTLS(tlsCfg)
192196
} else {
193-
creds = credentials.NewTLS(&tls.Config{
197+
tlsCfg := &tls.Config{
194198
Certificates: []tls.Certificate{cert},
195199
NextProtos: []string{"h2"},
196-
})
200+
}
201+
r.applyTLSOverrides(tlsCfg)
202+
creds = credentials.NewTLS(tlsCfg)
197203
}
198204
}
199205

@@ -239,3 +245,13 @@ func (r *ExtProcServerRunner) AsRunnable(logger logr.Logger) manager.Runnable {
239245
return runnable.GRPCServer("ext-proc", srv, r.GrpcPort).Start(ctx)
240246
}))
241247
}
248+
249+
// applyTLSOverrides sets MinVersion and CipherSuites on cfg when configured.
250+
func (r *ExtProcServerRunner) applyTLSOverrides(cfg *tls.Config) {
251+
if r.TLSMinVersion != 0 {
252+
cfg.MinVersion = r.TLSMinVersion
253+
}
254+
if len(r.TLSCipherSuites) > 0 {
255+
cfg.CipherSuites = r.TLSCipherSuites
256+
}
257+
}

0 commit comments

Comments
 (0)