Aller au contenu
Sécurité medium

Renouveler automatiquement des certificats TLS avec Vault PKI et Traefik

27 min de lecture

Ce guide montre comment automatiser le renouvellement de certificats TLS sans redémarrage de service. Vous allez déployer Traefik avec des certificats émis par Vault PKI, renouvelés automatiquement par Vault Agent, et rechargés dynamiquement par Traefik.

À la fin de ce guide, vous saurez :

  • Configurer Vault PKI pour émettre des certificats courts (5 min pour la démo)
  • Utiliser Vault Agent pour écrire cert.pem et key.pem sur disque
  • Configurer Traefik pour recharger les certificats sans restart
  • Observer le renouvellement automatique en action

Prérequis : Docker Compose, un terminal Linux/macOS.

Pourquoi combiner Vault PKI, Vault Agent et Traefik ?

Section intitulée « Pourquoi combiner Vault PKI, Vault Agent et Traefik ? »

Chacun des trois composants couvre une partie du problème et aucun ne suffit seul. Vault PKI est une autorité de certification interne : il émet un certificat à la demande, sans intervention humaine. Vault Agent est le processus qui réclame ce certificat, l'écrit sur disque et recommence avant l'expiration. Traefik consomme ces fichiers et sait les relire à chaud. Retirez l'une des trois briques et une opération manuelle réapparaît quelque part dans la chaîne.

La gestion classique des certificats TLS pose plusieurs problèmes :

ProblèmeConséquence
Certificats longue durée (1-2 ans)Fenêtre d'exploitation large si compromis
Renouvellement manuelOublis, expirations, interruptions de service
Clés privées dans les repos/configsRisque de fuite
Redémarrage requis pour rechargerDowntime lors du renouvellement

Avec Vault PKI + Vault Agent + Traefik :

AspectAvantAprès
Durée de vie1-2 ans24h-72h (ou moins)
RenouvellementManuelAutomatique
Impact sur le serviceRestart requisAucun (rechargement dynamique)
Stockage des clésStatiqueÉphémère (généré à la demande)

Voici comment les composants interagissent :

Architecture Vault PKI + Vault Agent + Traefik

Le flux complet :

  1. Vault PKI émet un certificat TLS avec un TTL court
  2. Vault Agent écrit cert.pem, key.pem et met à jour tls.yml
  3. Traefik détecte le changement de tls.yml et recharge les certificats
  4. Le service (whoami) reste accessible sans interruption
  5. Avant expiration, Vault Agent répète le cycle

Traefik distingue deux types de configuration :

TypeExemplesRechargeable à chaud ?
StatiqueEntrypoints, providers, logs❌ Non (restart requis)
DynamiqueRouters, services, middlewares, TLS✅ Oui

Les certificats TLS font partie de la configuration dynamique. Traefik peut les recharger sans redémarrage grâce au file provider avec l'option watch: true.

Toute la démo tient dans un dossier et un docker-compose.yml de quatre services : Vault, le service applicatif whoami, Traefik et Vault Agent. Les répertoires créés ci-dessous ne sont pas décoratifs : ce sont eux qui sont montés dans les conteneurs, et un chemin manquant empêche Traefik ou Vault Agent de démarrer.

