vladvitan.com

How to set up mutual TLS with Traefik for wildcard domains


· 5 min read ← back to posts

In a previous article I described how to configure mutual TLS (mTLS) with Traefik using a basic domain. How can you setup mTLS for wildcard domains? I’ll modify the setup described previously to support a wildcard domain.

First, I will need a certificate with the common name set to a wildcard domain. Previously I used localhost as a domain. I cannot use *.localhost for this example because localhost is a special-use domain that tools like curl or browsers treat differently. Instead, I’ll use another domain: *.example.com.

Let’s generate a server certificate for the *.example.com wildcard domain. Make sure to specify *.example.com as the CN (Common Name):

-> % ./gen_cert.sh --cacrt ca.crt --cakey ca.key -o server
...
-> % ./inspect_cert.sh server.crt
...
----------------------------------------
Certificate summary for: server.crt
Common Name (CN): *.example.com
Status: Valid (expires on 2035-07-01T13:13:07Z UTC)
----------------------------------------

Modern certificates use the Subject Alternative Name (SAN) extension to support multiple domains and simplify certificate management. However, for this example, a basic CN-based cert works fine.

To test the wildcard domain locally, I’ll add the foo.example.com subdomain in the /etc/hostsfile so my machine can resolve it to the localhost address, then test it with a ping:

-> % cat /etc/hosts | grep example.com
127.0.0.1    	foo.example.com
::1          	foo.example.com

-> % ping foo.example.com -c 3
PING foo.example.com (127.0.0.1): 56 data bytes
64 bytes from 127.0.0.1: icmp_seq=0 ttl=64 time=0.078 ms
64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.190 ms
64 bytes from 127.0.0.1: icmp_seq=2 ttl=64 time=0.116 ms

--- foo.example.com ping statistics ---
3 packets transmitted, 3 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 0.078/0.128/0.190/0.047 ms

Let’s create a TLS secret in the whoami namespace (will be used by an ingress resource in the whoami namespace):

-> % kubectl create secret tls server-wildcard-tls-certificate --cert=server.crt --key=server.key -n whoami
secret/server-wildcard-tls-certificate created

The certificate is available in the cluster. Next, an ingress rule for the wildcard domain must be created, so all requests going to a subdomain of example.com will be redirected to the whoami service. Let’s create it, apply it and then test it using curl:

-> % cat whoami-wildcard-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: whoami-wildcard
  namespace: whoami
spec:
  ingressClassName: traefik
  rules:
  - host: '*.example.com'
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: whoami
            port:
              name: whoamiweb
  tls:
    - hosts:
      - '*.example.com'
      secretName: server-wildcard-tls-certificate
-> % k apply -f whoami-wildcard-ingress.yaml
ingress.networking.k8s.io/whoami-wildcard created
-> % curl --cacert ca.crt https://foo.example.com
Hostname: whoami
IP: 127.0.0.1
IP: ::1
IP: 10.244.0.3
IP: fe80::dc26:15ff:fee1:cbe7
RemoteAddr: 10.244.0.4:54224
GET / HTTP/1.1
Host: foo.example.com
User-Agent: curl/8.7.1
Accept: */*
Accept-Encoding: gzip
X-Forwarded-For: 10.244.0.1
X-Forwarded-Host: foo.example.com
X-Forwarded-Port: 443
X-Forwarded-Proto: https
X-Forwarded-Server: traefik-6489fdb447-jgqgx
X-Real-Ip: 10.244.0.1

Everything is in place for the server certificates. What about the client certificates? Let’s add the TLS option and the middleware annotations to the ingress rule so that the client certificates are requested:

-> % cat whoami-wildcard-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: whoami-wildcard
  namespace: whoami
  annotations:
    traefik.ingress.kubernetes.io/router.tls.options: traefik-request-client-cert@kubernetescrd
    traefik.ingress.kubernetes.io/router.middlewares: traefik-pass-client-cert@kubernetescrd
spec:
  ingressClassName: traefik
  rules:
  - host: '*.example.com'
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: whoami
            port:
              name: whoamiweb
  tls:
    - hosts:
      - '*.example.com'
      secretName: server-wildcard-tls-certificate
-> % k apply -f whoami-wildcard-ingress.yaml
ingress.networking.k8s.io/whoami-wildcard configured
-> % curl --cacert ca.crt --cert client.crt --key client.key https://foo.example.com
Hostname: whoami
IP: 127.0.0.1
IP: ::1
IP: 10.244.0.3
IP: fe80::dc26:15ff:fee1:cbe7
RemoteAddr: 10.244.0.4:50252
GET / HTTP/1.1
Host: foo.example.com
User-Agent: curl/8.7.1
Accept: */*
Accept-Encoding: gzip
X-Forwarded-For: 10.244.0.1
X-Forwarded-Host: foo.example.com
X-Forwarded-Port: 443
X-Forwarded-Proto: https
X-Forwarded-Server: traefik-6489fdb447-jgqgx
X-Real-Ip: 10.244.0.1

Hmm that’s odd. The request went through but no client certificate header is present. Remember that the TLS option which is currently set only requires the client certificate to be sent in the request, but does not verify it. Let’s check the Traefik logs:

traefik-6489fdb447-jgqgx 2025-07-06T11:52:27Z DBG github.com/traefik/traefik/v3/pkg/mid
dlewares/passtlsclientcert/pass_tls_client_cert.go:154 > Tried to extract a certificate
 on a request without mutual TLS middlewareName=traefik-pass-client-cert@kubernetescrd
middlewareType=PassClientTLSCert

As it can be seen in the output, mTLS doesn’t seem to be picked up anymore. This is because Traefik cannot handle wildcard subdomains in the ingress routes because it needs an exact match for checking the certificate. There is a GitHub issue that describes this and also a note in the Traefik docs.

To go around this issue, you can either specify a rule for each subdomain in the ingress and it will work as previously. If you have a lot of domains, you can set a default TLS option for Traefik. This will be the fallback behavior in case no route is matched.

Let’s go with the second option and create a default TLS option for Traefik, then send the curl request again:

-> % cat traefik-default-tlsoption.yaml
apiVersion: traefik.io/v1alpha1
kind: TLSOption
metadata:
  name: default
  namespace: traefik
spec:
  clientAuth:
    secretNames:
      - ca-tls-certificate
    clientAuthType: RequireAndVerifyClientCert
-> % k apply -f traefik-default-tlsoption.yaml
tlsoption.traefik.io/default created
-> % curl --cacert ca.crt --cert client.crt --key client.key https://foo.example.com
Hostname: whoami
IP: 127.0.0.1
IP: ::1
IP: 10.244.0.3
IP: fe80::dc26:15ff:fee1:cbe7
RemoteAddr: 10.244.0.4:54310
GET / HTTP/1.1
Host: foo.example.com
User-Agent: curl/8.7.1
Accept: */*
Accept-Encoding: gzip
X-Forwarded-For: 10.244.0.1
X-Forwarded-Host: foo.example.com
X-Forwarded-Port: 443
X-Forwarded-Proto: https
X-Forwarded-Server: traefik-6489fdb447-jgqgx
X-Forwarded-Tls-Client-Cert: MIICbzCCAhSgAwIBAgIUORIAiefPdDRTDdMUPItO2bmZeLEwCgYIKoZIzj0EAwIwgZIxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMRMwEQYDVQQKDApNeSBDb21wYW55MQswCQYDVQQLDAJjYTESMBAGA1UEAwwJbG9jYWxob3N0MSAwHgYJKoZIhvcNAQkBFhFhZG1pbkBleGFtcGxlLmNvbTAeFw0yNTA3MDMxMzE0NTRaFw0zNTA3MDExMzE0NTRaMIGWMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzETMBEGA1UECgwKTXkgQ29tcGFueTEPMA0GA1UECwwGY2xpZW50MRIwEAYDVQQDDAlsb2NhbGhvc3QxIDAeBgkqhkiG9w0BCQEWEWFkbWluQGV4YW1wbGUuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEeba9ydqEa9dy4K6rxYHMg/H3OVkWeJ0Df+3IwCUvjlyuDwT1R6XKsqCpS2n0gKM3gQzh+ZBguvYAVMvrS4k/DKNCMEAwHQYDVR0OBBYEFBflZ0zG80LNSFR1bawiQuwtGxc5MB8GA1UdIwQYMBaAFFLGP9dP7t3q8tWEA6Tk8B/tIDxaMAoGCCqGSM49BAMCA0kAMEYCIQCc6BMwzUZCrwJTQ3OZzMdeyp/sRviLXfbfKGd1RFI1PQIhAMhRVPtwVGYqZJXQ4W6BFfB7vkATI9pIyQDLrOc1dCCl
X-Real-Ip: 10.244.0.1

As you can see, the client certificate was sent this time.

The downside to using a default TLS option is that it will be the fallback option for all the requests in the cluster if Traefik is used as an ingress controller. Not all services in your cluster might require a client certificate to be sent. Make sure to think about your specific use case and choose one of the client authentication types that Traefik provides.