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

12 KiB

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

# 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

# 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

export KUBECONFIG=~/.kube/xpeditis-prod.yaml
cd infra/prod
DB_PUBLIC_IP=<ip_db> bash scripts/preflight-check.sh

Contrôles manuels complémentaires

# 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

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

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.

# 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.


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