Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018BAUeCFpDkRD6tU5wGsc1C
311 lines
12 KiB
Markdown
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)**
|