Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018BAUeCFpDkRD6tU5wGsc1C
252 lines
8.1 KiB
Markdown
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)**
|