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)