Ces trois étapes créent l'arborescence, le fichier Compose et un certificat provisoire sans lequel Traefik refuserait de servir le point d'entrée HTTPS au premier démarrage.

  1. Créer les répertoires :

    Fenêtre de terminal
    mkdir -p lab-vault-traefik/{vault-agent,traefik/{dynamic,certs},scripts}
    cd lab-vault-traefik
  2. Créer le fichier docker-compose.yml :

    docker-compose.yml
    services:
    # Vault en mode dev pour la démo
    vault:
    image: hashicorp/vault:1.18
    container_name: vault
    cap_add:
    - IPC_LOCK
    environment:
    VAULT_DEV_ROOT_TOKEN_ID: root
    VAULT_DEV_LISTEN_ADDRESS: 0.0.0.0:8200
    VAULT_ADDR: http://127.0.0.1:8200
    ports:
    - "8200:8200"
    networks:
    - vault-traefik
    healthcheck:
    test: ["CMD", "vault", "status"]
    interval: 5s
    timeout: 3s
    retries: 10
    # Service de démo
    whoami:
    image: traefik/whoami:v1.11.0
    container_name: whoami
    networks:
    - vault-traefik
    # Traefik avec file provider
    traefik:
    image: traefik:v3.3
    container_name: traefik
    command:
    - "--api.insecure=true"
    - "--providers.file.directory=/etc/traefik/dynamic"
    - "--providers.file.watch=true"
    - "--entrypoints.web.address=:80"
    - "--entrypoints.websecure.address=:443"
    - "--log.level=INFO"
    ports:
    - "9080:80"
    - "9443:443"
    - "9088:8080" # Dashboard
    volumes:
    - ./traefik/dynamic:/etc/traefik/dynamic:ro
    - ./traefik/certs:/etc/traefik/certs:ro
    networks:
    - vault-traefik
    depends_on:
    - whoami
    # Vault Agent pour le renouvellement des certificats
    vault-agent:
    image: hashicorp/vault:1.18
    container_name: vault-agent
    entrypoint: ["vault", "agent", "-config=/etc/vault-agent/config.hcl"]
    environment:
    VAULT_ADDR: http://vault:8200
    volumes:
    - ./vault-agent:/etc/vault-agent:ro
    - ./traefik/certs:/output/certs
    - ./traefik/dynamic:/output/dynamic
    - vault-agent-token:/run/vault
    networks:
    - vault-traefik
    depends_on:
    vault:
    condition: service_healthy
    networks:
    vault-traefik:
    driver: bridge
    volumes:
    vault-agent-token:
  3. Créer un certificat placeholder pour le démarrage initial :

    Fenêtre de terminal
    openssl req -x509 -nodes -days 1 -newkey rsa:2048 \
    -keyout traefik/certs/key.pem \
    -out traefik/certs/cert.pem \
    -subj "/CN=placeholder.local.test"

Ce certificat temporaire permet à Traefik de démarrer. Vault Agent le remplacera par un vrai certificat signé par Vault PKI.

Traefik est piloté ici entièrement par fichiers, sans étiquettes Docker, précisément pour qu'un autre processus puisse réécrire une partie de sa configuration. Deux fichiers cohabitent dans le répertoire surveillé : routes.yml, écrit une fois à la main, et tls.yml, généré puis regénéré par Vault Agent à chaque renouvellement.

Créez le fichier de routes pour le service whoami :

traefik/dynamic/routes.yml
http:
routers:
whoami:
rule: Host(`whoami.local.test`)
service: whoami
entryPoints:
- websecure
tls: {}
# Redirection HTTP vers HTTPS
whoami-redirect:
rule: Host(`whoami.local.test`)
service: whoami
entryPoints:
- web
middlewares:
- redirect-to-https
middlewares:
redirect-to-https:
redirectScheme:
scheme: https
permanent: true
services:
whoami:
loadBalancer:
servers:
- url: http://whoami:80

Vault Agent est un processus autonome qui s'authentifie seul auprès de Vault, maintient son token valide et rend des templates sur disque. C'est la seule brique de l'architecture qui dialogue avec Vault : ni Traefik ni l'application ne détiennent de token.

Vault Agent utilise AppRole pour s'authentifier auprès de Vault et des templates pour écrire les certificats sur disque.

vault-agent/config.hcl
vault {
address = "http://vault:8200"
}
auto_auth {
method "approle" {
mount_path = "auth/approle"
config = {
role_id_file_path = "/etc/vault-agent/role_id"
secret_id_file_path = "/etc/vault-agent/secret_id"
remove_secret_id_file_after_reading = false
}
}
sink "file" {
config = {
path = "/run/vault/token"
mode = 0644
}
}
}
# Template pour le certificat (cert.pem)
template {
source = "/etc/vault-agent/cert.tpl"
destination = "/output/certs/cert.pem"
perms = 0644
}
# Template pour la clé privée (key.pem)
template {
source = "/etc/vault-agent/key.tpl"
destination = "/output/certs/key.pem"
perms = 0600
}
# Template TLS pour Traefik - force le rechargement du file provider
template {
source = "/etc/vault-agent/traefik-tls.tpl"
destination = "/output/dynamic/tls.yml"
perms = 0644
}
template_config {
static_secret_render_interval = "5m"
exit_on_retry_failure = true
}

