# 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= 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 # 2. Rien d'inattendu sur app-01 nmap -Pn -p- --min-rate 1000 # 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@ '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 </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@ ' 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@ 'sudo grep -E "\"verb\":\"(create|delete|patch)\"" /var/log/k3s/audit.log | tail -50' # 3. PRÉSERVER ssh deploy@ 'sudo tar czf /tmp/preuves.tgz /var/log/auth.log* /var/log/k3s/audit.log* /var/log/audit/' scp deploy@:/tmp/preuves.tgz ./incident-$(date +%F).tgz # 4. ÉVALUER : la base a-t-elle été touchée ? ssh deploy@ '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)**