From 1bc956939999bcf6b1b2454259c763c3efff5f38 Mon Sep 17 00:00:00 2001 From: "Gina A." <70909035+gndz07@users.noreply.github.com> Date: Thu, 29 Jan 2026 11:10:04 +0100 Subject: [PATCH] Support auth-tls-secret and auth-tls-verify-client annotations --- .../kubernetes/ingress-nginx.md | 26 +- .../kubernetes/ingress-nginx/annotations.go | 3 + .../20-ingress-with-auth-tls-secret.yml | 26 ++ ...21-ingress-with-auth-tls-verify-client.yml | 27 ++ .../ingress-nginx/fixtures/secrets.yml | 10 + .../kubernetes/ingress-nginx/kubernetes.go | 87 +++++ .../ingress-nginx/kubernetes_test.go | 318 ++++++++++++++++-- pkg/tls/tls.go | 24 ++ pkg/tls/tlsmanager.go | 10 +- 9 files changed, 486 insertions(+), 45 deletions(-) create mode 100644 pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/20-ingress-with-auth-tls-secret.yml create mode 100644 pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/21-ingress-with-auth-tls-verify-client.yml diff --git a/docs/content/reference/routing-configuration/kubernetes/ingress-nginx.md b/docs/content/reference/routing-configuration/kubernetes/ingress-nginx.md index d86fd9a32..07ba27e38 100644 --- a/docs/content/reference/routing-configuration/kubernetes/ingress-nginx.md +++ b/docs/content/reference/routing-configuration/kubernetes/ingress-nginx.md @@ -267,15 +267,17 @@ The following annotations are organized by category for easier navigation. ### SSL/TLS -| Annotation | Limitations / Notes | -|-------------------------------------------------------|--------------------------------------------------------------------------------------------| -| `nginx.ingress.kubernetes.io/ssl-redirect` | Cannot opt-out per route if enabled globally. | -| `nginx.ingress.kubernetes.io/force-ssl-redirect` | Cannot opt-out per route if enabled globally. | -| `nginx.ingress.kubernetes.io/ssl-passthrough` | Some differences in SNI/default backend handling. | -| `nginx.ingress.kubernetes.io/proxy-ssl-server-name` | | -| `nginx.ingress.kubernetes.io/proxy-ssl-name` | | -| `nginx.ingress.kubernetes.io/proxy-ssl-verify` | | -| `nginx.ingress.kubernetes.io/proxy-ssl-secret` | | +| Annotation | Limitations / Notes | +|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------| +| `nginx.ingress.kubernetes.io/ssl-redirect` | Cannot opt-out per route if enabled globally. | +| `nginx.ingress.kubernetes.io/force-ssl-redirect` | Cannot opt-out per route if enabled globally. | +| `nginx.ingress.kubernetes.io/ssl-passthrough` | Some differences in SNI/default backend handling. | +| `nginx.ingress.kubernetes.io/proxy-ssl-server-name` | | +| `nginx.ingress.kubernetes.io/proxy-ssl-name` | | +| `nginx.ingress.kubernetes.io/proxy-ssl-verify` | | +| `nginx.ingress.kubernetes.io/proxy-ssl-secret` | | +| `nginx.ingress.kubernetes.io/auth-tls-secret` | When validation fails, the rejection happens during the TLS handshake rather than returning a 400 Bad Request. | +| `nginx.ingress.kubernetes.io/auth-tls-verify-client` | When validation fails, the rejection happens during the TLS handshake rather than returning a 400 Bad Request. | ### Session Affinity @@ -363,10 +365,8 @@ The following annotations are organized by category for easier navigation. | Annotation | Notes | |-----------------------------------------------------------------------------|------------------------------------------------------| -| `nginx.ingress.kubernetes.io/affinity-canary-behavior` | | -| `nginx.ingress.kubernetes.io/auth-tls-secret` | | -| `nginx.ingress.kubernetes.io/auth-tls-verify-depth` | | -| `nginx.ingress.kubernetes.io/auth-tls-verify-client` | | +| `nginx.ingress.kubernetes.io/affinity-canary-behavior` | | | +| `nginx.ingress.kubernetes.io/auth-tls-verify-depth` | | | `nginx.ingress.kubernetes.io/auth-tls-error-page` | | | `nginx.ingress.kubernetes.io/auth-tls-pass-certificate-to-upstream` | | | `nginx.ingress.kubernetes.io/auth-tls-match-cn` | | diff --git a/pkg/provider/kubernetes/ingress-nginx/annotations.go b/pkg/provider/kubernetes/ingress-nginx/annotations.go index 953cda9ed..4425db6c4 100644 --- a/pkg/provider/kubernetes/ingress-nginx/annotations.go +++ b/pkg/provider/kubernetes/ingress-nginx/annotations.go @@ -19,6 +19,9 @@ type ingressConfig struct { AuthSignin *string `annotation:"nginx.ingress.kubernetes.io/auth-signin"` AuthResponseHeaders *string `annotation:"nginx.ingress.kubernetes.io/auth-response-headers"` + AuthTLSSecret *string `annotation:"nginx.ingress.kubernetes.io/auth-tls-secret"` + AuthTLSVerifyClient *string `annotation:"nginx.ingress.kubernetes.io/auth-tls-verify-client"` + ForceSSLRedirect *bool `annotation:"nginx.ingress.kubernetes.io/force-ssl-redirect"` SSLRedirect *bool `annotation:"nginx.ingress.kubernetes.io/ssl-redirect"` diff --git a/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/20-ingress-with-auth-tls-secret.yml b/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/20-ingress-with-auth-tls-secret.yml new file mode 100644 index 000000000..2d9e5c7e2 --- /dev/null +++ b/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/20-ingress-with-auth-tls-secret.yml @@ -0,0 +1,26 @@ +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: ingress-with-auth-tls-secret + namespace: default + annotations: + nginx.ingress.kubernetes.io/auth-tls-secret: "default/ca-secret" + +spec: + ingressClassName: nginx + tls: + - hosts: + - auth-tls-secret.localhost + - secretName: whoami-tls + rules: + - host: auth-tls-secret.localhost + http: + paths: + - path: / + pathType: Exact + backend: + service: + name: whoami + port: + number: 80 diff --git a/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/21-ingress-with-auth-tls-verify-client.yml b/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/21-ingress-with-auth-tls-verify-client.yml new file mode 100644 index 000000000..a327d59d4 --- /dev/null +++ b/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/21-ingress-with-auth-tls-verify-client.yml @@ -0,0 +1,27 @@ +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: ingress-with-auth-tls-verify-client + namespace: default + annotations: + nginx.ingress.kubernetes.io/auth-tls-secret: "default/ca-secret" + nginx.ingress.kubernetes.io/auth-tls-verify-client: "optional" + +spec: + ingressClassName: nginx + tls: + - hosts: + - auth-tls-verify-client.localhost + - secretName: whoami-tls + rules: + - host: auth-tls-verify-client.localhost + http: + paths: + - path: / + pathType: Exact + backend: + service: + name: whoami + port: + number: 80 diff --git a/pkg/provider/kubernetes/ingress-nginx/fixtures/secrets.yml b/pkg/provider/kubernetes/ingress-nginx/fixtures/secrets.yml index 7c53f7fb0..32a475059 100644 --- a/pkg/provider/kubernetes/ingress-nginx/fixtures/secrets.yml +++ b/pkg/provider/kubernetes/ingress-nginx/fixtures/secrets.yml @@ -7,3 +7,13 @@ metadata: data: tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0t tls.key: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0t + +--- +kind: Secret +apiVersion: v1 +metadata: + namespace: default + name: ca-secret + +data: + ca.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0t \ No newline at end of file diff --git a/pkg/provider/kubernetes/ingress-nginx/kubernetes.go b/pkg/provider/kubernetes/ingress-nginx/kubernetes.go index f27bffa1f..e2e7cebe2 100644 --- a/pkg/provider/kubernetes/ingress-nginx/kubernetes.go +++ b/pkg/provider/kubernetes/ingress-nginx/kubernetes.go @@ -267,6 +267,7 @@ func (p *Provider) loadConfiguration(ctx context.Context) *dynamic.Configuration ingresses := p.k8sClient.ListIngresses() uniqCerts := make(map[string]*tls.CertAndStores) + tlsOptions := make(map[string]tls.Options) for _, ingress := range ingresses { logger := log.Ctx(ctx).With().Str("ingress", ingress.Name).Str("namespace", ingress.Namespace).Logger() ctxIngress := logger.WithContext(ctx) @@ -294,6 +295,23 @@ func (p *Provider) loadConfiguration(ctx context.Context) *dynamic.Configuration } } + var clientAuthTLSOptionName string + if ingressConfig.AuthTLSSecret != nil { + tlsOptName := provider.Normalize(ingress.Namespace + "-" + ingress.Name + "-" + *ingressConfig.AuthTLSSecret) + + if _, exists := tlsOptions[tlsOptName]; !exists { + tlsOpt, err := p.buildClientAuthTLSOption(ingress.Namespace, ingressConfig) + if err != nil { + logger.Error().Err(err).Msg("Error configuring client auth TLS") + continue + } + + tlsOptions[tlsOptName] = tlsOpt + } + + clientAuthTLSOptionName = tlsOptName + } + namedServersTransport, err := p.buildServersTransport(ingress.Namespace, ingress.Name, ingressConfig) if err != nil { logger.Error().Err(err).Msg("Ignoring Ingress cannot create proxy SSL configuration") @@ -336,6 +354,9 @@ func (p *Provider) loadConfiguration(ctx context.Context) *dynamic.Configuration Service: defaultBackendName, TLS: &dynamic.RouterTLSConfig{}, } + if clientAuthTLSOptionName != "" { + rtTLS.TLS.Options = clientAuthTLSOptionName + } if err := p.applyMiddlewares(ingress.Namespace, defaultBackendTLSName, "", ingressConfig, false, rtTLS, conf); err != nil { logger.Error().Err(err).Msg("Error applying middlewares") @@ -427,6 +448,9 @@ func (p *Provider) loadConfiguration(ctx context.Context) *dynamic.Configuration Service: key, TLS: &dynamic.RouterTLSConfig{}, } + if clientAuthTLSOptionName != "" { + rtTLS.TLS.Options = clientAuthTLSOptionName + } if err := p.applyMiddlewares(ingress.Namespace, key+"-tls", "", ingressConfig, false, rtTLS, conf); err != nil { logger.Error().Err(err).Msg("Error applying middlewares") @@ -481,6 +505,10 @@ func (p *Provider) loadConfiguration(ctx context.Context) *dynamic.Configuration } if hasTLS { rt.TLS = &dynamic.RouterTLSConfig{} + + if clientAuthTLSOptionName != "" { + rt.TLS.Options = clientAuthTLSOptionName + } } routerKey := provider.Normalize(fmt.Sprintf("%s-%s-rule-%d-path-%d", ingress.Namespace, ingress.Name, ri, pi)) @@ -502,6 +530,7 @@ func (p *Provider) loadConfiguration(ctx context.Context) *dynamic.Configuration conf.TLS = &dynamic.TLSConfiguration{ Certificates: slices.Collect(maps.Values(uniqCerts)), + Options: tlsOptions, } return conf @@ -1298,3 +1327,61 @@ func throttleEvents(ctx context.Context, throttleDuration time.Duration, pool *s return eventsChanBuffered } + +func (p *Provider) buildClientAuthTLSOption(ingressNamespace string, config ingressConfig) (tls.Options, error) { + secretParts := strings.SplitN(*config.AuthTLSSecret, "/", 2) + if len(secretParts) != 2 { + return tls.Options{}, errors.New("auth-tls-secret is not in a correct namespace/name format") + } + + // Expected format: namespace/name. + secretNamespace := secretParts[0] + secretName := secretParts[1] + + if secretNamespace == "" { + return tls.Options{}, errors.New("auth-tls-secret has empty namespace") + } + if secretName == "" { + return tls.Options{}, errors.New("auth-tls-secret has empty name") + } + // Cross-namespace secrets are not supported. + if secretNamespace != ingressNamespace { + return tls.Options{}, fmt.Errorf("cross-namespace auth-tls-secret is not supported: secret namespace %q does not match ingress namespace %q", secretNamespace, ingressNamespace) + } + + blocks, err := p.certificateBlocks(secretNamespace, secretName) + if err != nil { + return tls.Options{}, fmt.Errorf("reading client certificate: %w", err) + } + + if blocks.CA == nil { + return tls.Options{}, errors.New("secret does not contain a CA certificate") + } + + // Default verifyClient value is "on" on ingress-nginx. + // on means that client certificate is required and must be signed by a trusted CA certificate. + clientAuthType := tls.RequireAndVerifyClientCert + if config.AuthTLSVerifyClient != nil { + switch *config.AuthTLSVerifyClient { + // off means that client certificate is not requested and no verification will be passed. + case "off": + clientAuthType = tls.NoClientCert + // optional means that the client certificate is requested, but not required. + // If the certificate is present, it needs to be verified. + case "optional": + clientAuthType = tls.VerifyClientCertIfGiven + // optional_no_ca means that the client certificate is requested, but does not require it to be signed by a trusted CA certificate. + case "optional_no_ca": + clientAuthType = tls.RequestClientCert + } + } + + tlsOpt := tls.Options{} + tlsOpt.SetDefaults() + tlsOpt.ClientAuth = tls.ClientAuth{ + CAFiles: []types.FileOrContent{*blocks.CA}, + ClientAuthType: clientAuthType, + } + + return tlsOpt, nil +} diff --git a/pkg/provider/kubernetes/ingress-nginx/kubernetes_test.go b/pkg/provider/kubernetes/ingress-nginx/kubernetes_test.go index 3f76c203b..2de7b5722 100644 --- a/pkg/provider/kubernetes/ingress-nginx/kubernetes_test.go +++ b/pkg/provider/kubernetes/ingress-nginx/kubernetes_test.go @@ -8,6 +8,7 @@ import ( "testing" "time" + "github.com/go-acme/lego/v4/challenge/tlsalpn01" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ptypes "github.com/traefik/paerser/types" @@ -46,7 +47,9 @@ func TestLoadIngresses(t *testing.T) { Services: map[string]*dynamic.Service{}, ServersTransports: map[string]*dynamic.ServersTransport{}, }, - TLS: &dynamic.TLSConfiguration{}, + TLS: &dynamic.TLSConfiguration{ + Options: map[string]tls.Options{}, + }, }, }, { @@ -105,7 +108,9 @@ func TestLoadIngresses(t *testing.T) { }, }, }, - TLS: &dynamic.TLSConfiguration{}, + TLS: &dynamic.TLSConfiguration{ + Options: map[string]tls.Options{}, + }, }, }, { @@ -182,6 +187,7 @@ func TestLoadIngresses(t *testing.T) { }, }, }, + Options: map[string]tls.Options{}, }, }, }, @@ -244,7 +250,9 @@ func TestLoadIngresses(t *testing.T) { }, }, }, - TLS: &dynamic.TLSConfiguration{}, + TLS: &dynamic.TLSConfiguration{ + Options: map[string]tls.Options{}, + }, }, }, { @@ -305,7 +313,9 @@ func TestLoadIngresses(t *testing.T) { }, }, }, - TLS: &dynamic.TLSConfiguration{}, + TLS: &dynamic.TLSConfiguration{ + Options: map[string]tls.Options{}, + }, }, }, { @@ -452,6 +462,7 @@ func TestLoadIngresses(t *testing.T) { }, }, }, + Options: map[string]tls.Options{}, }, }, }, @@ -496,7 +507,9 @@ func TestLoadIngresses(t *testing.T) { Services: map[string]*dynamic.Service{}, ServersTransports: map[string]*dynamic.ServersTransport{}, }, - TLS: &dynamic.TLSConfiguration{}, + TLS: &dynamic.TLSConfiguration{ + Options: map[string]tls.Options{}, + }, }, }, { @@ -561,7 +574,9 @@ func TestLoadIngresses(t *testing.T) { }, }, }, - TLS: &dynamic.TLSConfiguration{}, + TLS: &dynamic.TLSConfiguration{ + Options: map[string]tls.Options{}, + }, }, }, { @@ -617,7 +632,9 @@ func TestLoadIngresses(t *testing.T) { }, }, }, - TLS: &dynamic.TLSConfiguration{}, + TLS: &dynamic.TLSConfiguration{ + Options: map[string]tls.Options{}, + }, }, }, { @@ -681,7 +698,9 @@ func TestLoadIngresses(t *testing.T) { }, }, }, - TLS: &dynamic.TLSConfiguration{}, + TLS: &dynamic.TLSConfiguration{ + Options: map[string]tls.Options{}, + }, }, }, { @@ -730,7 +749,9 @@ func TestLoadIngresses(t *testing.T) { }, }, }, - TLS: &dynamic.TLSConfiguration{}, + TLS: &dynamic.TLSConfiguration{ + Options: map[string]tls.Options{}, + }, }, }, { @@ -789,7 +810,9 @@ func TestLoadIngresses(t *testing.T) { }, }, }, - TLS: &dynamic.TLSConfiguration{}, + TLS: &dynamic.TLSConfiguration{ + Options: map[string]tls.Options{}, + }, }, }, { @@ -841,7 +864,9 @@ func TestLoadIngresses(t *testing.T) { }, }, }, - TLS: &dynamic.TLSConfiguration{}, + TLS: &dynamic.TLSConfiguration{ + Options: map[string]tls.Options{}, + }, }, }, { @@ -901,7 +926,9 @@ func TestLoadIngresses(t *testing.T) { }, }, }, - TLS: &dynamic.TLSConfiguration{}, + TLS: &dynamic.TLSConfiguration{ + Options: map[string]tls.Options{}, + }, }, }, { @@ -961,7 +988,9 @@ func TestLoadIngresses(t *testing.T) { }, }, }, - TLS: &dynamic.TLSConfiguration{}, + TLS: &dynamic.TLSConfiguration{ + Options: map[string]tls.Options{}, + }, }, }, { @@ -1013,7 +1042,9 @@ func TestLoadIngresses(t *testing.T) { }, }, }, - TLS: &dynamic.TLSConfiguration{}, + TLS: &dynamic.TLSConfiguration{ + Options: map[string]tls.Options{}, + }, }, }, { @@ -1066,7 +1097,9 @@ func TestLoadIngresses(t *testing.T) { }, ServersTransports: map[string]*dynamic.ServersTransport{}, }, - TLS: &dynamic.TLSConfiguration{}, + TLS: &dynamic.TLSConfiguration{ + Options: map[string]tls.Options{}, + }, }, }, { @@ -1125,7 +1158,9 @@ func TestLoadIngresses(t *testing.T) { }, }, }, - TLS: &dynamic.TLSConfiguration{}, + TLS: &dynamic.TLSConfiguration{ + Options: map[string]tls.Options{}, + }, }, }, { @@ -1184,7 +1219,9 @@ func TestLoadIngresses(t *testing.T) { }, }, }, - TLS: &dynamic.TLSConfiguration{}, + TLS: &dynamic.TLSConfiguration{ + Options: map[string]tls.Options{}, + }, }, }, { @@ -1243,7 +1280,9 @@ func TestLoadIngresses(t *testing.T) { }, }, }, - TLS: &dynamic.TLSConfiguration{}, + TLS: &dynamic.TLSConfiguration{ + Options: map[string]tls.Options{}, + }, }, }, { @@ -1296,7 +1335,9 @@ func TestLoadIngresses(t *testing.T) { }, }, }, - TLS: &dynamic.TLSConfiguration{}, + TLS: &dynamic.TLSConfiguration{ + Options: map[string]tls.Options{}, + }, }, }, { @@ -1357,7 +1398,9 @@ func TestLoadIngresses(t *testing.T) { }, }, }, - TLS: &dynamic.TLSConfiguration{}, + TLS: &dynamic.TLSConfiguration{ + Options: map[string]tls.Options{}, + }, }, }, { @@ -1418,7 +1461,9 @@ func TestLoadIngresses(t *testing.T) { }, }, }, - TLS: &dynamic.TLSConfiguration{}, + TLS: &dynamic.TLSConfiguration{ + Options: map[string]tls.Options{}, + }, }, }, { @@ -1479,7 +1524,9 @@ func TestLoadIngresses(t *testing.T) { }, }, }, - TLS: &dynamic.TLSConfiguration{}, + TLS: &dynamic.TLSConfiguration{ + Options: map[string]tls.Options{}, + }, }, }, { @@ -1540,7 +1587,9 @@ func TestLoadIngresses(t *testing.T) { }, }, }, - TLS: &dynamic.TLSConfiguration{}, + TLS: &dynamic.TLSConfiguration{ + Options: map[string]tls.Options{}, + }, }, }, { @@ -1601,7 +1650,9 @@ func TestLoadIngresses(t *testing.T) { }, }, }, - TLS: &dynamic.TLSConfiguration{}, + TLS: &dynamic.TLSConfiguration{ + Options: map[string]tls.Options{}, + }, }, }, { @@ -1662,7 +1713,9 @@ func TestLoadIngresses(t *testing.T) { }, }, }, - TLS: &dynamic.TLSConfiguration{}, + TLS: &dynamic.TLSConfiguration{ + Options: map[string]tls.Options{}, + }, }, }, { @@ -1723,7 +1776,9 @@ func TestLoadIngresses(t *testing.T) { }, }, }, - TLS: &dynamic.TLSConfiguration{}, + TLS: &dynamic.TLSConfiguration{ + Options: map[string]tls.Options{}, + }, }, }, { @@ -1771,7 +1826,216 @@ func TestLoadIngresses(t *testing.T) { }, }, }, - TLS: &dynamic.TLSConfiguration{}, + TLS: &dynamic.TLSConfiguration{ + Options: map[string]tls.Options{}, + }, + }, + }, + { + desc: "Auth TLS secret", + paths: []string{ + "services.yml", + "secrets.yml", + "ingressclasses.yml", + "ingresses/20-ingress-with-auth-tls-secret.yml", + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Services: map[string]*dynamic.TCPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{ + "default-ingress-with-auth-tls-secret-rule-0-path-0": { + Rule: "Host(`auth-tls-secret.localhost`) && Path(`/`)", + RuleSyntax: "default", + Service: "default-ingress-with-auth-tls-secret-whoami-80", + TLS: &dynamic.RouterTLSConfig{ + Options: "default-ingress-with-auth-tls-secret-default-ca-secret", + }, + }, + "default-ingress-with-auth-tls-secret-rule-0-path-0-http": { + EntryPoints: []string{"web"}, + Rule: "Host(`auth-tls-secret.localhost`) && Path(`/`)", + RuleSyntax: "default", + Middlewares: []string{"default-ingress-with-auth-tls-secret-rule-0-path-0-redirect-scheme"}, + Service: "noop@internal", + }, + }, + Middlewares: map[string]*dynamic.Middleware{ + "default-ingress-with-auth-tls-secret-rule-0-path-0-redirect-scheme": { + RedirectScheme: &dynamic.RedirectScheme{ + Scheme: "https", + ForcePermanentRedirect: true, + }, + }, + }, + Services: map[string]*dynamic.Service{ + "default-ingress-with-auth-tls-secret-whoami-80": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://10.10.0.1:80", + }, + { + URL: "http://10.10.0.2:80", + }, + }, + Strategy: "wrr", + PassHostHeader: ptr.To(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: dynamic.DefaultFlushInterval, + }, + ServersTransport: "default-ingress-with-auth-tls-secret", + }, + }, + }, + ServersTransports: map[string]*dynamic.ServersTransport{ + "default-ingress-with-auth-tls-secret": { + ForwardingTimeouts: &dynamic.ForwardingTimeouts{ + DialTimeout: ptypes.Duration(60 * time.Second), + }, + }, + }, + }, + TLS: &dynamic.TLSConfiguration{ + Certificates: []*tls.CertAndStores{ + { + Certificate: tls.Certificate{ + CertFile: "-----BEGIN CERTIFICATE-----", + KeyFile: "-----BEGIN CERTIFICATE-----", + }, + }, + }, + Options: map[string]tls.Options{ + "default-ingress-with-auth-tls-secret-default-ca-secret": { + ClientAuth: tls.ClientAuth{ + CAFiles: []types.FileOrContent{"-----BEGIN CERTIFICATE-----"}, + ClientAuthType: "RequireAndVerifyClientCert", + }, + CipherSuites: []string{ + "TLS_AES_128_GCM_SHA256", + "TLS_AES_256_GCM_SHA384", + "TLS_CHACHA20_POLY1305_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", + "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA", + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", + "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256", + "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", + }, + ALPNProtocols: []string{"h2", "http/1.1", tlsalpn01.ACMETLS1Protocol}, + }, + }, + }, + }, + }, + + { + desc: "Auth TLS verify client", + paths: []string{ + "services.yml", + "secrets.yml", + "ingressclasses.yml", + "ingresses/21-ingress-with-auth-tls-verify-client.yml", + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Services: map[string]*dynamic.TCPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{ + "default-ingress-with-auth-tls-verify-client-rule-0-path-0": { + Rule: "Host(`auth-tls-verify-client.localhost`) && Path(`/`)", + RuleSyntax: "default", + Service: "default-ingress-with-auth-tls-verify-client-whoami-80", + TLS: &dynamic.RouterTLSConfig{ + Options: "default-ingress-with-auth-tls-verify-client-default-ca-secret", + }, + }, + "default-ingress-with-auth-tls-verify-client-rule-0-path-0-http": { + EntryPoints: []string{"web"}, + Rule: "Host(`auth-tls-verify-client.localhost`) && Path(`/`)", + RuleSyntax: "default", + Middlewares: []string{"default-ingress-with-auth-tls-verify-client-rule-0-path-0-redirect-scheme"}, + Service: "noop@internal", + }, + }, + Middlewares: map[string]*dynamic.Middleware{ + "default-ingress-with-auth-tls-verify-client-rule-0-path-0-redirect-scheme": { + RedirectScheme: &dynamic.RedirectScheme{ + Scheme: "https", + ForcePermanentRedirect: true, + }, + }, + }, + Services: map[string]*dynamic.Service{ + "default-ingress-with-auth-tls-verify-client-whoami-80": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://10.10.0.1:80", + }, + { + URL: "http://10.10.0.2:80", + }, + }, + Strategy: "wrr", + PassHostHeader: ptr.To(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: dynamic.DefaultFlushInterval, + }, + ServersTransport: "default-ingress-with-auth-tls-verify-client", + }, + }, + }, + ServersTransports: map[string]*dynamic.ServersTransport{ + "default-ingress-with-auth-tls-verify-client": { + ForwardingTimeouts: &dynamic.ForwardingTimeouts{ + DialTimeout: ptypes.Duration(60 * time.Second), + }, + }, + }, + }, + TLS: &dynamic.TLSConfiguration{ + Certificates: []*tls.CertAndStores{ + { + Certificate: tls.Certificate{ + CertFile: "-----BEGIN CERTIFICATE-----", + KeyFile: "-----BEGIN CERTIFICATE-----", + }, + }, + }, + Options: map[string]tls.Options{ + "default-ingress-with-auth-tls-verify-client-default-ca-secret": { + ClientAuth: tls.ClientAuth{ + CAFiles: []types.FileOrContent{"-----BEGIN CERTIFICATE-----"}, + ClientAuthType: "VerifyClientCertIfGiven", + }, + CipherSuites: []string{ + "TLS_AES_128_GCM_SHA256", + "TLS_AES_256_GCM_SHA384", + "TLS_CHACHA20_POLY1305_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", + "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA", + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", + "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256", + "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", + }, + ALPNProtocols: []string{"h2", "http/1.1", tlsalpn01.ACMETLS1Protocol}, + }, + }, + }, }, }, } diff --git a/pkg/tls/tls.go b/pkg/tls/tls.go index e3119e833..1a60a8427 100644 --- a/pkg/tls/tls.go +++ b/pkg/tls/tls.go @@ -4,6 +4,30 @@ import "github.com/traefik/traefik/v3/pkg/types" const certificateHeader = "-----BEGIN CERTIFICATE-----\n" +const ( + // NoClientCert indicates that no client certificate should be requested + // during the handshake, and if any certificates are sent they will not + // be verified. + NoClientCert = "NoClientCert" + // RequestClientCert indicates that a client certificate should be requested + // during the handshake, but does not require that the client send any + // certificates. + RequestClientCert = "RequestClientCert" + // RequireAnyClientCert indicates that a client certificate should be requested + // during the handshake, and that at least one certificate is required to be + // sent by the client, but that certificate is not required to be valid. + RequireAnyClientCert = "RequireAnyClientCert" + // VerifyClientCertIfGiven indicates that a client certificate should be requested + // during the handshake, but does not require that the client sends a + // certificate. If the client does send a certificate it is required to be + // valid. + VerifyClientCertIfGiven = "VerifyClientCertIfGiven" + // RequireAndVerifyClientCert indicates that a client certificate should be requested + // during the handshake, and that at least one valid certificate is required + // to be sent by the client. + RequireAndVerifyClientCert = "RequireAndVerifyClientCert" +) + // +k8s:deepcopy-gen=true // ClientAuth defines the parameters of the client authentication part of the TLS connection, if any. diff --git a/pkg/tls/tlsmanager.go b/pkg/tls/tlsmanager.go index edd5987d9..9003c21f3 100644 --- a/pkg/tls/tlsmanager.go +++ b/pkg/tls/tlsmanager.go @@ -460,15 +460,15 @@ func buildTLSConfig(tlsOption Options) (*tls.Config, error) { } switch clientAuthType { - case "NoClientCert": + case NoClientCert: conf.ClientAuth = tls.NoClientCert - case "RequestClientCert": + case RequestClientCert: conf.ClientAuth = tls.RequestClientCert - case "RequireAnyClientCert": + case RequireAnyClientCert: conf.ClientAuth = tls.RequireAnyClientCert - case "VerifyClientCertIfGiven": + case VerifyClientCertIfGiven: conf.ClientAuth = tls.VerifyClientCertIfGiven - case "RequireAndVerifyClientCert": + case RequireAndVerifyClientCert: conf.ClientAuth = tls.RequireAndVerifyClientCert default: return nil, fmt.Errorf("unknown client auth type %q", clientAuthType)