Ces templates demandent un nouveau certificat à Vault PKI et extraient les données nécessaires.

Template pour le certificat (cert.pem) :

vault-agent/cert.tpl
{{- with secret "pki_int/issue/traefik-web" "common_name=whoami.local.test" "alt_names=whoami.local.test" "ttl=5m" -}}
{{ .Data.certificate }}
{{ .Data.issuing_ca }}
{{- end -}}

Template pour la clé privée (key.pem) :

vault-agent/key.tpl
{{- with secret "pki_int/issue/traefik-web" "common_name=whoami.local.test" "alt_names=whoami.local.test" "ttl=5m" -}}
{{ .Data.private_key }}
{{- end -}}

Template pour la config TLS Traefik (tls.yml) :

vault-agent/traefik-tls.tpl
{{- with secret "pki_int/issue/traefik-web" "common_name=whoami.local.test" "alt_names=whoami.local.test" "ttl=5m" -}}
# Configuration TLS - regénéré automatiquement par Vault Agent
# Serial: {{ .Data.serial_number }}
# Expiration: {{ .Data.expiration }}
tls:
certificates:
- certFile: /etc/traefik/certs/cert.pem
keyFile: /etc/traefik/certs/key.pem
stores:
default:
defaultCertificate:
certFile: /etc/traefik/certs/cert.pem
keyFile: /etc/traefik/certs/key.pem
{{- end -}}

La configuration de Vault s'effectue une seule fois et enchaîne cinq opérations : une CA racine, une CA intermédiaire signée par la première, un rôle d'émission restreint au domaine de la démo, une policy limitée à ce rôle, puis les identifiants AppRole que Vault Agent utilisera pour s'authentifier. Les regrouper dans un script rend le lab rejouable après un docker compose down.

Ce script configure Vault PKI (Root CA + Intermediate CA), crée un rôle pour Traefik, et génère les credentials AppRole pour Vault Agent.

