Cet article explicite la génération d'un certificat SSL de type 'wildcard' généré gratuitement par Let's Encrypt. Avoir un certificat par service ou sous-domaine c'est bien, mais allons plus loin et exploitons la fonctionnalité wildcard permettant de générer un unique certificat pour tous les sous-domaines.

Importante mise Ă  jour de l'article par ldez au 15/07/2020. Une nouvelle fois, un grand merci !!
Version Date Commentaires
1 07/2020 Création de l'article
1.1 05/2020 Reformatage de l'article
1.2 07/2022 Mise Ă  jour des informations
2.0 08/2023 Mise Ă  jour des versions, ajout des blocs "docker labels" et "sans docker label", suppression des mauvaises informations

Objectif : Configurer Traefik pour obtenir un certificat wildcard via Let's Encrypt pour tous vous services.

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

Contexte d'exécution :

jho@vmi866042:/opt/docker/dc$ tree
.
├── conf
│   ├── acme.json
│   ├── traefik.yml
│   ├── traefikdynamic
│   │   ├── dynamic.yml
├── docker-compose.yml
└── logs
    ├── traefikAccess.log
    ├── traefik.log
RĂ©sultat de la commande "tree" dans le dossier /opt/docker/dc
  • dossier oĂč se trouvent tous les fichiers et dossiers pour cet exemple : /opt/docker/dc
  • emplacement du fichier de configuration principal de Traefik : /opt/docker/dc/conf/traefik.yml
  • dossier oĂč se trouvent les configurations de Traefik (fichiers dynamiques) : /opt/docker/dc/conf/traefikdynamic
  • emplacement du fichier comportant tous les certificats gĂ©nĂ©rĂ©s par let's encrypt (ou autre) : /opt/docker/dc/conf/acme.json
  • dossier oĂč se trouvent les journaux d'Ă©vĂ©nements (logs) : /opt/docker/dc/logs/

Cet article implique une modification dans votre zone DNS. Assurez-vous d'avoir vos identifiants quant à l'accÚs au service ou serveur DNS, y compris les droits nécessaires pour ajouter des enregistrements et générer des clés API. Ici, j'utiliserai le fournisseur "Gandi" avec la fonctionnalité "LiveDNS". La procédure est similaire avec d'autres gestionnaires de service DNS, mais ne sera pas vu dans cet article.

Préparation du DNS

Plusieurs moyens de vérification sont possibles quant à la génération d'un certificat wildcard : TLS, HTTP ou encore DNS. Des API sont fournies par les hébergeurs, qui seront notamment utilisées par Traefik au travers de l'outil lego.

Vous devez crĂ©er les enregistrements de type "A" nĂ©cessaires pour vos services. Ensuite, gĂ©nĂ©rez une clĂ© d'authentification API auprĂšs de votre hĂ©bergeur. Si comme moi, vous ĂȘtes chez Gandi, connectez-vous sur l'adresse "account.gandi.net", dirigez-vous dans l'onglet "Security" et gĂ©nĂ©rez une "Production API key". Gardez cette clĂ© prĂ©cieusement, si vous la perdez, vous devrez la rĂ©gĂ©nĂ©rer.

Configurations pour Docker et Traefik

J'utiliserai le dossier /opt/docker/dc pour y stocker les fichiers YAML docker-compose et les fichiers de configuration de Traefik.

Ci-dessous le fichier docker-compose.yml que j'utiliserai comme base pour cette documentation :

---
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:

Fichier conf/traefik.yml (attention, j'utilise Gandi, référez-vous à la page officielle pour adapter la configuration selon votre fournisseur DNS) :

---
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: 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"

Fichier 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

J'attire votre attention à propos du fichier traefik.yml⁣ : il y a deux lignes caServer, pointant vers des serveurs Let's Encrypt. Pendant vos tests, pour éviter un bannissement auprÚs de Let's Encrypt (nombre maximal de demandes par semaine), utilisez uniquement la ligne "caServer ... acme-staging". Lorsque vos tests seront validés (certificats correctement récupérés et générés), vous pourrez commenter la ligne "caServer ... acme-staging" et reprendre "caServer ... acme-v02". N'oubliez pas de redémarrer Traefik pour prendre en compte les modifications et régénérer les certificats "de production".

En résumé :

  • Le fichier docker-compose contient la clĂ© API prĂ©cĂ©demment gĂ©nĂ©rĂ©e, dans une variable d'environnement pour Traefik
  • Le fichier acme.json est toujours prĂ©sent avec les droits requis
  • Les labels TLS pour les services sont beaucoup plus courts et simples ! Tout se trouve dans la configuration de Traefik.

Enfin, utilisez soit l'une des deux méthodes : labels docker ou fichier de configuration dynamique.

Utilisation des labels docker

Chaque service que vous ajouterez dans votre docker-compose.yml devra comporter ces 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"

Si on reprend le service traefik et qu'on ajoute (par exemple) le service portainer, voici ce Ă  quoi doit ressembler votre fichier 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:

Utilisation de la configuration dynamique plutĂŽt que les labels docker

Si vous n'utilisez pas les labels docker, vous devez ajouter les blocs nécessaires dans le fichier de configuration dynamique :

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

Le fichier docker-compose (le mĂȘme que plus haut dans l'article) ne comporte aucun label (et n'en a pas besoin).


Le fichier acme.json doit maintenant comporter quelques lignes pour votre certificat. Avec cette configuration, vos services seront à présent derriÚre Traefik, seront accessibles via SSL (et utiliseront le certificat SSL wildcard), et bénéficieront des options TLS.

Sources : Traefik et Let's Encrypt

Partager l'article