Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018BAUeCFpDkRD6tU5wGsc1C
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é).
NetworkPolicyk8s : refus par défaut, sortie Internet sans les plages privées — un pod compromis ne peut pas balayer10.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 :
hostssluniquement, 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
Secretchiffré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