scripts/init-vault.sh
#!/bin/bash
set -e
echo "=== Configuration Vault PKI + AppRole pour Traefik ==="
# Fonction pour exécuter vault via docker
vault() {
docker exec -e VAULT_ADDR=http://127.0.0.1:8200 -e VAULT_TOKEN=root vault vault "$@"
}
# 1. Activer et configurer PKI Root CA
echo ">>> 1. Activation PKI Root CA..."
vault secrets enable -path=pki pki 2>/dev/null || echo " (pki déjà activé)"
vault secrets tune -max-lease-ttl=87600h pki
vault write -field=certificate pki/root/generate/internal \
common_name="Demo Root CA" \
issuer_name="root-ca" \
ttl=87600h > /dev/null
vault write pki/config/urls \
issuing_certificates="http://vault:8200/v1/pki/ca" \
crl_distribution_points="http://vault:8200/v1/pki/crl"
echo " ✓ PKI Root CA configuré"
# 2. Activer et configurer PKI Intermediate CA
echo ">>> 2. Activation PKI Intermediate CA..."
vault secrets enable -path=pki_int pki 2>/dev/null || echo " (pki_int déjà activé)"
vault secrets tune -max-lease-ttl=43800h pki_int
vault write -field=csr pki_int/intermediate/generate/internal \
common_name="Demo Intermediate CA" \
issuer_name="intermediate-ca" \
> /tmp/pki_int.csr
vault write -field=certificate pki/root/sign-intermediate \
csr=@/tmp/pki_int.csr \
format=pem_bundle \
ttl="43800h" \
> /tmp/int.cert.pem
vault write pki_int/intermediate/set-signed \
certificate=@/tmp/int.cert.pem
echo " ✓ PKI Intermediate CA configuré"
# 3. Créer le rôle PKI pour Traefik
echo ">>> 3. Création du rôle PKI 'traefik-web'..."
vault write pki_int/roles/traefik-web \
allowed_domains="local.test" \
allow_subdomains=true \
allow_bare_domains=false \
max_ttl="24h" \
ttl="5m" # TTL court pour la démo
echo " ✓ Rôle traefik-web créé (TTL: 5 min)"
# 4. Créer la policy pour Vault Agent
echo ">>> 4. Création de la policy 'traefik-pki'..."
vault policy write traefik-pki - <<POLICY
path "pki_int/issue/traefik-web" {
capabilities = ["create", "update"]
}
POLICY
echo " ✓ Policy traefik-pki créée"
# 5. Activer et configurer AppRole
echo ">>> 5. Configuration AppRole..."
vault auth enable approle 2>/dev/null || echo " (approle déjà activé)"
vault write auth/approle/role/traefik-agent \
token_policies="traefik-pki" \
token_ttl=1h \
token_max_ttl=4h \
secret_id_ttl=0
echo " ✓ Rôle AppRole traefik-agent créé"
# 6. Récupérer les credentials AppRole
echo ">>> 6. Génération des credentials AppRole..."
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ROLE_ID=$(vault read -field=role_id auth/approle/role/traefik-agent/role-id)
SECRET_ID=$(vault write -f -field=secret_id auth/approle/role/traefik-agent/secret-id)
echo "$ROLE_ID" > "$SCRIPT_DIR/../vault-agent/role_id"
echo "$SECRET_ID" > "$SCRIPT_DIR/../vault-agent/secret_id"
echo " ✓ Credentials sauvegardés dans vault-agent/"
echo ""
echo "=== Configuration terminée ==="
echo ""
echo "Prochaines étapes :"
echo " 1. docker compose up -d vault-agent"
echo " 2. Ajouter '127.0.0.1 whoami.local.test' à /etc/hosts"
echo " 3. curl -k https://whoami.local.test:9443"

Rendez le script exécutable :

Fenêtre de terminal
chmod +x scripts/init-vault.sh

L'ordre de démarrage compte. Vault doit être opérationnel et sa PKI configurée avant que Vault Agent ne tente son premier rendu : avec exit_on_retry_failure = true, l'agent s'arrête au lieu de boucler indéfiniment sur un chemin inexistant.

Cinq étapes dans cet ordre : les services de base, la configuration de Vault, l'agent, puis la résolution DNS locale sans laquelle le client ne présenterait pas le bon nom d'hôte au moment de la négociation TLS.

  1. Démarrer Vault, Traefik et whoami :

    Fenêtre de terminal
    docker compose up -d vault whoami traefik

    Attendez que Vault soit healthy :

    Fenêtre de terminal
    docker compose ps
  2. Configurer Vault PKI :

    Fenêtre de terminal
    ./scripts/init-vault.sh

    Vous devriez voir :

    === Configuration terminée ===
    Prochaines étapes :
    1. docker compose up -d vault-agent
    2. Ajouter '127.0.0.1 whoami.local.test' à /etc/hosts
    3. curl -k https://whoami.local.test:9443
  3. Démarrer Vault Agent :

    Fenêtre de terminal
    docker compose up -d vault-agent
  4. Vérifier les certificats générés :

    Fenêtre de terminal
    ls -la traefik/certs/

    Vous devriez voir cert.pem et key.pem avec des timestamps récents.

  5. Ajouter l'entrée DNS locale :

    Fenêtre de terminal
    echo "127.0.0.1 whoami.local.test" | sudo tee -a /etc/hosts

Examinez le certificat émis par Vault PKI :

Fenêtre de terminal
openssl x509 -in traefik/certs/cert.pem -noout -subject -issuer -dates

Résultat attendu :

subject=CN = whoami.local.test
issuer=CN = Demo Intermediate CA
notBefore=Mar 16 16:56:40 2026 GMT
notAfter=Mar 16 17:01:40 2026 GMT

Le certificat est signé par "Demo Intermediate CA" et valide 5 minutes.

