Produire la clé de chiffrement au premier démarrage

Le déploiement restait bloqué sur ENCRYPTION_KEY. J'ai tenu trop longtemps la
position « pas de valeur par défaut », en confondant deux choses : refuser une
clé livrée avec l'image — ce qui reste juste, une clé publiée dans un dépôt ne
protège rien — et exiger qu'un humain en fabrique une avant tout démarrage.

Une variable d'environnement n'est d'ailleurs pas un bon coffre : elle s'affiche
dans `docker inspect` et dans l'interface de gestion. Un fichier produit au
démarrage, dans un volume distinct de la base et des documents, n'est pas moins
protégé — et une sauvegarde de l'un n'emporte plus la clé de l'autre.

Le point d'entrée la produit donc si elle manque, l'écrit en 0600, et l'affiche
une fois dans les journaux avec ce qu'il faut en faire. Une clé fournie
explicitement l'emporte toujours : un déploiement qui gère ses secrets ailleurs
ne doit pas être contrarié. La pile démarre désormais sans aucune variable.

Éprouvé sur le script lui-même : première exécution, clé de 32 octets produite
et annoncée ; deuxième, reprise en silence depuis le fichier ; avec une clé
fournie, le fichier reste intact.

Corrigé au passage, révélé par un échec transitoire du test : une écriture
disque impossible — volume plein, droits, montage absent — remontait en
exception non traitée. L'écran restait muet, la pièce n'était pas déposée et
rien ne le disait.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cr9dkEHwbDgkWPnyGj1Rjv
This commit is contained in:
Claude committed 2026-08-09 09:41:24 +00:00
1 parent 68e0fd69f0
commit e5524c6299
6 files changed
+91 -19

No files matched your search

+12 -5
View File
@@ -52,11 +52,12 @@ services:
# superutilisateur contourne la row-level security, y compris déclarée en
# FORCE, et l'isolation ne reposerait plus que sur la couche applicative.
DATABASE_URL: postgresql://${APP_DB_USER:-planflow_app}:${APP_DB_PASSWORD:-planflow-app-interne}@db:5432/${POSTGRES_DB:-planflow}
# Seule variable sans valeur par défaut, et il n'y en aura pas : elle
# chiffre le NIR, l'IBAN et les arrêts de travail. Une clé livrée avec
# l'image serait connue de tous et le chiffrement ne protégerait plus
# rien. La produire : openssl rand -base64 32
ENCRYPTION_KEY: ${ENCRYPTION_KEY:?ENCRYPTION_KEY est requis. Produire une clé avec - openssl rand -base64 32}
# Laissée vide, elle est **produite au premier démarrage** et conservée
# dans le volume `secrets`. La renseigner ici reste possible pour un
# déploiement qui gère ses secrets par ailleurs — mais une variable
# d'environnement s'affiche dans `docker inspect` et dans l'interface de
# gestion, ce qui n'en fait pas un meilleur coffre qu'un fichier.
ENCRYPTION_KEY: ${ENCRYPTION_KEY:-}
APP_URL: ${APP_URL:-http://localhost:9317}
# Chemin **dans le volume**, pas dans l'image : les pièces du dossier
# salarié écrites dans la couche du conteneur disparaîtraient au premier
@@ -64,6 +65,9 @@ services:
DOCUMENT_STORE: /data/documents
volumes:
- documents:/data
# Volume distinct de la base et des documents : sauvegarder l'un ne doit
# pas emporter la clé qui déchiffre l'autre.
- secrets:/secrets
networks:
# `interne` pour joindre la base, `nginx_default` pour être joignable par
# le reverse-proxy — qui atteint le conteneur sur son port 3000, sans
@@ -98,6 +102,9 @@ networks:
volumes:
db-data:
# Clé de chiffrement produite au premier démarrage. À sauvegarder
# **séparément** du reste : réunis, le coffre et sa clé ne protègent plus rien.
secrets:
# Pièces du dossier salarié, chiffrées au repos avec ENCRYPTION_KEY. À
# sauvegarder avec la base : l'une sans l'autre restitue un dossier amputé,
# et sans la clé le volume est illisible.