Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018BAUeCFpDkRD6tU5wGsc1C
8.1 KiB
06 — Secrets (SOPS + age)
Durée : environ 1 h.
1. Pourquoi SOPS et pas autre chose
| Approche | Pourquoi elle a été écartée |
|---|---|
| Secrets en clair dans le YAML | C'est ce que fait la preprod aujourd'hui. Résultat : mot de passe de base, JWT_SECRET et clé SMTP Brevo publiés dans l'historique Git. |
| Sealed Secrets | Bonne solution, mais elle ne chiffre que des Secret Kubernetes. Le fichier .env.data de db-01 (hors cluster) resterait sans solution. |
| Vault / OpenBao | Excellent, et une pièce d'infrastructure de plus à héberger, sauvegarder et desceller après chaque redémarrage. Disproportionné ici. |
| Secrets GitHub uniquement | Le dépôt deviendrait le coffre-fort de la production. Un jeton d'accès compromis livrerait les mots de passe de la base. |
SOPS + age couvre les deux mondes avec une seule clé, sans composant à héberger, et produit des différentiels lisibles en revue (seules les valeurs sont chiffrées, pas les noms de clés).
2. Créer les clés
Clé principale
mkdir -p ~/.config/sops/age
age-keygen -o ~/.config/sops/age/keys.txt
chmod 600 ~/.config/sops/age/keys.txt
grep 'public key' ~/.config/sops/age/keys.txt
# public key: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
Clé de secours
Ne sautez pas cette étape. Sans elle, perdre votre poste signifie perdre définitivement l'accès à tous les secrets de production.
age-keygen -o /tmp/xpeditis-backup-key.txt
cat /tmp/xpeditis-backup-key.txt
Trois destinations, au moins deux d'entre elles :
- imprimée sur papier, dans un coffre ou un lieu physique distinct ;
- sur une clé USB chiffrée, rangée ailleurs que votre poste ;
- dans un gestionnaire de mots de passe différent de celui du quotidien.
shred -u /tmp/xpeditis-backup-key.txt
Déclarer les deux clés
cd infra/prod
$EDITOR .sops.yaml
Remplacez AGE_RECIPIENT_PRIMARY et AGE_RECIPIENT_BACKUP par les deux clés
publiques (age1…).
3. Composer les secrets
cp k8s/base/03-secrets.template.yaml /tmp/secrets.yaml
$EDITOR /tmp/secrets.yaml
Générer les valeurs
# JWT_SECRET — minimum 32 caractères (validé par Joi au démarrage)
openssl rand -base64 64 | tr -d '\n'
# DOCUMENT_PASSWORD_SECRET — minimum 16
openssl rand -base64 32 | tr -d '\n'
Correspondances à ne pas manquer
| Secret Kubernetes | Doit être identique à |
|---|---|
DATABASE_PASSWORD |
POSTGRES_PASSWORD de /opt/xpeditis/data-node/.env.data |
REDIS_PASSWORD |
REDIS_PASSWORD du même fichier |
Une divergence produit un backend qui démarre puis boucle en
CrashLoopBackOff avec password authentication failed.
Trois pièges
1. Les identifiants de tarif Stripe. Le code lit
STRIPE_SILVER_*, STRIPE_GOLD_*, STRIPE_PLATINIUM_*. Le stack de preprod
définit STRIPE_STARTER_*, STRIPE_PRO_*, STRIPE_ENTERPRISE_* — des noms
que personne ne lit, si bien qu'en preprod ces identifiants sont en réalité
absents. Utilisez les noms attendus par le code, ceux du gabarit.
2. DOCUMENT_PASSWORD_SECRET. S'il est vide, le code retombe sur
JWT_SECRET. Une rotation ultérieure du JWT_SECRET rendrait alors illisibles
tous les documents transporteurs déjà émis. Définissez-le explicitement, et
ne le changez plus jamais sans plan de migration.
3. SWAGGER_USERNAME / SWAGGER_PASSWORD. Laissez-les vides. main.ts
désactive alors totalement /api/docs en production. Les renseigner expose la
documentation complète de l'API derrière une simple authentification Basic.
4. Chiffrer
cd infra/prod
sops -e /tmp/secrets.yaml > k8s/base/03-secrets.sops.yaml
shred -u /tmp/secrets.yaml
Vérifier avant de commiter
# Les noms de clés restent lisibles, les valeurs sont chiffrées
grep -A3 'DATABASE_PASSWORD' k8s/base/03-secrets.sops.yaml
# DATABASE_PASSWORD: ENC[AES256_GCM,data:...,tag:...]
# Aucune valeur en clair ne subsiste
grep -c 'ENC\[' k8s/base/03-secrets.sops.yaml # doit être > 20
# Le déchiffrement fonctionne
sops -d k8s/base/03-secrets.sops.yaml | head -20
make secrets-check # refuse tout fichier sensible non ignoré
git add k8s/base/03-secrets.sops.yaml
git commit -m "prod: secrets chiffres SOPS"
5. Appliquer sur le cluster
export KUBECONFIG=~/.kube/xpeditis-prod.yaml
make secrets-apply
Le déchiffré ne touche jamais le disque : sops écrit sur la sortie standard,
kubectl lit sur l'entrée standard.
kubectl -n xpeditis-prod get secrets
kubectl -n monitoring get secrets
kubectl -n cert-manager get secret cloudflare-api-token
Appliquez maintenant le ClusterIssuer, qui attendait ce secret :
kubectl apply -f k8s/cluster/cluster-issuer.yaml
kubectl get clusterissuer
6. Modifier un secret
sops k8s/base/03-secrets.sops.yaml # ouvre votre éditeur, rechiffre en sortie
make secrets-apply
# Indispensable : modifier un Secret ne redémarre pas les pods
kubectl -n xpeditis-prod rollout restart deploy/xpeditis-backend
7. Rotation
| Secret | Fréquence | Effet |
|---|---|---|
JWT_SECRET |
annuelle, ou immédiatement en cas de fuite | Déconnecte tous les utilisateurs. À faire hors heures ouvrées. |
DATABASE_PASSWORD |
annuelle | Changer d'abord dans PostgreSQL (ALTER ROLE), puis dans le Secret, puis redémarrer. Brève coupure. |
REDIS_PASSWORD |
annuelle | Vider le cache est sans conséquence, les sessions le sont davantage : prévoyez une déconnexion. |
STRIPE_SECRET_KEY |
sur incident | Rotation depuis le tableau de bord Stripe. |
| Clé SMTP Brevo | sur incident | À faire maintenant : celle de preprod est publiée. |
DOCUMENT_PASSWORD_SECRET |
jamais | Rendrait illisibles les documents déjà émis. |
| Clé age | tous les 2 ans | sops updatekeys sur tous les fichiers après avoir modifié .sops.yaml. |
Rotation du mot de passe PostgreSQL, sans surprise
# 1. Nouveau mot de passe côté base
ssh deploy@<db_ip> 'cd /opt/xpeditis/data-node && \
sudo docker compose exec -T -u postgres postgres \
psql -c "ALTER ROLE xpeditis WITH PASSWORD '"'"'<nouveau>'"'"';"'
# 2. Le fichier .env.data de db-01 (postgres-exporter l'utilise)
ssh deploy@<db_ip> 'sudo $EDITOR /opt/xpeditis/data-node/.env.data'
ssh deploy@<db_ip> 'cd /opt/xpeditis/data-node && \
sudo docker compose --env-file .env.data up -d postgres-exporter'
# 3. Le Secret Kubernetes
sops k8s/base/03-secrets.sops.yaml
make secrets-apply
kubectl -n xpeditis-prod rollout restart deploy/xpeditis-backend
# 4. La version chiffrée du .env.data
sops -e --input-type dotenv --output-type dotenv <fichier> > data-node/data-node.sops.env
8. Perte de la clé age
Si vous perdez la clé principale et la clé de secours, les secrets chiffrés
dans Git sont définitivement illisibles. La production continue de tourner (les
Secret sont déjà dans le cluster), mais vous ne pouvez plus les modifier ni
les redéployer ailleurs.
Récupération d'urgence, tant que le cluster fonctionne :
# Extraire les valeurs depuis le cluster
kubectl -n xpeditis-prod get secret xpeditis-backend-secrets -o json \
| jq -r '.data | to_entries[] | "\(.key)=\(.value|@base64d)"'
Puis recréez une paire de clés age et rechiffrez. Faites-le immédiatement : si le cluster tombe entre-temps, les secrets sont perdus.
9. Contrôle
[ ] Clé age principale créée, chmod 600
[ ] Clé de secours créée et rangée dans DEUX endroits physiques distincts
[ ] .sops.yaml renseigné avec les deux clés publiques
[ ] 03-secrets.sops.yaml créé, > 20 valeurs ENC[…]
[ ] DATABASE_PASSWORD et REDIS_PASSWORD identiques à ceux de db-01
[ ] Identifiants de tarif Stripe aux noms SILVER / GOLD / PLATINIUM
[ ] DOCUMENT_PASSWORD_SECRET défini explicitement
[ ] SWAGGER_USERNAME et SWAGGER_PASSWORD laissés vides
[ ] Secrets appliqués sur le cluster
[ ] ClusterIssuer letsencrypt-prod créé
[ ] make secrets-check passe
[ ] /tmp/secrets.yaml effacé avec shred
→ Suite : 07 — Stockage objet