xpeditis2.0/docs/mise-en-prod/06-secrets-sops.md
David b22f4e0b74 docs: procedure de mise en production pas a pas
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018BAUeCFpDkRD6tU5wGsc1C
2026-09-07 21:40:50 +02:00

252 lines
8.1 KiB
Markdown

# 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
```bash
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.
```bash
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.
```bash
shred -u /tmp/xpeditis-backup-key.txt
```
### Déclarer les deux clés
```bash
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
```bash
cp k8s/base/03-secrets.template.yaml /tmp/secrets.yaml
$EDITOR /tmp/secrets.yaml
```
### Générer les valeurs
```bash
# 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
```bash
cd infra/prod
sops -e /tmp/secrets.yaml > k8s/base/03-secrets.sops.yaml
shred -u /tmp/secrets.yaml
```
### Vérifier avant de commiter
```bash
# 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
```
```bash
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
```bash
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.
```bash
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 :
```bash
kubectl apply -f k8s/cluster/cluster-issuer.yaml
kubectl get clusterissuer
```
---
## 6. Modifier un secret
```bash
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
```bash
# 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 :
```bash
# 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](./07-stockage-objet-s3.md)**