xpeditis2.0/docs/mise-en-prod/13-securite-durcissement.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

311 lines
12 KiB
Markdown

# 13 — Sécurité
**Durée : environ 3 h.** C'est le document à relire avant chaque revue et après
chaque incident.
---
## Rotation obligatoire avant la mise en production
`infra/preprod/docker-stack.preprod.yml` est **versionné dans Git** et contient
en clair :
| Secret | Nature | Action |
|---|---|---|
| Mot de passe PostgreSQL preprod | interne | Ne jamais réutiliser en prod. Changer aussi en preprod. |
| Mot de passe Redis preprod | interne | Idem. |
| `JWT_SECRET` preprod | interne | Idem. |
| Identifiants MinIO preprod | interne | Idem. |
| **Clé SMTP Brevo** | **tiers, active** | **Révoquer immédiatement.** |
| Clés Stripe de test | tiers, test | Faire tourner par précaution. |
| Mot de passe admin Grafana | interne | Changer. |
Ils sont dans l'historique Git : les retirer du fichier ne les efface pas.
Toute personne ayant eu accès au dépôt — actuel ou ancien collaborateur, fork,
sauvegarde, outil d'analyse — les possède.
### La clé Brevo d'abord
C'est le seul identifiant qui donne un pouvoir **hors de votre infrastructure** :
envoyer des e-mails signés `noreply@xpeditis.com`. Un hameçonnage envoyé depuis
votre domaine à vos propres clients est un scénario nettement plus grave qu'un
accès à une base de preprod.
```
1. Brevo → SMTP & API → révoquer la clé publiée
2. Créer une clé PRODUCTION → Secret Kubernetes
3. Créer une clé PREPROD distincte → stack preprod
4. Vérifier les envois : Brevo → Statistiques → Transactionnel
```
### Puis les autres
```bash
# Preprod : nouveaux mots de passe partout, puis migration du stack vers SOPS
# Production : les secrets sont déjà générés à neuf en 06-secrets-sops.md
```
### Empêcher la récidive
```bash
# Détection de secrets dans le dépôt
brew install gitleaks
gitleaks detect --source . --verbose
# Crochet de pré-commit
cat > .git/hooks/pre-commit <<'EOF'
#!/bin/sh
if command -v gitleaks >/dev/null; then
gitleaks protect --staged --redact || {
echo "Un secret a ete detecte dans les fichiers indexes. Commit refuse."
exit 1
}
fi
EOF
chmod +x .git/hooks/pre-commit
```
Migrez également la preprod vers SOPS, sur le modèle de la production.
---
## 1. Modèle de menace
Ce à quoi une plateforme B2B de réservation maritime est réellement exposée :
| Menace | Vraisemblance | Impact | Ce qui la contre |
|---|---|---|---|
| Balayage automatisé / force brute SSH | permanente | faible | Clé uniquement, fail2ban, firewall par IP |
| Réutilisation d'identifiants clients | élevée | fort | Argon2, limitation de débit à 3 niveaux, jetons courts |
| Fuite de secret par le dépôt | **avérée** | **fort** | SOPS, gitleaks, rotation |
| Injection SQL | faible | critique | TypeORM paramétré, `class-validator` |
| Documents accessibles sans autorisation | moyenne | fort | Bucket privé, URLs pré-signées |
| Déni de service | moyenne | moyen | Cloudflare, limitation Traefik |
| Rançongiciel sur db-01 | faible | **critique** | Snapshots côté fournisseur (hors de portée du serveur) |
| Compromission de la chaîne CI | faible | critique | Environnement protégé, clé SSH restreinte, promotion d'images |
| Erreur humaine | **élevée** | fort | PITR, confirmations explicites, `preflight-check.sh` |
L'erreur humaine et la fuite de secret sont les deux plus probables. C'est là
que porte l'essentiel des mesures.
---
## 2. Défenses en place, par couche
### Réseau
- Firewall Hetzner : SSH et 6443 sur vos IP ; 80/443 sur les IP Cloudflare.
- db-01 : **aucun port applicatif public**. Réseau privé uniquement.
- UFW sur les deux nœuds (le firewall Hetzner ne filtre pas le réseau privé).
- `NetworkPolicy` k8s : refus par défaut, sortie Internet sans les plages
privées — un pod compromis ne peut pas balayer `10.10.0.0/16`.
- Firewall CI vide au repos, ouvert deux minutes pour une seule IP.
### Transport
- TLS 1.2 minimum, HSTS 1 an, Full (strict) chez Cloudflare.
- PostgreSQL : `hostssl` uniquement, une connexion en clair est refusée.
- Certificats ECDSA, rotation de clé à chaque renouvellement.
### Système
- SSH par clé, root interdit, 3 tentatives, algorithmes modernes.
- fail2ban, bannissement permanent des récidivistes.
- Mises à jour de sécurité automatiques, redémarrage nocturne si nécessaire.
- auditd sur les fichiers sensibles et les commandes root.
- Compte root verrouillé (la console Hetzner ne donne donc pas de session).
### Kubernetes
- `Secret` chiffrés au repos (`--secrets-encryption`).
- Journal d'audit de l'API, 30 jours, sans jamais journaliser le contenu des
`Secret`.
- Pod Security Admission `restricted` : pas de root, pas de privilèges, pas de
capability.
- Quotas et `LimitRange` : une fuite mémoire n'emporte pas Traefik.
- Tableau de bord Traefik désactivé.
### Application
- Argon2 pour les mots de passe.
- JWT 15 min / rafraîchissement 7 j, cookies `httpOnly`.
- Helmet, CORS en liste blanche stricte (pas de joker, `credentials: true`).
- Limitation de débit à trois niveaux indépendants : Cloudflare, Traefik, NestJS.
- Swagger désactivé en production.
- Journaux expurgés (`authorization`, `x-api-key`, mots de passe).
### Données
- Bucket privé, clés d'accès cloisonnées application / sauvegardes.
- Sauvegardes chiffrées côté client (libsodium et age) : un bucket qui fuite ne
livre rien.
- PITR à 5 minutes.
---
## 3. Revue avant ouverture
```bash
export KUBECONFIG=~/.kube/xpeditis-prod.yaml
cd infra/prod
DB_PUBLIC_IP=<ip_db> bash scripts/preflight-check.sh
```
### Contrôles manuels complémentaires
```bash
# 1. Rien d'exposé sur db-01 (depuis un AUTRE réseau)
nmap -Pn -p- --min-rate 1000 <db_public_ipv4>
# 2. Rien d'inattendu sur app-01
nmap -Pn -p- --min-rate 1000 <app_public_ipv4>
# 3. Qualité TLS
# https://www.ssllabs.com/ssltest/analyze.html?d=app.xpeditis.com → A ou A+
# 4. En-têtes de sécurité
# https://securityheaders.com/?q=https%3A%2F%2Fapp.xpeditis.com → A ou A+
# 5. Comptes de démonstration morts
for c in admin manager user; do
echo -n "$c: "
curl -s -o /dev/null -w '%{http_code}\n' -X POST https://api.xpeditis.com/api/v1/auth/login \
-H 'Content-Type: application/json' \
-d "{\"email\":\"$c@xpeditis.com\",\"password\":\"Password123!\"}"
done
# Attendu : 401 trois fois
# 6. Un seul administrateur
ssh deploy@<db_ip> 'cd /opt/xpeditis/data-node && sudo docker compose exec -T -u postgres postgres \
psql -d xpeditis_prod -c "SELECT email, role, is_active FROM users WHERE role = '"'"'ADMIN'"'"';"'
# 7. Documents inaccessibles sans autorisation
curl -sI https://fsn1.your-objectstorage.com/xpeditis-prod-documents/ | head -1 # 403
# 8. Aucun secret en clair dans le dépôt
gitleaks detect --source . --verbose
# 9. Une route protégée refuse l'anonyme
curl -s -o /dev/null -w '%{http_code}\n' https://api.xpeditis.com/api/v1/bookings # 401
```
### Analyse des images
```bash
brew install trivy
trivy image --severity HIGH,CRITICAL rg.fr-par.scw.cloud/weworkstudio/xpeditis-backend:latest
trivy image --severity HIGH,CRITICAL rg.fr-par.scw.cloud/weworkstudio/xpeditis-frontend:latest
```
Les images `node:20-alpine` remontent régulièrement des CVE de la bibliothèque
système. Traitez les CRITICAL avec vecteur réseau ; les autres peuvent attendre
la prochaine reconstruction. Reconstruisez les images **au moins une fois par
mois** pour absorber les correctifs de base.
---
## 4. Ce que cette configuration ne couvre pas
Soyez lucide sur les limites :
| Non couvert | Conséquence | Quand le traiter |
|---|---|---|
| **Panne d'un nœud** | app-01 en panne = site hors ligne (1 h de reconstruction). | Phase 2 : second nœud + Load Balancer. |
| **PostgreSQL non redondé** | Panne de db-01 = coupure jusqu'à restauration (< 2 h). | Phase 2 : standby en réplication. |
| **Panne du datacenter fsn1** | Coupure jusqu'à reconstruction ailleurs. | Phase 3 : bi-région. |
| **2FA sur les comptes utilisateurs** | Une réutilisation d'identifiants suffit. Le schéma a une colonne `totp_secret` inutilisée. | Dans les 3 mois. |
| **Analyse de vulnérabilité en continu** | Trivy est manuel. | Ajouter à la CI quand possible. |
| **SOC 2** | Bloquant pour les grands comptes. | Quand un client l'exige (Vanta, ~800 €/mois). |
| **Test d'intrusion externe** | Aucun regard extérieur. | Avant les premiers clients importants (3 à 8 k€). |
| **Documents non versionnés** | Une suppression est définitive. | [12 § 7](./12-sauvegardes-restauration.md#7--ce-qui-nest-pas-sauvegarde-et-pourquoi) |
---
## 5. Réponse à incident
### 5.1 Suspicion de compromission
**Ne redémarrez rien.** Un redémarrage détruit les preuves en mémoire et
l'attaquant a probablement déjà un moyen de revenir.
```bash
# 1. ISOLER — bloquer tout le trafic entrant sauf votre IP
cat > /tmp/isolement.json <<JSON
[{"direction":"in","protocol":"tcp","port":"22","source_ips":["<votre_ip>/32"]}]
JSON
hcloud firewall replace-rules xpeditis-prod-fw-app --rules-file /tmp/isolement.json
# Le site devient injoignable : c'est l'effet recherché. Pour ne couper que le
# trafic public sans perdre l'accès, préférez le mode « Under Attack » de
# Cloudflare, qui laisse vos IP passer.
# 2. CONSTATER
ssh deploy@<app_ip> '
last -20 # connexions récentes
sudo journalctl -t xpeditis-ssh-deploy -n 100
sudo ausearch -k rootcmd -ts today | tail -50
sudo grep -i "accepted" /var/log/auth.log | tail -30
'
kubectl -n xpeditis-prod get events --sort-by=.lastTimestamp
ssh deploy@<app_ip> 'sudo grep -E "\"verb\":\"(create|delete|patch)\"" /var/log/k3s/audit.log | tail -50'
# 3. PRÉSERVER
ssh deploy@<app_ip> 'sudo tar czf /tmp/preuves.tgz /var/log/auth.log* /var/log/k3s/audit.log* /var/log/audit/'
scp deploy@<app_ip>:/tmp/preuves.tgz ./incident-$(date +%F).tgz
# 4. ÉVALUER : la base a-t-elle été touchée ?
ssh deploy@<db_ip> 'cd /opt/xpeditis/data-node && sudo docker compose logs postgres | grep -iE "connection authorized|FATAL" | tail -50'
```
### 5.2 Compromission avérée
```
1. Couper l'accès public (firewall, ou Cloudflare en mode « Under Attack »)
2. Faire tourner TOUS les secrets (06-secrets-sops.md)
3. Révoquer les jetons tiers : Scaleway, Hetzner, Cloudflare, Stripe, Brevo
4. Reconstruire app-01 à partir de zéro — ne jamais « nettoyer » un serveur
5. Restaurer la base à un instant ANTÉRIEUR à la compromission (PITR)
6. Forcer la déconnexion de tous les utilisateurs (rotation du JWT_SECRET)
7. Analyser les journaux : quelles données ont été consultées ?
8. Notifier la CNIL sous 72 h si des données personnelles sont concernées (16)
9. Informer les clients concernés
```
> **Point 4 : reconstruire, pas nettoyer.** Un serveur compromis ne se
> désinfecte pas — on ne peut jamais prouver l'absence de porte dérobée.
> L'architecture est faite pour cela : app-01 est sans état, sa reconstruction
> prend une heure.
### 5.3 Fuite de données personnelles
Le RGPD impose une notification à la CNIL **sous 72 heures** à compter de la
prise de connaissance. Voir [16 § Violation de données](./16-rgpd-conformite.md).
---
## 6. Entretien
| Fréquence | Action |
|---|---|
| **Hebdomadaire** | Revue Grafana (erreurs, latence, disque). Vérifier que le firewall CI est vide. Vérifier le résultat de la vérification de sauvegarde. |
| **Mensuelle** | `trivy` sur les images, reconstruire pour absorber les correctifs de base. Relire les nouveaux comptes ADMIN. `gitleaks detect`. |
| **Trimestrielle** | `refresh-cloudflare-ips.sh`. Test de restauration PITR complet. Revue des accès (Hetzner, Cloudflare, GitHub, Stripe). Mise à jour de k3s. |
| **Annuelle** | Rotation `JWT_SECRET`, mots de passe base et Redis, clé age. Revue du modèle de menace. Test d'intrusion externe si le chiffre d'affaires le permet. |
---
## 7. Contrôle
```
[ ] Clé SMTP Brevo publiée RÉVOQUÉE, nouvelle clé de production en place
[ ] Mots de passe base / Redis / JWT / MinIO de preprod changés
[ ] Mot de passe admin Grafana changé
[ ] gitleaks : aucun secret détecté
[ ] Crochet de pré-commit gitleaks installé
[ ] preflight-check.sh : aucun point bloquant
[ ] nmap depuis l'extérieur : db-01 entièrement filtré
[ ] SSL Labs ≥ A
[ ] securityheaders.com ≥ A
[ ] Comptes de démonstration : 401 sur les trois
[ ] Un seul ADMIN actif, le vôtre
[ ] Bucket documents : 403 en anonyme
[ ] trivy : aucune CRITICAL exploitable à distance
[ ] Procédure de réponse à incident lue et comprise
[ ] Entretien planifié dans l'agenda
```
→ **Suite : [14 — Runbook de mise en ligne](./14-runbook-go-live.md)**