L'option -k désactive la vérification de la chaîne de confiance : la CA de la démo n'est pas connue du magasin système. La réponse de whoami confirme que Traefik a bien terminé la connexion TLS puis relayé la requête en clair vers le conteneur applicatif.

Fenêtre de terminal
curl -k https://whoami.local.test:9443

Résultat :

Hostname: 9b134b252f36
IP: 127.0.0.1
IP: 172.23.0.3
RemoteAddr: 172.23.0.4:44418
GET / HTTP/1.1
Host: whoami.local.test:9443
...

Le renouvellement est invisible depuis le client : aucune coupure, aucune erreur TLS. Pour le constater, il faut comparer le numéro de série du certificat servi d'une connexion à l'autre. Deux points d'observation se complètent, le certificat vu depuis le réseau et les journaux de Vault Agent.

Exécutez ce script pour observer le renouvellement :

Fenêtre de terminal
echo "=== Observation du renouvellement ==="
LAST_SERIAL=""
while true; do
SERIAL=$(openssl s_client -connect whoami.local.test:9443 \
-servername whoami.local.test </dev/null 2>/dev/null \
| openssl x509 -noout -serial | cut -d= -f2)
DATES=$(openssl s_client -connect whoami.local.test:9443 \
-servername whoami.local.test </dev/null 2>/dev/null \
| openssl x509 -noout -enddate | cut -d= -f2)
NOW=$(date "+%H:%M:%S")
if [[ "$SERIAL" != "$LAST_SERIAL" ]] && [[ -n "$LAST_SERIAL" ]]; then
echo ""
echo ">>> CERTIFICAT RENOUVELÉ ! <<<"
echo ""
fi
LAST_SERIAL="$SERIAL"
echo "[$NOW] Serial: ${SERIAL:0:20}... | Expire: $DATES"
sleep 30
done

Après environ 3-4 minutes, vous verrez le certificat changer :

[17:57:07] Serial: 2F92FE700EA7F45AFAA8... | Expire: Mar 16 17:01:39 2026 GMT
[17:57:37] Serial: 2F92FE700EA7F45AFAA8... | Expire: Mar 16 17:01:39 2026 GMT
[17:58:07] Serial: 2F92FE700EA7F45AFAA8... | Expire: Mar 16 17:01:39 2026 GMT
>>> CERTIFICAT RENOUVELÉ ! <<<
[17:58:37] Serial: 5193A369CE35F6173E6F... | Expire: Mar 16 17:06:11 2026 GMT

Chaque cycle produit trois lignes rendered, une par template, séparées de quelques millisecondes : c'est la confirmation que les trois fichiers proviennent bien du même certificat.

Fenêtre de terminal
docker compose logs vault-agent --tail=20 | grep rendered

Vous verrez les moments de renouvellement :

vault-agent | 2026-03-16T16:56:40.091Z [INFO] agent: (runner) rendered "/etc/vault-agent/cert.tpl" => "/output/certs/cert.pem"
vault-agent | 2026-03-16T16:56:40.094Z [INFO] agent: (runner) rendered "/etc/vault-agent/key.tpl" => "/output/certs/key.pem"
vault-agent | 2026-03-16T16:56:40.096Z [INFO] agent: (runner) rendered "/etc/vault-agent/traefik-tls.tpl" => "/output/dynamic/tls.yml"
vault-agent | 2026-03-16T17:01:11.620Z [INFO] agent: (runner) rendered "/etc/vault-agent/cert.tpl" => "/output/certs/cert.pem"
vault-agent | 2026-03-16T17:01:11.623Z [INFO] agent: (runner) rendered "/etc/vault-agent/key.tpl" => "/output/certs/key.pem"
vault-agent | 2026-03-16T17:01:11.625Z [INFO] agent: (runner) rendered "/etc/vault-agent/traefik-tls.tpl" => "/output/dynamic/tls.yml"

Le point important n'est pas le nombre de conteneurs, mais la répartition des responsabilités : un seul composant détient un accès à Vault, et l'application ignore totalement son existence. C'est cette séparation qui rend le montage réutilisable pour n'importe quel autre service placé derrière le même reverse proxy.

