# DRILL — ANALYSE BRUNEL : L'ORAGE DE MINUIT
> Raisonnement par **charge maximale** appliqué. Je challenge chaque vecteur.
---
## RÉSUMÉ EXÉCUTIF
**Score d'exposition : 7/7 pièges identifiés** (6 réels + 1 faux positif partiel)
**Déclencheur racine :** Le certificat SSL non rechargé à 02h45 est la bombe à retardement — tout le reste est l'effet domino.
---
## CLASSIFICATION PAR SÉVÉRITÉ
---
### ⛔ P0 — PRODUCTION EN DANGER / PERTE DE DONNÉES
#### P0.1 — Certificat SSL non rechargé → 502 sur preprod + contamination prod
**Piège :** Certbot régénère le `.pem` mais Nginx continue à servir l'ancien certificat en mémoire. En HTTP/2, les sessions sont multiplexées et restent ouvertes — Nginx ne reload pas automatiquement lors d'un renouvellement fichier.
**Mécanisme exact :**
```
Certbot --deploy-hook manquant
→ Nouveau .pem sur disque, Nginx l'ignore
→ Worker Nginx sert l'ancien cert (en cache mémoire)
→ Clients HTTP/2 reçoivent un certificat invalide/expirant
→ 502 intermittents (1-2% = sessions déjà établies survivent)
```
**Contamination prod :** Le worker Nginx partagé (`ac_nginx`) gère `un client.fr` ET `preprod.un client.fr`. Si les worker processes sont surchargés par les retry SSL sur preprod, les requêtes prod subissent du délai (contention de workers, pas de partage de session SSL — voir P0-faux positif piège 7).
**Correction immédiate :**
```bash
# Vérification état réel du cert en mémoire vs disque
openssl s_client -connect preprod.un client.fr:443 </dev/null 2>/dev/null | grep "Serial Number"
openssl x509 -in /etc/letsencrypt/live/preprod.un client.fr/cert.pem -noout -serial
# Reload Nginx (SANS restart — zéro downtime)
nginx -t && nginx -s reload
```
**Fix pérenne — `/etc/letsencrypt/renewal/preprod.un client.fr.conf` :**
```ini
[renewalparams]
post_hook = nginx -t && nginx -s reload
```
**Biais Brunel activé :** Mon réflexe "charge maximale" me pousserait à déployer un cluster Nginx avec reload automatisé. La vraie solution : 2 lignes de config Certbot. Je le signale.
---
#### P0.2 — deploy-nuxt.sh appelé via webhook sans vérification de branche → écrasement
**Piège :** Un webhook mal configuré déclenche `deploy-nuxt.sh` en parallèle des opérations en cours. Si le script ne vérifie pas la branche active, il peut déployer du code `main` non testé sur la preprod — ou pire, du code preprod sur la prod si la cible est mal configurée.
**Vecteur :**
```
Webhook → deploy-nuxt.sh (sans guard)
→ git pull origin main (ou mauvaise branche)
→ PM2 reload avec code non testé
→ Si webhook pointe vers prod : CATASTROPHE
```
**Correction — guard obligatoire en tête de `deploy-nuxt.sh` :**
```bash
#!/bin/bash
set -euo pipefail
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
EXPECTED_BRANCH="${DEPLOY_BRANCH:-preprod}"
if [ "$CURRENT_BRANCH" != "$EXPECTED_BRANCH" ]; then
echo "ABORT: branche $CURRENT_BRANCH ≠ $EXPECTED_BRANCH attendue"
exit 1
fi
# Vérification verrou déploiement concurrent
LOCKFILE="/tmp/deploy-nuxt.lock"
if [ -f "$LOCKFILE" ]; then
echo "ABORT: déploiement en cours (PID $(cat $LOCKFILE))"
exit 1
fi
echo $$ > "$LOCKFILE"
trap "rm -f $LOCKFILE" EXIT
```
**Le webhook lui-même :** doit exiger un token HMAC + vérification de la branche dans le payload. Sans ça, n'importe quelle requête HTTP peut déclencher un deploy.
---
#### P0.3 — Corruption des sauvegardes MariaDB via volumes non démontés
**Piège :** `docker-compose down` sur preprod en cours d'écriture + `save-preprod.sh` qui tente un backup = corruption InnoDB. MariaDB n'a pas eu le temps de faire `fsync` de ses redo logs.
**Mécanisme :**
```
docker-compose down (SIGTERM → MariaDB)
→ Buffer pool non flushé si shutdown trop rapide
→ save-preprod.sh lance mysqldump sur un état incohérent
→ Backup corrompu (tables InnoDB en état "recovery needed")
```
**Test de corruption :**
```bash
# Vérifier intégrité après backup
mysqlcheck -u root -p --all-databases < /tmp/backup.sql
# Ou tenter un restore en isolation et vérifier
docker run --rm -v backup_vol:/var/lib/mysql mariadb:10.6 \
mariadb -u root -e "SELECT 1" 2>&1
```
**Fix `save-preprod.sh` :**
```bash
# AVANT backup : flush + lock propre
docker exec preprod_mariadb mariadb -u root -p"$MARIADB_ROOT_PASSWORD" \
-e "FLUSH TABLES WITH READ LOCK; FLUSH LOGS;"
# Utiliser --single-transaction pour InnoDB (pas de lock global)
docker exec preprod_mariadb mysqldump \
--single-transaction \
--routines \
--triggers \
--flush-logs \
-u root -p"$MARIADB_ROOT_PASSWORD" \
prestashop > /backups/preprod_$(date +%Y%m%d_%H%M%S).sql
# Unlock
docker exec preprod_mariadb mariadb -u root -p"$MARIADB_ROOT_PASSWORD" \
-e "UNLOCK TABLES;"
```
**Règle absolue :** `save-preprod.sh` doit TOUJOURS vérifier qu'aucun `docker-compose down` n'est en cours (check du PID ou status des containers) avant de lancer le backup.
---
### 🔴 P1 — PREPROD SÉVÈREMENT DÉGRADÉE / RISQUE PROPAGATION
#### P1.1 — Docker health checks inter-containers ignorés → PrestaShop démarre avant MariaDB
**Piège :** `docker-compose down && docker-compose up -d` relance tous les containers simultanément. PrestaShop démarre, tente de connecter MariaDB qui est encore en init → crash → restart loop.
**Le vrai problème :** `depends_on` sans `condition: service_healthy` ne garantit pas que MariaDB est *prête*, seulement qu'elle est *démarrée*.
**État actuel probable dans docker-compose.preprod.yml :**
```yaml
# INSUFFISANT
depends_on:
- preprod_mariadb
```
**Fix :**
```yaml
services:
preprod_mariadb:
healthcheck:
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
preprod_prestashop:
depends_on:
preprod_mariadb:
condition: service_healthy # ← garantit que MariaDB répond vraiment
restart: on-failure
deploy:
restart_policy:
condition: on-failure
delay: 5s
max_attempts: 3
```
**Détection :** `docker ps` montrant PrestaShop en `Restarting (1) X seconds ago` = symptôme classique.
---
#### P1.2 — Processus PM2 zombies après docker-compose down
**Piège :** PM2 tourne dans le container Nuxt. Quand Docker envoie SIGTERM au container, si l'entrypoint ne délègue pas proprement le signal à PM2, les workers Nuxt deviennent zombies (PPID=1, non récupérés par init).
**Symptôme :** `docker stats` montre 15% CPU résiduel après restart. `docker exec preprod_nuxt ps aux` montre des processus `node` en état `Z` (zombie).
**Mécanisme :**
```
docker stop preprod_nuxt → SIGTERM → PID 1 (PM2 daemon ou shell)
→ Si PID 1 = shell (CMD ["sh", "-c", "pm2-runtime ..."]) : shell reçoit SIGTERM, ne le transmet pas
→ Workers Nuxt continuent, ne reçoivent jamais SIGTERM
→ Docker attend 10s → SIGKILL → crash propre mais sans flush
```
**Fix Dockerfile :**
```dockerfile
# MAUVAIS (shell form, pas de propagation signal)
CMD ["sh", "-c", "pm2-runtime ecosystem.config.js"]
# CORRECT (exec form, PM2 = PID 1, reçoit les signaux)
CMD ["pm2-runtime", "ecosystem_preprod.config.js"]
```
**Fix ecosystem_preprod.config.js :**
```javascript
module.exports = {
apps: [{
name: 'preprod-nuxt',
script: '.output/server/index.mjs',
kill_timeout: 5000, // ← délai avant SIGKILL
wait_ready: true, // ← attend signal 'ready' avant de marquer UP
listen_timeout: 10000,
shutdown_with_message: true
}]
}
```
**Purge manuelle des zombies détectés :**
```bash
docker exec preprod_nuxt pm2 delete all
docker exec preprod_nuxt pm2-runtime ecosystem_preprod.config.js
```
---
#### P1.3 — Saturation CPU sur preprod_network (95% utilisation)
**Piège :** Docker applique des limites cgroups CPU par défaut (`cpu_quota=100000000ns / cpu_period=100000ms` = 1 CPU max). 500 requêtes HTTP/2 en 30 secondes vers `/module/*` (PrestaShop) + MariaDB en restart = saturation.
**Note technique :** La formulation du scénario est imprécise — ce n'est pas le "réseau" qui sature le CPU, mais les processus dans les containers sur le réseau `preprod_network` qui consomment le quota CPU cgroup. Je le note comme faux-ami terminologique, pas faux positif.
**Fix docker-compose.preprod.yml :**
```yaml
services:
preprod_nuxt:
deploy:
resources:
limits:
cpus: '0.75'
memory: 512M
reservations:
cpus: '0.25'
memory: 256M
preprod_prestashop:
deploy:
resources:
limits:
cpus: '0.5'
memory: 256M
preprod_mariadb:
deploy:
resources:
limits:
cpus: '0.5'
memory: 512M
```
**Monitoring :**
```bash
# Surveiller en temps réel
docker stats preprod_nuxt preprod_prestashop preprod_mariadb --format \
"table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.NetIO}}"
```
---
### 🟡 P2 — RISQUE MINEUR / TECHNIQUE
#### P2.1 — Faux positif partiel : Conflit de connexions Nginx entre prod et preprod
**Le piège énoncé :** "Nginx réutilise des connexions SSL corrompues entre production et préprod."
**Mon analyse :**
C'est **techniquement incorrect tel que formulé** — et c'est intentionnel dans le drill.
Les raisons :
1. **Nginx → containers : pas de SSL interne.** Les connexions upstream `ac_nuxt:3000` et `preprod_nuxt:3000` sont en HTTP simple (dans le réseau Docker). Il n'y a pas de SSL à "corrompre" côté upstream.
2. **Réseaux isolés.** `ac_network` et `preprod_network` sont des réseaux Docker séparés. Pas de partage de socket TCP entre eux.
3. **Vhosts séparés.** Les directives `proxy_pass` de chaque vhost Nginx sont indépendantes — pas de réutilisation de pool de connexions entre `un client.fr` et `preprod.un client.fr`.
**Ce qui est RÉEL (impact indirect) :** Si les workers Nginx sont tous occupés à retry des connexions 502 vers `preprod_nuxt`, les requêtes prod attendent en queue → latence prod (P1 par contamination de workers, pas P0 par corruption SSL). Cet effet a été traité dans P0.1.
**Verdict : FAUX POSITIF PARTIEL** — l'effe