Introduction #
SIGDOCKER25 est un serveur sur lequel Docker est installé, au sein de VCA (Vienne Condrieu Agglomération). Il me sert à déployer des services en interne, à des fins de tests, de bac à sable ou de R&D — si je peux me permettre ce terme — dans le cadre de mes missions de géomaticien, mais aussi pour proposer d’autres services aux agents de la collectivité : sauvegarde PostgreSQL, OliveTin, Node-RED, et quelques autres outils.
Une instance Traefik y tourne également depuis un moment. Historiquement, je l’ai configurée pour consommer le certificat TLS que la DSI m’avait fourni, et qui couvre leur périmètre — ce n’est pas moi qui gère ce certificat vienne-condrieu-agglomeration.fr, c’est la DSI. Il sert le HTTPS pour une poignée de services internes — Technitium, LimeSurvey, et quelques autres — sur des sous-domaines de ce domaine. Il expire en décembre. Ce n’est pas encore urgent, mais c’est le genre d’échéance qui devient urgente très vite si on ne s’en occupe pas.
Plutôt que d’attendre un nouveau certificat de la DSI, ce billet couvre la mise en place d’une alternative : une autorité de certification (CA) privée, hébergée en interne avec step-ca et couplée à Traefik. Elle me permet de servir du HTTPS sur mes propres sous-domaines — *.igeo.vienne-condrieu-agglomeration.fr — moyennant la propagation du root_ca.crt sur les postes clients dans un premier temps, et délivre/renouvelle elle-même les certificats — automatiquement, sans jamais plus de date d’expiration à surveiller sur un calendrier.
Table des matières #
- Ce qu’un certificat HTTPS fait vraiment
- Le problème du certificat auto-signé
- Une autorité de certification, c’est quoi ?
- Pourquoi pas simplement Let’s Encrypt ?
- step-ca : une CA privée, automatisée
- L’architecture retenue
- Déploiement de step-ca
- Le sidecar de renouvellement automatique
- Le parcours d’obstacles (et ce qu’il apprend)
- Un certificat wildcard pour *.igeo
- Faire confiance à la CA sur les postes clients
- Ce qui tourne maintenant, au quotidien
- Conclusion
Ce qu’un certificat HTTPS fait vraiment #
Avant de parler de CA privée, il faut être au clair sur ce que fait un certificat, parce que la confusion la plus commune est de penser qu’il ne sert qu’à “chiffrer”. En réalité, un certificat HTTPS répond à deux questions séparées, et c’est la confusion entre les deux qui rend ce sujet difficile à appréhender :
- Est-ce que la communication est chiffrée ? — personne en chemin (le réseau de l’agglo, un Wi-Fi public, un FAI) ne peut lire ce qui transite.
- Est-ce que je parle vraiment au bon serveur ? — je ne suis pas en train de parler à un imposteur qui se fait passer pour
limesurvey.vienne-condrieu-agglomeration.fr.
Un certificat auto-signé, ou même un certificat valide, peut répondre “oui” à la première question tout en laissant la seconde grande ouverte. C’est cette seconde question qui est difficile à résoudre, et c’est elle qui justifie toute la mécanique de ce billet.
graph LR
A["Client (navigateur)"] -->|"1. Chiffrement du tuyau"| B["Le trafic est illisible en chemin"]
A -->|"2. Authentification du serveur"| C["Le certificat a-t-il été signé
par une autorité de confiance ?"]
C -->|Oui| D["🔒 Cadenas vert, aucun avertissement"]
C -->|Non| E["⚠️ Avertissement navigateur"]
style D fill:#d1fae5,stroke:#059669,color:#000
style E fill:#fee2e2,stroke:#dc2626,color:#000
Comment un navigateur répond-il à la question 2 ? Par une signature cryptographique. Le certificat que le serveur présente contient une signature, produite par la clé privée d’une autorité de certification. Le navigateur vérifie cette signature à l’aide de la clé publique de cette autorité — qu’il doit connaître à l’avance. S’il ne la connaît pas, il ne peut pas vérifier la signature, et il affiche un avertissement plutôt que de faire confiance aveuglément.
Le problème du certificat auto-signé #
Un certificat “auto-signé”, c’est un certificat qui se signe lui-même — l’autorité de certification et le serveur ne font qu’un. Ça chiffre très bien (question 1 résolue), mais ça ne répond à rien pour la question 2 : n’importe qui peut générer un certificat auto-signé pour limesurvey.vienne-condrieu-agglomeration.fr, y compris un attaquant. Le navigateur n’a aucun moyen de distinguer le vrai serveur d’un faux, donc il prévient l’utilisateur au lieu de trancher à sa place.
C’est pour ça qu’un certificat auto-signé déclenche systématiquement l’écran rouge “Votre connexion n’est pas privée” — pas parce que le chiffrement serait cassé, mais parce que personne d’autre que le serveur lui-même ne garantit son identité.
Une autorité de certification, c’est quoi ? #
Une CA (Certificate Authority), c’est une entité qui possède une paire de clés (une privée, gardée secrète ; une publique, largement distribuée) et qui accepte de signer les certificats d’autres serveurs avec sa clé privée. En signant un certificat, elle dit en substance : “j’ai vérifié que ce certificat appartient bien à ce nom de domaine, et j’engage ma réputation là-dessus.”
Le navigateur, lui, embarque une longue liste de CA publiques auxquelles il fait confiance par défaut (DigiCert, Let’s Encrypt/ISRG, Sectigo…). C’est ce magasin de confiance préinstallé qui permet à n’importe quel site public en HTTPS de fonctionner sans configuration côté client.
Le problème pour un service interne comme limesurvey.vienne-condrieu-agglomeration.fr : aucune de ces CA publiques ne va signer un certificat pour un serveur qu’elle ne peut pas vérifier publiquement de façon simple. Il existe deux façons de contourner ça — Let’s Encrypt avec un challenge particulier, ou monter sa propre CA. On regarde les deux.
Pourquoi pas simplement Let’s Encrypt ? #
C’est une vraie option, pas un mauvais choix — juste un choix avec un compromis différent.
Let’s Encrypt peut émettre un certificat pour un nom de domaine public (vienne-condrieu-agglomeration.fr en est un) même si le serveur qu’il protège n’est pas exposé sur Internet, à condition d’utiliser le challenge DNS-01 : on prouve la maîtrise du domaine en publiant un enregistrement DNS temporaire, pas en exposant le serveur au challenge HTTP-01 classique.
- Let's Encrypt (DNS-01)
- CA privée (step-ca)
Avantage principal : la racine ISRG de Let’s Encrypt est déjà dans tous les magasins de confiance du monde. Zéro déploiement côté client — chaque poste, chaque navigateur fait déjà confiance à cette autorité sans rien installer.
Contrepartie : il faut un accès à l’API DNS de la zone publique pour automatiser le challenge, et la durée de vie (90 jours aujourd’hui, appelée à se réduire encore par la suite selon les décisions du CA/Browser Forum) impose un renouvellement automatisé — mais ça reste un cycle de plusieurs mois, pas de plusieurs heures.
Avantage principal : aucune dépendance à une API DNS publique, aucune dépendance à Internet pour émettre un certificat — tout se passe sur le réseau interne. Les certificats peuvent avoir une durée de vie extrêmement courte (24h par défaut chez step-ca), ce qui réduit fortement la fenêtre d’exposition en cas de compromission d’une clé.
Contrepartie : chaque poste client doit apprendre à faire confiance à cette CA — un déploiement à faire une fois, sur tout le parc.
step-ca : une CA privée, automatisée #
step-ca, développé par Smallstep, est un serveur qui joue le rôle de CA privée. Il génère lui-même sa paire de clés racine, et surtout, il sait parler ACME — le même protocole que Let’s Encrypt utilise pour automatiser l’émission et le renouvellement des certificats. Ce n’est donc pas juste “une CA maison bricolée” : c’est un outil pensé dès le départ pour l’automatisation, avec le même niveau d’outillage que ce qu’offre l’écosystème public.
Deux notions à distinguer avant d’aller plus loin :
- Le certificat racine (
root_ca.crt) : c’est LA pièce de confiance. Une fois qu’un poste le connaît, il fait confiance à tout ce que cette CA signera, pour toujours (ou jusqu’à expiration de la racine elle-même — dix ans par défaut). C’est le fichier à distribuer une fois, sur chaque poste. - Les certificats serveur (ceux que Traefik présente aux visiteurs) : signés par la racine (via une CA intermédiaire, bonne pratique standard), ils ont une durée de vie très courte et sont renouvelés automatiquement, sans jamais toucher aux postes clients.
graph TB
ROOT["🔑 Root CA
(root_ca.crt — 10 ans)
distribué UNE FOIS sur les postes"]
INT["🔑 CA Intermédiaire
signe les certificats serveur au quotidien"]
SRV["📜 Certificat serveur
*.igeo.vienne-condrieu-agglomeration.fr
(24h — renouvelé automatiquement)"]
ROOT -->|signe| INT
INT -->|signe| SRV
style ROOT fill:#dbeafe,stroke:#2563eb,color:#000
style INT fill:#fef3c7,stroke:#d97706,color:#000
style SRV fill:#d1fae5,stroke:#059669,color:#000
C’est cette séparation qui fait tout l’intérêt du système : un poste qui connaît la racine fera automatiquement confiance à n’importe quel certificat signé par cette CA, y compris ceux qui n’existent pas encore aujourd’hui.
L’architecture retenue #
Traefik était déjà en place, en mode “provider fichier” : il lit un certificat .crt/.key déclaré dans un fichier de règles, et le recharge à chaud dès que ces fichiers changent sur disque. Cette architecture rend l’intégration de step-ca particulièrement simple : remplacer la source du fichier de certificat, sans toucher au reste de la configuration Traefik.
graph TB
subgraph SIGDOCKER25
SC["step-ca
(la CA elle-même, port 9000)"]
SCR["step-cert-renewer
(sidecar : demande + renouvelle)"]
TR["Traefik
(sert le HTTPS aux visiteurs)"]
VOL[("./traefik/certs/
.crt + .key")]
end
CLIENT["Poste client
(root_ca.crt importé)"]
SCR -->|"1. demande un certificat"| SC
SC -->|"2. signe et retourne"| SCR
SCR -->|"3. écrit sur disque"| VOL
VOL -->|"4. lu et surveillé"| TR
CLIENT -->|"5. HTTPS, valide car
root_ca.crt fait confiance"| TR
style SC fill:#dbeafe,stroke:#2563eb,color:#000
style SCR fill:#fef3c7,stroke:#d97706,color:#000
style TR fill:#d1fae5,stroke:#059669,color:#000
style CLIENT fill:#fce7f3,stroke:#db2777,color:#000
Deux conteneurs Docker font tout le travail :
step-ca: le serveur de CA lui-même.step-cert-renewer: un petit conteneurstep-cli, qui demande un premier certificat au démarrage, puis reste en tâche de fond (--daemon) pour le renouveler automatiquement avant expiration.
Traefik, lui, ne sait même pas que step-ca existe. Il continue de lire un fichier .crt/.key à un chemin fixe, exactement comme avec l’ancien certificat commercial.
Déploiement de step-ca #
Tout tourne en Docker Compose, à côté de la stack Traefik existante :
[igeo@SIGDOCKER25:frontend-stack] $ pwd
/opt/docker/frontend-stack
[igeo@SIGDOCKER25:frontend-stack] $ tree -L 2 .
.
├── docker-compose.yml
├── step-ca
│ ├── docker-compose.yml
│ ├── secrets
│ └── step
└── traefik
├── certs
├── config.yml
├── docker-compose.yml
├── extra
├── logs
└── rules
8 directories, 4 files
[igeo@SIGDOCKER25:frontend-stack] $La première étape : générer le mot de passe qui protège la clé privée de la CA — le fichier le plus sensible de toute cette installation.
# /opt/docker/frontend-stack/step-ca/secrets/password
mkdir -p secrets step
openssl rand -base64 32 > secrets/password
chmod 600 secrets/passwordPuis le service lui-même :
# /opt/docker/frontend-stack/step-ca/docker-compose.yml
services:
step-ca:
container_name: step-ca
image: smallstep/step-ca:latest
restart: unless-stopped
ports:
- "9000:9000"
volumes:
- ./step:/home/step
- ./secrets/password:/home/step/secrets/password
environment:
DOCKER_STEPCA_INIT_NAME: "VCA Internal CA"
DOCKER_STEPCA_INIT_DNS_NAMES: "step-ca.vienne-condrieu-agglomeration.fr,step-ca,localhost,10.10.22.102"
DOCKER_STEPCA_INIT_PROVISIONER_NAME: "admin"
DOCKER_STEPCA_INIT_PASSWORD_FILE: "/home/step/secrets/password"
networks:
- frontendAu tout premier démarrage sur un dossier ./step vide, le conteneur s’auto-initialise : il génère la racine, l’intermédiaire, un provisioner d’administration par défaut, et écrit tout ça dans ./step/. Ce dossier — clés comprises — est à sauvegarder avec la même rigueur qu’une base de données : le perdre, c’est perdre la CA et devoir tout redistribuer sur les postes.
DOCKER_STEPCA_INIT_PASSWORD_FILE : sans cette variable, l’entrypoint du conteneur génère lui-même un mot de passe aléatoire, l’affiche une seule fois dans les logs, et tente de le déplacer vers secrets/password — ce qui échoue puisque ce chemin est un fichier monté depuis l’hôte (immuable de l’intérieur). Résultat : les clés sont chiffrées avec un mot de passe qui n’est écrit nulle part, donc perdu. Au redémarrage suivant, step-ca tente de déchiffrer ces clés avec notre fichier secrets/password — qui ne correspond plus. Le message d’erreur (x509: decryption password incorrect) ne ment pas, mais ne dit pas non plus où chercher : c’est cette variable d’environnement, présente dès le premier lancement, qui évite le problème.
Le sidecar de renouvellement automatique #
# /opt/docker/frontend-stack/step-ca/docker-compose.yml (suite)
step-cert-renewer:
container_name: step-cert-renewer
image: smallstep/step-cli:latest
restart: unless-stopped
depends_on:
- step-ca
volumes:
- ../traefik/certs:/certs
- ./step/certs:/step-ca-certs:ro
- ./secrets/password:/provisioner-password:ro
entrypoint:
- /bin/sh
- -c
- |
step ca certificate igeo.vienne-condrieu-agglomeration.fr \
/certs/wildcard.igeo.vienne-condrieu-agglomeration.fr.crt \
/certs/wildcard.igeo.vienne-condrieu-agglomeration.fr.key \
--ca-url https://step-ca:9000 \
--root /step-ca-certs/root_ca.crt \
--provisioner-password-file /provisioner-password \
--san igeo.vienne-condrieu-agglomeration.fr \
--san "*.igeo.vienne-condrieu-agglomeration.fr" \
--force &&
step ca renew /certs/wildcard.igeo.vienne-condrieu-agglomeration.fr.crt /certs/wildcard.igeo.vienne-condrieu-agglomeration.fr.key \
--ca-url https://step-ca:9000 \
--root /step-ca-certs/root_ca.crt \
--daemon --force
networks:
- frontendTrois points méritent une explication, parce qu’ils ne sont pas évidents en lisant juste ce fichier :
--root /step-ca-certs/root_ca.crt au lieu d’un step ca bootstrap avec une empreinte (fingerprint). Normalement, la première fois qu’un client contacte une CA, il doit vérifier son identité via une empreinte SHA-256 fournie hors bande — c’est le mécanisme qui évite qu’un attaquant présente un faux certificat racine lors de ce tout premier contact. Ici, ce n’est pas nécessaire : les deux conteneurs tournent sur la même machine, et le sidecar lit directement le fichier root_ca.crt généré par step-ca sur le disque hôte, sans jamais avoir à le télécharger via le réseau. Pas de premier contact “à l’aveugle” à sécuriser.
--provisioner-password-file : au même titre que les clés de la CA, le compte “provisioner” (celui qui autorise la demande de certificat) est lui aussi protégé par un mot de passe. Sans ce flag, step tente d’ouvrir un prompt interactif pour le demander — ce qui échoue immédiatement en conteneur détaché (error allocating terminal: open /dev/tty).
--san "*.igeo.vienne-condrieu-agglomeration.fr" : c’est le SAN (Subject Alternative Name) qui rend le certificat wildcard — valable pour n’importe quel sous-domaine d’un seul niveau sous igeo.. C’est ce qui permet d’ajouter de nouveaux services (traefik.igeo..., dns.igeo...) sans jamais avoir à régénérer de certificat.
Le parcours d’obstacles (et ce qu’il apprend) #
Ce déploiement n’a pas marché du premier coup — loin de là. Plutôt que de gommer ce détail, ça vaut le coup de le documenter : chaque erreur rencontrée est un piège assez générique pour ressurgir sur d’autres projets Docker.
Le fil des erreurs, dans l’ordre où elles sont apparues :
entrypoint: >(style YAML “folded”) transforme tous les retours à la ligne en espaces. Or un script shell qui utilise\en fin de ligne pour continuer sur la suivante casse complètement si ce retour à la ligne devient un simple espace —\(backslash-espace) ne veut pas dire la même chose que\<retour-à-la-ligne>pour un shell. Le correctif :entrypoint: |-(style “literal”), qui préserve les vrais retours à la ligne.- Même en
|-, Docker Compose re-tokenise lui-même une chaîneentrypoint, ce qui peut encore casser la logique de continuation shell. Le correctif définitif : écrireentrypointcomme une liste YAML ([/bin/sh, -c, <script>]) plutôt qu’une chaîne — dans ce cas, Compose ne retouche plus rien, le script arrive intact. - Une erreur
lookup step-ca on 127.0.0.11:53: server misbehavinga d’abord semblé être un problème de résolution DNS interne à Docker. En réalité, c’était un symptôme :step-calui-même redémarrait en boucle, donc il n’y avait tout simplement rien de stable à joindre en face. Toujours vérifier l’état du service cible avant de creuser le réseau. open /home/step/secrets/root_ca_key: permission denied— l’image officielle tourne avec un utilisateur non-root (UID 1000). Le dossier./stepsur l’hôte doit lui appartenir, sans quoi le conteneur ne peut pas écrire ses propres clés.- Même après avoir corrigé les permissions du dossier parent, l’erreur persistait : Docker recrée automatiquement les sous-dossiers manquants d’un bind-mount, en root, quand il doit y accrocher un fichier monté séparément (ici
secrets/password). Il faut créer soi-même ce sous-dossier à l’avance, avec les bons droits, avant que Docker n’ait la moindre chance de le faire à sa place.
Chacune de ces étapes a un point commun : le message d’erreur décrit un symptôme technique précis, mais jamais la cause réelle. C’est normal — et c’est aussi pour ça que ce genre de déploiement se documente mieux après coup, une fois la cause connue, que pendant qu’on la cherche.
Un certificat wildcard pour *.igeo #
Une fois la mécanique stable, la question suivante était : plutôt que de lister explicitement chaque sous-domaine (apicarto., monitoring., storage.…) à chaque fois qu’un nouveau service apparaît, autant migrer vers un sous-domaine dédié igeo.vienne-condrieu-agglomeration.fr, couvert par un unique certificat wildcard.
# extrait de /opt/docker/frontend-stack/step-ca/docker-compose.yml (entrypoint step-cert-renewer)
--san igeo.vienne-condrieu-agglomeration.fr \
--san "*.igeo.vienne-condrieu-agglomeration.fr" \Le premier SAN couvre le domaine nu (igeo.vienne-condrieu-agglomeration.fr, sans rien devant), le second couvre tout sous-domaine d’un seul niveau (traefik.igeo..., dns.igeo...). C’est une règle standard du format X.509, pas une particularité de step-ca : un wildcard *.igeo. ne descend qu’à un seul niveau, api.traefik.igeo... (deux niveaux) ne serait pas couvert.
Migrer les routers Traefik existants vers ce nouveau sous-domaine se fait service par service, en éditant chaque Host() dans les fichiers de règles concernés — Traefik recharge chaque fichier à la volée, donc rien n’empêche de migrer un service, de vérifier qu’il fonctionne, puis de passer au suivant.
Faire confiance à la CA sur les postes clients #
C’est l’étape qui, dans les faits, demande le plus de coordination — parce qu’elle touche tout le parc, pas juste le serveur.
“Faire confiance à step-ca”, concrètement, ça veut dire : avoir une copie du fichier root_ca.crt installée dans le magasin de certificats de chaque poste. Rien de plus mystérieux que ça — c’est exactement ce que font déjà les navigateurs avec les CA publiques, sauf que pour une CA privée, personne ne l’a fait à l’avance.
- Windows (via GPO)
- Firefox
- Linux
Sur un parc joint à un domaine Active Directory, la méthode qui passe à l’échelle est la stratégie de groupe :
Configuration ordinateur → Stratégies → Paramètres Windows → Paramètres de sécurité → Stratégies de clé publique → Autorités de certification racines de confiance
Importer root_ca.crt dans cette GPO, la lier à l’OU des postes concernés, puis un gpupdate (ou l’attente du cycle normal) propage la confiance à tout le parc d’un coup — y compris aux futurs postes qui rejoindront le domaine.
Firefox a son propre magasin de certificats, indépendant de celui de Windows — Chrome et Edge lisent le magasin système, Firefox non, par défaut. La GPO ci-dessus ne suffit donc pas pour lui.
Deux options :
- Importer
root_ca.crtdirectement dans Firefox (about:preferences#privacy→ Certificats → Afficher les certificats → Autorités → Importer, en cochant “faire confiance à cette AC pour identifier des sites web”). - Activer la politique
security.enterprise_roots.enabled(viaabout:configpour un test, ou via un modèle ADMX Firefox pour tout le parc), qui dit à Firefox d’aller lire le magasin de certificats du système — le même que celui alimenté par la GPO Windows.
Sur les machines Linux du parc (Docker hosts, WSL, etc.) :
# fichier copié : /usr/local/share/ca-certificates/root_ca.crt
sudo cp root_ca.crt /usr/local/share/ca-certificates/
sudo update-ca-certificatesSi /usr/local/share/ca-certificates/ n’existe pas (No such file or directory), c’est ce paquet qui est manquant — installez-le, le dossier apparaît avec :
sudo apt install ca-certificatesSur une distro RHEL/CentOS/Rocky, le mécanisme diffère : paquet ca-certificates installé via sudo dnf install ca-certificates, dossier /etc/pki/ca-trust/source/anchors/ et commande update-ca-trust extract.
root_ca.crt fait automatiquement confiance à tout ce que cette CA signera par la suite — parce que chaque nouveau certificat serveur est signé par la même clé intermédiaire, elle-même signée par cette même racine. Exactement le même principe qu’avec Let’s Encrypt : le navigateur ne réimporte jamais rien à chaque renouvellement, la racine ISRG qu’il connaît déjà suffit à valider chaque nouveau certificat qui arrive.
Ce qui tourne maintenant, au quotidien #
Une fois les deux conteneurs stables, le cycle de vie devient entièrement invisible :
sequenceDiagram
participant SCR as step-cert-renewer
participant SC as step-ca
participant Disk as ./traefik/certs/
participant TR as Traefik
loop Toutes les ~24h
SCR->>SC: Demande de renouvellement
SC-->>SCR: Nouveau certificat signé
SCR->>Disk: Écrase .crt / .key
Disk-->>TR: Fichier modifié détecté
TR->>TR: Recharge à chaud, aucune coupure
end
Seuls les fichiers wildcard.igeo.vienne-condrieu-agglomeration.fr.crt et .key bougent dans ce cycle. root_ca.crt — celui importé une fois sur les postes — reste parfaitement statique.
Conclusion #
Ce qu’il faut retenir :
- Un certificat répond à deux questions distinctes — chiffrement et authentification — et c’est la seconde qui justifie toute la mécanique des autorités de certification.
- step-ca sépare une racine stable (des années de durée de vie, distribuée une fois) de certificats serveur éphémères (24h, renouvelés automatiquement) — le poste client n’a jamais besoin de suivre ce second cycle.
- L’intégration à une stack Traefik existante ne demande aucun changement de configuration côté Traefik : seule la source des fichiers
.crt/.keychange. - La distribution du certificat racine reste la seule étape qui touche tout le parc — et Firefox, avec son magasin de certificats séparé de celui de Windows, mérite une attention particulière.