# 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@ 'cd /opt/xpeditis/data-node && \ sudo docker compose exec -T -u postgres postgres \ psql -c "ALTER ROLE xpeditis WITH PASSWORD '"'"''"'"';"' # 2. Le fichier .env.data de db-01 (postgres-exporter l'utilise) ssh deploy@ 'sudo $EDITOR /opt/xpeditis/data-node/.env.data' ssh deploy@ '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 > 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)**