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 post
1.1 05/2022 Reformating post
1.2 07/2022 Informations update

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

Environment : Debian 11.2, Docker 20.10.x, docker-compose 2.4.x, Traefik 2.8

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

I am using the path /opt/docker/dc to store YAML files (docker-compose and Traefik configuration files) :

mkdir -p /opt/docker/dc/{conf,logs}
mkdir -p /opt/docker/dc/conf/traefikdynamic

touch /opt/docker/dc/conf/acme.json
chmod 0600 /opt/docker/dc/conf/acme.json

touch /opt/docker/dc/logs/traefik.log /opt/docker/dc/conf/traefik.yml /opt/docker/dc/conf/traefikdynamic/dynamic.yml

This example is a Traefik "static configuration" ; it will use docker labels in the docker-compose file. Here is the docker-compose.yml file :

---
services:
  traefik:
    image: traefik:2.8
    restart: unless-stopped
    environment:
      - "GANDIV5_API_KEY=<INSERT-WITHOUT-HOOK>"
    ports:
      - 80:80
      - 443:443
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./conf/traefik.yml:/etc/traefik/traefik.yml:ro
      - ./conf/traefikdynamic:/etc/traefik/dynamic:ro
      - ./conf/acme.json:/acme.json
    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:2.14.0
    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 :

---
global:
  sendAnonymousUsage: false
  checkNewVersion: false

api:
  dashboard: true

pilot:
  dashboard: false

log:
  level: ERROR

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

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

certificatesResolvers:
  letsencrypt:
    acme:
      email: contact@domain.local
#      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/traefik_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:
    compression:
      compress:
        excludedContentTypes:
          - text/event-stream
 
    security:
      headers:
        accessControlAllowMethods:
          - GET
          - OPTIONS
          - PUT
        accessControlMaxAge: 100
        addVaryHeader: true
        browserXssFilter: true
        contentTypeNosniff: true
        forceSTSHeader: true
        frameDeny: true
        stsPreload: true
        customFrameOptionsValue: SAMEORIGIN
        referrerPolicy: "origin-when-cross-origin"
        permissionsPolicy: "camera 'none'; microphone 'none'; geolocation 'none'; payment 'none';"
        stsSeconds: 315360000
        hostsProxyHeaders:
          - "X-Forwarded-Host"

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.

In short :

  • docker-compose file has the API key, in an environment variable for Traefik ;
  • acme.json file is ready with needed rights ;
  • TLS labels for services are shorter and simpler! Everything needed are in Traefik configuration.

For every service in the docker-compose.yml file, you have to add 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

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

Partager l'article