ComposantResponsabilité
Vault PKIÉmet des certificats courts, signés par une CA interne
Vault AgentDemande et renouvelle les certificats, écrit sur disque
TraefikSert le trafic HTTPS, recharge les certificats dynamiquement
whoamiService de démo, ne connaît pas l'existence de Vault

Points clés :

  • Le service (whoami) n'a aucune dépendance directe sur Vault
  • Traefik recharge les certificats sans redémarrage
  • Vault Agent gère tout le cycle de vie des certificats
  • Si Vault est temporairement indisponible, les certificats existants restent valides jusqu'à expiration

Ce lab prend des raccourcis assumés, à commencer par Vault en mode dev, dont les données vivent en mémoire et dont le token root est écrit en clair dans le fichier Compose. Trois familles de sujets demandent un traitement différent en production : la sécurité des identifiants d'amorçage, la disponibilité de Vault, et le réglage des durées de vie.

Les fichiers role_id et secret_id produits par le script d'initialisation sont les identifiants d'amorçage de l'agent : quiconque les obtient peut faire émettre des certificats pour le domaine autorisé par le rôle.

AspectRecommandation
Clé privéeProtéger key.pem avec les permissions 0600
AppRole secretsNe pas committer role_id/secret_id dans Git
Vault en mode devUtiliser un cluster Vault en production avec TLS

Une panne de Vault n'interrompt pas le service immédiatement : le certificat déjà écrit sur disque continue d'être servi jusqu'à son expiration. Le TTL devient donc le délai dont vous disposez pour rétablir Vault.

RisqueMitigation
Vault down au démarrageLe certificat existant reste valide jusqu'à expiration
Vault down au renouvellementVault Agent réessaie. Prévoir un TTL assez long
Perte du stockage VaultBackups réguliers, cluster HA

Le TTL de 5 minutes n'a d'autre but que de rendre le cycle observable pendant la lecture du guide. En production, cette valeur définit à la fois la fenêtre d'exploitation d'une clé compromise et la marge de manoeuvre en cas d'indisponibilité de Vault.

ParamètreRecommandation production
TTL certificat24h-72h, assez long pour absorber une indisponibilité de Vault
max_ttl7-30 jours selon les besoins
allowed_domainsRestreindre aux domaines réellement utilisés

Trois modifications suffisent à passer du lab à un déploiement réel : allonger les durées de vie, remplacer le domaine de test par le vôtre dans le rôle et dans les templates, et abandonner AppRole si la plateforme fournit déjà une identité vérifiable, comme un ServiceAccount Kubernetes.

Modifiez le rôle PKI pour un TTL de 24h :

Fenêtre de terminal
# max_ttl : 7 jours au maximum, ttl : 24h par défaut
vault write pki_int/roles/traefik-web \
allowed_domains="votre-domaine.com" \
allow_subdomains=true \
max_ttl="168h" \
ttl="24h"

Mettez à jour les templates avec votre domaine :

vault-agent/cert.tpl
{{- with secret "pki_int/issue/traefik-web" "common_name=app.votre-domaine.com" "alt_names=app.votre-domaine.com,api.votre-domaine.com" "ttl=24h" -}}
{{ .Data.certificate }}
{{ .Data.issuing_ca }}
{{- end -}}

En environnement Kubernetes, remplacez AppRole par l'auth Kubernetes :

vault-agent/config.hcl (Kubernetes)
auto_auth {
method "kubernetes" {
mount_path = "auth/kubernetes"
config = {
role = "traefik-pki"
}
}
# ...
}
  1. Vault PKI génère des certificats TLS courts et signés
  2. Vault Agent renouvelle automatiquement avant expiration
  3. Traefik recharge les certificats sans redémarrage grâce au file provider
  4. Le template tls.yml force Traefik à détecter le changement
  5. Le service backend n'a aucune dépendance sur Vault
  6. En production, utilisez des TTL de 24-72h et un Vault sécurisé

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracking. Un soutien, même symbolique, m'aide à couvrir l'hébergement et à garder ces ressources gratuites. Merci pour votre appui.

Le formulaire ne s'affiche pas ? Ouvrir Ko-fi dans un onglet.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn