🔏 Traefik 2, generate a wildcard SSL certificate with Let's Encrypt

How to generate a wildcard SSL certificate with Traefik ? This post is about it, with Gandi and Let's Encrypt to do so.

logo traefik

This post will explain how to generate a wildcard SSL certificate with Let's Encrypt. Have a certificate one-by-one per service is a good practice, you can also exploit the wildcard functionality of Traefik to generate a wildcard SSL certificate for all your services.

Important update of this post by ldez on 15/07/2020. Again, a big THANK YOU !
Version Date Comments
1 07/2020 Initial creation
1.1 05/2020 Reformating post
1.2 07/2022 Update informations
2.0 08/2023 Update versions, adding blocs "docker labels" and "without docker label", delete old informations

Goal : Configure Traefik to gain a wildcard certificate from Let's Encrypt.

Environment : Debian 12, Docker 24.x, docker compose (plugin) 2.20.x, Traefik 2.10.

Execution context :

jho@vmi866042:/opt/docker/dc$ tree
.
├── conf
│   ├── acme.json
│   ├── traefik.yml
│   ├── traefikdynamic
│   │   ├── general.yml
│   │   ├── routersservices.yml
├── docker-compose.yml
└── logs
    ├── traefikAccess.log
    ├── traefik.log
  • path where are every folder and files : /opt/docker/dc
  • path of the principal configuration file for Traefik : /opt/docker/dc/conf/traefik.yml
  • folder where are every dynamic configuration files : /opt/docker/dc/conf/traefikdynamic
  • path of the file which is used to store SSL certificates for let's encrypt (or other provider) : /opt/docker/dc/conf/acme.json
  • folder to store logs : /opt/docker/dc/logs/

This post involves a modification to your DNS zone. Be sure to have necessary access and rights to your DNS service or server, to add DNS records and generate API keys. Here, I will use "Gandi" with the "LiveDNS" function. The procedure is similar to other DNS services and providers, but will not be seen in this article.

DNS preparation

Many verification methods are available to generate a wildcard certificate : TLS, HTTP or DNS. Some APIs are provided by DNS providers, which are used by Traefik with its own tool lego.

You have to create "A" records of your services. Next, generate an API key from your DNS provider. If you are using Gandi like me, login to "account.gandi.net", go to the tab "Security" and click on "Production API key". Keep this key, if you lose it you will have to generate a new one.

Docker and Traefik configuration

Here is the docker-compose.yml base file for this post :

---
services:
  traefik:
    image: traefik:saintmarcelin
    container_name: traefik
    restart: unless-stopped
    ports:
      - target : 80
        published : 80
        protocol: tcp
        mode : host
      ### BEGIN dashboad
      - target : 8080
        published : 8080
        protocol: tcp
        mode : host
      ### END dashboard
      - target : 443
        published : 443
        protocol: tcp
        mode : host
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./conf/traefikdynamic:/dynamic
      - ./conf/traefik.yml:/etc/traefik/traefik.yml:ro
      - ./conf/acme.json:/etc/traefik/acme.json
      - ./logs/traefik.log:/etc/traefik/applog.log
    environment:
      TZ: Europe/Paris
      GANDIV5_API_KEY: apikey

  portainer:
    container_name: portainer
    image: portainer/portainer-ce:alpine
    restart: unless-stopped
    depends_on:
      - traefik
    command: -H unix:///var/run/docker.sock
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - dataportainer:/data
    labels:
      traefik.enable: true
      traefik.http.routers.portainer.entrypoints: websecure
      traefik.http.routers.portainer.rule: Host(`portainer.domain.local`)
      traefik.http.routers.portainer.middlewares: security@file
      traefik.http.routers.portainer.tls: true

volumes:
  dataportainer:

File conf/traefik.yml. Remember, I'm using Gandi as DNS provider. Don't forget to modify to your needs and your provider:

---
global:
  sendAnonymousUsage: false
  checkNewVersion: false

api:
  dashboard: true

log:
  filePath: "/etc/traefik/applog.log"
  format: json
  level: "ERROR"

providers:
  docker:
    endpoint: unix:///var/run/docker.sock
    exposedByDefault: false
    watch: true
    swarmMode: false
  file:
    directory: "/dynamic"
    watch: true

entryPoints:
  web:
    address: ":80"
    http:
      redirections:
        entryPoint:
          to: websecure
          scheme: https
  websecure:
    address: ":443"

certificatesResolvers:
  letsencrypt:
    acme:
      email: [email protected]
#      caServer: https://acme-staging-v02.api.letsencrypt.org/directory
      caServer: https://acme-v02.api.letsencrypt.org/directory
      storage: acme.json
      keyType: EC256
      dnsChallenge:
        provider: gandiv5
        resolvers:
          - "1.1.1.1:53"
          - "8.8.8.8:53"

File conf/traefikdynamic/dynamic.yml :

---
tls:
  options:
    default:
      minVersion: VersionTLS12
      sniStrict: true
      cipherSuites:
        - TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
        - TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
        - TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305
        - TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256
        - TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
        - TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305
        - TLS_AES_128_GCM_SHA256
        - TLS_AES_256_GCM_SHA384
        - TLS_CHACHA20_POLY1305_SHA256
      curvePreferences:
        - CurveP521
        - CurveP384
      alpnProtocols:
        - h2
        - http/1.1
    mintls13:
      minVersion: VersionTLS13

http:
  middlewares:
    security:
      headers:
        accessControlAllowMethods:
          - GET
          - OPTIONS
          - PUT
        accessControlMaxAge: 100
        addVaryHeader: true
        browserXssFilter: true
        contentTypeNosniff: true
        customFrameOptionsValue: SAMEORIGIN
        customResponseHeaders:
          Access-Control-Allow-Origin: "*"
          Sec-Fetch-Site: cross-site
          X-Forwarded-Proto: https
        forceSTSHeader: true
        frameDeny: true
        hostsProxyHeaders:
          - "X-Forwarded-Host"
        referrerPolicy: "strict-origin-when-cross-origin"
        stsIncludeSubdomains: true
        stsPreload: true
        stsSeconds: 31536000

Be aware of the traefik.yml⁣ file : there are two lines with caServer, pointing to Let's Encrypt. During your build, to avoid a ban from Let's Encrypt (rate limiter on demands per week), use the line "caServer ... acme-staging". When you have finished your build, comment the line "caServer ... acme-staging" and uncomment "caServer ... acme-v02". Don't forget to restart Traefik to take into account the changes and regenerate the production certificate.

Use on the method above: docker labels or dynamic configuration.

Usage of docker labels

Every service in your docker-compose.yml needs these labels:

    labels:
      traefik.enable: true
      traefik.http.routers.SERVICENAME.entrypoints: websecure
      traefik.http.routers.SERVICENAME.rule: Host(`SERVICENAME.domain.local`)
      traefik.http.routers.SERVICENAME.middlewares: security@file
      traefik.http.routers.SERVICENAME.tls: true
      traefik.http.routers.SERVICENAME.tls.certresolver: "letsencrypt"

If we take our traefik and portainer services (for example), here's the docker-compose.yml :

---
services:
  traefik:
    image: traefik:saintmarcelin
    container_name: traefik
    restart: unless-stopped
    ports:
      - target : 80
        published : 80
        protocol: tcp
        mode : host
      ### BEGIN dashboad
      - target : 8080
        published : 8080
        protocol: tcp
        mode : host
      ### END dashboard
      - target : 443
        published : 443
        protocol: tcp
        mode : host
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./conf/traefikdynamic:/dynamic
      - ./conf/traefik.yml:/etc/traefik/traefik.yml:ro
      - ./conf/acme.json:/etc/traefik/acme.json
      - ./logs/traefik.log:/etc/traefik/applog.log
    environment:
      TZ: Europe/Paris
      GANDIV5_API_KEY: apikey
    labels:
      traefik.enable: true
      traefik.http.routers.traefik-secure.entrypoints: websecure
      traefik.http.routers.traefik-secure.rule: Host(`traefik.domain.local`)
      traefik.http.routers.traefik-secure.tls: true
      traefik.http.routers.traefik-secure.tls.certresolver: letsencrypt
      traefik.http.routers.traefik-secure.tls.domains[0].main: domain.local
      traefik.http.routers.traefik-secure.tls.domains[0].sans: *.domain.local
      traefik.http.routers.traefik-secure.service: api@internal

  portainer:
    container_name: portainer
    image: portainer/portainer-ce:alpine
    restart: unless-stopped
    depends_on:
      - traefik
    command: -H unix:///var/run/docker.sock
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - dataportainer:/data
    labels:
      traefik.enable: true
      traefik.http.routers.portainer.entrypoints: websecure
      traefik.http.routers.portainer.rule: Host(`portainer.domain.local`)
      traefik.http.routers.portainer.middlewares: security@file
      traefik.http.routers.portainer.tls: true

volumes:
  dataportainer:

Usage of a dynamic configuration file instead of docker labels

If you do not want docker labels, you have to create a file in the dynamic configuration folder of traefik, and type this:

http:
...
  services:
    sc-SERVICENAME:
      loadBalancer:
        servers:
        - url: "http://SERVICENAME:PORT"
 
   routers:
     rt-SERVICENAME:
      entryPoints:
      - websecure
      middlewares:
      - security
      service: sc-SERVICENAME
      rule: Host(`SERVICENAME.domain.local`)
      tls:
        certResolver: letsencrypt

The docker-compose file is the same as before in this article, and have no labels.

acme.json  file now has more lines with the wildcard certificate. With this configuration, your services are behind Traefik, can be accessed with SSL (and will use the wildcard SSL certificate).

Sources : Traefik et Let's Encrypt