diff --git a/http.go b/http.go index 32fd62362..363de8182 100644 --- a/http.go +++ b/http.go @@ -11,8 +11,6 @@ import ( "k8s.io/apiserver/pkg/server/dynamiccertificates" - oscrypto "github.com/openshift/library-go/pkg/crypto" - "github.com/openshift/oauth-proxy/util" ) @@ -72,14 +70,55 @@ func (s *Server) ServeHTTP() { log.Printf("HTTP: closing %s", listener.Addr()) } -func (s *Server) ServeHTTPS(ctx context.Context) { - addr := s.Opts.HttpsAddress +func (s *Server) buildTLSConfig() *tls.Config { + config := &tls.Config{ + MinVersion: tls.VersionTLS13, + NextProtos: []string{"http/1.1"}, + } + + if s.Opts.TLSMinVersion != "" { + if v, ok := tlsVersionMap[s.Opts.TLSMinVersion]; ok { + config.MinVersion = v + } + } - config := oscrypto.SecureTLSConfig(&tls.Config{}) - if config.NextProtos == nil { - config.NextProtos = []string{"http/1.1"} + if len(s.Opts.TLSCipherSuites) > 0 { + suites := make([]uint16, 0, len(s.Opts.TLSCipherSuites)) + for _, name := range s.Opts.TLSCipherSuites { + if id, ok := tlsCipherSuiteMap[name]; ok { + suites = append(suites, id) + } + } + if len(suites) > 0 { + config.CipherSuites = suites + } } + return config +} + +var tlsVersionMap = map[string]uint16{ + "VersionTLS10": tls.VersionTLS10, + "VersionTLS11": tls.VersionTLS11, + "VersionTLS12": tls.VersionTLS12, + "VersionTLS13": tls.VersionTLS13, +} + +var tlsCipherSuiteMap = func() map[string]uint16 { + m := make(map[string]uint16) + for _, cs := range tls.CipherSuites() { + m[cs.Name] = cs.ID + } + for _, cs := range tls.InsecureCipherSuites() { + m[cs.Name] = cs.ID + } + return m +}() + +func (s *Server) ServeHTTPS(ctx context.Context) { + addr := s.Opts.HttpsAddress + config := s.buildTLSConfig() + var err error servingCertProvider, err := dynamiccertificates.NewDynamicServingContentFromFiles("serving", s.Opts.TLSCertFile, s.Opts.TLSKeyFile) if err != nil { diff --git a/http_test.go b/http_test.go new file mode 100644 index 000000000..ffcbf43f9 --- /dev/null +++ b/http_test.go @@ -0,0 +1,85 @@ +package main + +import ( + "crypto/tls" + "testing" +) + +func TestBuildTLSConfig_Defaults(t *testing.T) { + s := &Server{Opts: &Options{}} + config := s.buildTLSConfig() + + if config.MinVersion != tls.VersionTLS13 { + t.Errorf("Expected default MinVersion TLS 1.3 (%d), got %d", tls.VersionTLS13, config.MinVersion) + } +} + +func TestBuildTLSConfig_MinVersion(t *testing.T) { + tests := []struct { + name string + minVersion string + expected uint16 + }{ + { + name: "VersionTLS12", + minVersion: "VersionTLS12", + expected: tls.VersionTLS12, + }, + { + name: "VersionTLS13", + minVersion: "VersionTLS13", + expected: tls.VersionTLS13, + }, + { + name: "unknown version keeps default", + minVersion: "VersionTLS99", + expected: tls.VersionTLS13, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &Server{Opts: &Options{TLSMinVersion: tt.minVersion}} + config := s.buildTLSConfig() + + if config.MinVersion != tt.expected { + t.Errorf("Expected MinVersion %d, got %d", tt.expected, config.MinVersion) + } + }) + } +} + +func TestBuildTLSConfig_CipherSuites(t *testing.T) { + tests := []struct { + name string + cipherSuites []string + expectCount int + }{ + { + name: "single valid cipher", + cipherSuites: []string{"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"}, + expectCount: 1, + }, + { + name: "multiple valid ciphers", + cipherSuites: []string{"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"}, + expectCount: 2, + }, + { + name: "unknown cipher is skipped", + cipherSuites: []string{"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", "TLS_INVALID_CIPHER"}, + expectCount: 1, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &Server{Opts: &Options{TLSCipherSuites: tt.cipherSuites}} + config := s.buildTLSConfig() + + if len(config.CipherSuites) != tt.expectCount { + t.Errorf("Expected %d cipher suites, got %d", tt.expectCount, len(config.CipherSuites)) + } + }) + } +} diff --git a/main.go b/main.go index 637874d70..1cd6d0aff 100644 --- a/main.go +++ b/main.go @@ -32,6 +32,7 @@ func main() { openshiftCAs := NewStringArray() clientCA := "" upstreamCAs := NewStringArray() + tlsCipherSuitesStr := "" config := flagSet.String("config", "", "path to config file") showVersion := flagSet.Bool("version", false, "print version string") @@ -42,6 +43,8 @@ func main() { flagSet.String("tls-cert", "", "path to certificate file") flagSet.String("tls-key", "", "path to private key file") flagSet.StringVar(&clientCA, "tls-client-ca", clientCA, "path to a CA file for admitting client certificates.") + flagSet.String("tls-min-version", "", "minimum TLS version (e.g., VersionTLS12, VersionTLS13). Defaults to TLS 1.3") + flagSet.StringVar(&tlsCipherSuitesStr, "tls-cipher-suites", "", "comma-separated list of TLS cipher suites") flagSet.String("redirect-url", "", "the OAuth Redirect URL. ie: \"https://internalapp.yourcompany.com/oauth/callback\"") flagSet.Bool("set-xauthrequest", false, "set X-Auth-Request-User and X-Auth-Request-Email response headers (useful in Nginx auth_request mode)") flagSet.Var(upstreams, "upstream", "the http url(s) of the upstream endpoint or file:// paths for static files. Routing is based on the path") @@ -131,6 +134,18 @@ func main() { cfg.LoadEnvForStruct(opts) options.Resolve(opts, flagSet, cfg) + // Parse comma-separated TLS cipher suites from CLI (overrides config file) + if tlsCipherSuitesStr != "" { + cipherSuites := strings.Split(tlsCipherSuitesStr, ",") + opts.TLSCipherSuites = make([]string, 0, len(cipherSuites)) + for _, cipher := range cipherSuites { + trimmed := strings.TrimSpace(cipher) + if trimmed != "" { + opts.TLSCipherSuites = append(opts.TLSCipherSuites, trimmed) + } + } + } + var p providers.Provider switch opts.Provider { case "openshift": diff --git a/options.go b/options.go index 33f3faf01..3fca3bd31 100644 --- a/options.go +++ b/options.go @@ -40,6 +40,8 @@ type Options struct { TLSCertFile string `flag:"tls-cert" cfg:"tls_cert_file"` TLSKeyFile string `flag:"tls-key" cfg:"tls_key_file"` TLSClientCAFile string `flag:"tls-client-ca" cfg:"tls_client_ca"` + TLSMinVersion string `flag:"tls-min-version" cfg:"tls_min_version"` + TLSCipherSuites []string `cfg:"tls_cipher_suites"` // No flag tag - manually parsed after Resolve() AuthenticatedEmailsFile string `flag:"authenticated-emails-file" cfg:"authenticated_emails_file"` EmailDomains []string `flag:"email-domain" cfg:"email_domains"`