documentation
Kohinos v3 Doc générée le commit 8d60b1904

Runbooks d'exploitation — incidents type & procédures

Un runbook est une procédure éprouvée — pas un brainstorming. Il décrit comment réagir à un incident connu, étape par étape, dans l'ordre, sans avoir à réfléchir à 3 h du matin. La règle d'or : tenir cette page à jour APRÈS chaque incident (post-mortem), et tester chaque procédure en staging avant de la laisser ici. Un runbook obsolète est pire que pas de runbook.

⚠ Document opérationnel sensible

Cette page est marquée noindex,nofollow et ne doit pas être publiée sur l'Internet ouvert. Elle décrit des procédures qui touchent à la base financière. À héberger sur le wiki interne ou le réseau privé de l'association.

Index des runbooks

IDSévéritéTitreDernière révision
RB-01mediumWebhook HelloAsso non traité2026-05-19
RB-02highSolde adhérent incohérent2026-05-19
RB-03highSEPA en double prélèvement2026-05-19
RB-04highSSA redistribution échouée mid-flight2026-05-19
RB-05highMandat SEPA annulé mais prélèvement parti2026-05-19
RB-06criticalServer down (nginx / php-fpm / mariadb)2026-05-19
RB-07criticalDB corruption / crash2026-05-19
RB-08medium-highCron qui ne tourne plus2026-05-19
RB-09highDisque plein2026-05-19
RB-10criticalCompromission soupçonnée2026-05-19
RB-11highMigration Doctrine échouée en prod2026-05-19
RB-12highIBAN chiffré illisible2026-05-19
⛔ Règle absolue — intégrité financière

JAMAIS toucher la BDD financière en SQL direct sans backup vérifié et test reproduisant l'incident. Un UPDATE hâtif sur operation_*, account_*, flux ou sepa_* peut créer ou détruire silencieusement de la monnaie locale. C'est la priorité n°1 du projet (cf. ../dev/invariants.html).


RB-01 — Webhook HelloAsso non traité

Sévérité : medium · Domaine : HelloAsso / adhésion / Flux

Symptômes

Vérifications

# 1. Le webhook est-il arrivé jusqu'à nous ?
grep "helloasso" var/log/prod.log | tail -50
# ou via MCP : monolog-search term="helloasso" channel="webhook"

# 2. L'enregistrement existe-t-il en table helloasso ?
mysql kohinos -e "SELECT id, helloassoid, type, montant, created_at \
    FROM helloasso ORDER BY id DESC LIMIT 10;"

# 3. Y a-t-il un Flux pour ce helloassoid ?
mysql kohinos -e "SELECT f.id, f.reference, f.created_at \
    FROM flux f WHERE f.reference LIKE '%HELLOASSO%' ORDER BY f.id DESC LIMIT 10;"

# 4. La signature HMAC du webhook est-elle vérifiée ?
grep -E "(signature|HMAC|webhook)" var/log/prod.log | tail -20

Procédure

  1. Identifier la cause : signature invalide, secret rotatif désync, endpoint 4xx, code applicatif qui rejette.
  2. Rejouer le webhook manuellement :
    # Récupérer le payload depuis HelloAsso (back-office HA → onglet notifications)
    curl -X POST https://kohinos.example.org/webhook/helloasso \
        -H "Content-Type: application/json" \
        -H "X-HelloAsso-Signature: <sig>" \
        --data @payload.json
  3. Vérifier l'idempotence : un seul Flux par helloassoid. Cf. ../dev/webhooks.html pour les invariants.
  4. Si le retry échoue, contacter le support HelloAsso (ils peuvent rejouer côté serveur).

Prévention


RB-02 — Solde adhérent incohérent

Sévérité : HIGH — toute incohérence BDD financière est priorité absolue.

Symptômes

Vérifications

# 1. Détection automatique
php bin/console kohinos:accounts:check

# 2. Recalcul manuel pour un compte donné
mysql kohinos -e "
    SELECT a.id, a.balance,
           SUM(CASE WHEN o.account_to_id = a.id THEN o.montant ELSE 0 END) AS in_sum,
           SUM(CASE WHEN o.account_from_id = a.id THEN o.montant ELSE 0 END) AS out_sum
    FROM account_user a
    LEFT JOIN operation_user o ON o.account_to_id = a.id OR o.account_from_id = a.id
    WHERE a.id = <ID>
    GROUP BY a.id, a.balance;"

# 3. Tracer les modifs récentes via audit_log
mysql kohinos -e "SELECT * FROM audit_log \
    WHERE object_class LIKE 'App\\\\Entity\\\\Account%' \
    AND object_id = <ID> ORDER BY logged_at DESC LIMIT 20;"

Procédure

  1. ⛔ NE PAS toucher la BDD à la main. Un UPDATE balance cache la cause sans la corriger.
  2. Identifier la transaction divergente via audit_log + flux (la divergence apparaît à un timestamp précis).
  3. Geler l'écriture sur ce compte si possible (mode lecture seule applicatif).
  4. Re-jouer le calcul agrégé : la somme des opérations donne la vérité.
  5. Si correction nécessaire : écrire une commande Symfony dédiée (idempotente, audit log, validation trésorier) ou une migration. Jamais d'UPDATE direct.
  6. Écrire un test reproduisant la divergence avant le fix.

Prévention


RB-03 — SEPA en double prélèvement

Sévérité : HIGH — impact direct sur le compte bancaire de l'adhérent.

Symptômes

Vérifications

# 1. État de l'idempotence
mysql kohinos -e "SELECT id, mandate_reference, last_processed_month \
    FROM sepa_mandate WHERE id = <ID>;"
# last_processed_month doit avoir été mis à jour AVANT la génération

# 2. Doublons sur le mois courant
mysql kohinos -e "
    SELECT sepa_mandate_id, DATE_FORMAT(created_at, '%Y%m') AS month, COUNT(*)
    FROM recurring_purchase_request
    GROUP BY sepa_mandate_id, month
    HAVING COUNT(*) > 1;"

# 3. Audit log de la commande sepa
grep "kohinos:sepa:process-monthly" var/log/prod.log | tail -50

Procédure

  1. Geler la commande SEPA (kohinos:sepa:process-monthly) jusqu'à fix.
  2. Identifier les doublons via la requête ci-dessus.
  3. Si le fichier SEPA n'est pas encore envoyé à la banque : régénérer après suppression du doublon, valider en double avec le trésorier.
  4. Si le débit est déjà parti :
    • Rembourser manuellement l'adhérent (virement SEPA out).
    • Annuler les opérations Kohinos correspondantes via une commande dédiée (cf. règle RB-02 — pas d'UPDATE direct).
  5. Corriger l'idempotence : vérifier que lastProcessedMonth est mis à jour dans la même transaction Doctrine que la création du RecurringPurchaseRequest.
  6. Écrire un test reproduisant le scénario (commande lancée 2× sur le même mois → 1 seule requête générée).

Communication

Communication OBLIGATOIRE

Mail explicatif à l'adhérent dans les 24 h + notification du trésorier de l'association. Si l'incident touche plus de 10 adhérents, prévenir la fédération.

Prévention


RB-04 — SSA redistribution échouée mid-flight

Sévérité : HIGH — partial state = conservation rompue.

Symptômes

Vérifications

# 1. État des items SSA
mysql kohinos -e "SELECT id, solidoume_parameter_id, last_month_payed \
    FROM solidoume_item WHERE solidoume_parameter_id = <ID> \
    ORDER BY id;"

# 2. Conservation : somme avant / après
mysql kohinos -e "
    SELECT SUM(montant) FROM operation_user
    WHERE reference LIKE 'Solidoume prélèvement %' AND DATE(created_at) = CURDATE();
    SELECT SUM(montant) FROM operation_prestataire
    WHERE reference LIKE 'Solidoume %' AND DATE(created_at) = CURDATE();"
# Les deux sommes doivent être EXACTEMENT égales

# 3. Logs d'exécution
grep "kohinos:ssa:execute" var/log/prod.log | tail -100

Procédure

  1. Geler immédiatement (ne pas relancer la commande).
  2. Évaluer l'état : la commande doit être atomique (transaction Doctrine). Si elle ne l'est pas, c'est le bug à corriger en priorité.
  3. Cas A — rollback possible : les modifications sont annulées par la transaction. Vérifier solidoume_item.last_month_payed non incrémenté → relancer en --test puis en réel.
  4. Cas B — commit partiel : situation à éviter à tout prix.
    • Identifier les écritures déjà passées.
    • Écrire une commande de compensation (annulation des opérations partielles).
    • NE PAS faire d'UPDATE direct sur les balances.
    • Re-jouer avec --force-date=YYYY-MM-DD après fix.
  5. Vérifier conservation Σ entrées = Σ sorties.
  6. Post-mortem obligatoire + test d'intégration reproduisant l'échec mid-flight.

Prévention


RB-05 — Mandat SEPA annulé mais prélèvement parti

Sévérité : HIGH — débit non autorisé = R-transaction garantie + plainte.

Symptômes

Vérifications

# 1. Statut du mandat au moment du process
mysql kohinos -e "SELECT id, mandate_reference, status, cancelled_at, last_processed_month \
    FROM sepa_mandate WHERE id = <ID>;"
# status doit être 'active' au moment du process. Si 'cancelled', bug.

# 2. Requêtes récurrentes restées actives
mysql kohinos -e "SELECT * FROM recurring_purchase_request \
    WHERE sepa_mandate_id = <ID> AND status != 'cancelled';"

Procédure

  1. Geler la commande SEPA si elle tourne encore.
  2. Si le débit n'est pas parti → retirer la ligne du fichier pain.008, re-signer, ré-envoyer.
  3. Si le débit est parti → remboursement immédiat (virement out), annuler les opérations Kohinos via commande dédiée.
  4. Corriger le code : l'annulation d'un mandat doit, dans la même transaction, annuler atomiquement toutes les RecurringPurchaseRequest et tous les RecurringSubscription liés (cf. règle métier dans CLAUDE.md).
  5. Test d'intégration : annulation mandat → assertions sur l'absence de débit suivant.
  6. Communication adhérent + trésorier obligatoire.

Prévention


RB-06 — Server down (nginx / php-fpm / mariadb)

Sévérité : critical — indisponibilité totale.

Vérifications

# 1. État des services
sudo systemctl status nginx php8.4-fpm mariadb

# 2. Logs récents
sudo journalctl -xeu nginx --since "10 min ago"
sudo journalctl -xeu php8.4-fpm --since "10 min ago"
sudo journalctl -xeu mariadb --since "10 min ago"

# 3. Erreurs nginx applicatives
sudo tail -100 /var/log/nginx/error.log
sudo tail -100 /var/log/nginx/kohinos_error.log

# 4. OOM killer ?
sudo dmesg | grep -i -E "(killed|out of memory)" | tail -20

Procédure

  1. Restart séquentiel (du bas vers le haut) :
    sudo systemctl restart mariadb
    sleep 5
    sudo systemctl restart php8.4-fpm
    sudo systemctl restart nginx
  2. Vérifier la santé en HTTP :
    curl -sS -o /dev/null -w "%{http_code}\n" https://kohinos.example.org/
  3. Si OOM sur php-fpm : vérifier les batches Doctrine ($em->clear() manquant ?), réduire pm.max_children temporairement.
  4. Si mariadb refuse de redémarrer → voir RB-07.
  5. Si nginx refuse de redémarrer : sudo nginx -t pour vérifier la conf.

Prévention


RB-07 — DB corruption / crash

Sévérité : critical — risque de perte de données financières.

Symptômes

Vérifications

# 1. Diagnostic
sudo mariadb-check --all-databases
sudo tail -200 /var/log/mysql/error.log

# 2. Espace disque (cause fréquente)
df -h
ls -lh /var/lib/mysql/

# 3. État des binlogs (CRITIQUE — ne pas les perdre)
ls -lh /var/lib/mysql/*-bin.*

Procédure

  1. Stopper l'application immédiatement (mode maintenance nginx).
  2. NE PAS PERDRE LES BINLOGS : ils permettent le point-in-time recovery.
    sudo cp -a /var/lib/mysql/*-bin.* /var/backups/binlogs-$(date +%F)/
  3. Dump avant tout fix :
    sudo mariadb-dump --all-databases --single-transaction --routines --triggers \
        --master-data=2 > /var/backups/pre-fix-$(date +%F-%H%M).sql
    Si mariadb-dump échoue, copier /var/lib/mysql à froid (DB stoppée).
  4. Tentative de réparation :
    sudo mariadb-check --auto-repair --all-databases
    # ou en cas d'InnoDB corruption, mode recovery :
    # innodb_force_recovery=1 dans /etc/mysql/mariadb.conf.d/50-server.cnf
    # puis remonter par paliers (1 → 6), dump, restore propre
  5. Si réparation impossible : restauration depuis backup (cf. backup-restore) + replay binlogs.
  6. Vérification post-restore :
    php bin/console kohinos:accounts:check
    php bin/console doctrine:schema:validate
    # Comparer les soldes globaux avant/après
  7. Si écart financier : bloquer toute opération, communiquer trésorier, audit complet.

Prévention


RB-08 — Cron qui ne tourne plus

Sévérité : medium à high selon le job (SEPA/SSA = high, nettoyage cache = low).

Symptômes

Vérifications

# 1. Crontab effectif
sudo crontab -l -u kohinos
sudo crontab -l -u www-data
cat /etc/cron.d/kohinos 2>/dev/null

# 2. Service cron actif
sudo systemctl status cron

# 3. Historique d'exécution
mysql kohinos -e "SELECT command, started_at, finished_at, status \
    FROM cron_report ORDER BY started_at DESC LIMIT 30;"

# 4. Lockfile resté en place ?
ls -la var/lock/ /var/lock/kohinos-*

Procédure

  1. Identifier le job concerné — voir cron.html pour la liste.
  2. Si lockfile résiduel (crash précédent qui ne l'a pas relâché) :
    sudo rm /var/lock/kohinos-sepa-process-monthly.lock
    # puis replay manuel
    sudo -u www-data php bin/console kohinos:sepa:process-monthly --test
    # si OK, sans --test
  3. Si cron daemon down : sudo systemctl restart cron.
  4. Si permissions : vérifier que l'utilisateur cron peut écrire dans var/log et var/cache.
  5. Replay manuel du job manqué avec --force-date=YYYY-MM-DD si applicable.

Prévention


RB-09 — Disque plein

Sévérité : high — provoque crash MariaDB, échec d'écritures Symfony.

Vérifications

# 1. Vue d'ensemble
df -h
df -i  # inodes (cause souvent oubliée)

# 2. Coupables habituels
sudo du -sh /var/www/kohinos/var/log
sudo du -sh /var/www/kohinos/var/cache
sudo du -sh /var/lib/mysql
sudo du -sh /var/log/nginx /var/log/mysql
sudo du -sh /var/backups

Procédure

  1. Faire de la place sans rien casser :
    # Logs applicatifs > 30 jours (garder les logs financiers : sepa/, ssa/, audit/)
    sudo find /var/www/kohinos/var/log -name "*.log" -mtime +30 \
        -not -path "*/sepa/*" -not -path "*/ssa/*" -not -path "*/audit/*" -delete
    
    # Cache Symfony prod (à régénérer ensuite)
    sudo -u www-data rm -rf /var/www/kohinos/var/cache/prod/*
    sudo -u www-data php /var/www/kohinos/bin/console cache:warmup --env=prod
    
    # Sessions expirées
    sudo find /var/www/kohinos/var/sessions -mtime +7 -delete
    
    # Logs nginx
    sudo logrotate -f /etc/logrotate.d/nginx
  2. Backups anciens : NE PAS supprimer aveuglément. Conserver au moins : dernier daily × 7, dernier weekly × 4, dernier monthly × 12.
  3. Vérifier que la rotation de logs (logrotate) fait son travail : cat /etc/logrotate.d/kohinos.
  4. Si MariaDB binlogs prennent tout : PURGE BINARY LOGS BEFORE NOW() - INTERVAL 14 DAY; (seulement après backup vérifié).

Prévention


RB-10 — Compromission soupçonnée

Sévérité : critical — RGPD, fonds, réputation.

Symptômes

Procédure (dans cet ordre, sans hésiter)

  1. Isoler immédiatement :
    # Bloquer tout sauf l'IP admin
    sudo ufw default deny incoming
    sudo ufw allow from <IP_ADMIN> to any port 22
    sudo ufw allow from <IP_ADMIN> to any port 443
    sudo ufw enable
    
    # Couper l'app au public (maintenance mode)
    sudo systemctl stop nginx
  2. Rotation IMMÉDIATE de tous les secrets :
    • APP_SECRET (Symfony) → déconnecte toutes les sessions.
    • Password MariaDB (SET PASSWORD FOR kohinos@localhost = PASSWORD('...');) + DATABASE_URL.
    • Clé de chiffrement IBAN (KOHINOS_IBAN_SECRET) — déclenche RB-12.
    • Clés Ed25519 SEPA (libsodium).
    • Tokens HelloAsso / PayZen (révocation côté provider + nouveaux).
    • Mots de passe SMTP (MAILER_DSN).
    • Clés SSH du serveur.
  3. Dump volume forensic AVANT toute remédiation :
    sudo dd if=/dev/sda of=/mnt/forensic/disk-$(date +%F).img bs=4M status=progress
    # ou snapshot LVM / hyperviseur
  4. Audit logs :
    mysql kohinos -e "SELECT * FROM audit_log WHERE logged_at > NOW() - INTERVAL 30 DAY \
        ORDER BY logged_at DESC LIMIT 500;"
    sudo last -50
    sudo grep "Accepted" /var/log/auth.log | tail -100
  5. Identifier le vecteur d'entrée (CVE non patché ? credentials volés ? injection ?).
  6. Restauration depuis un backup antérieur à la compromission (cf. backup-restore) sur une nouvelle machine.
  7. Communication obligatoire :
    • Trésorier + bureau de l'association.
    • Si données personnelles touchées : notification CNIL sous 72 h (article 33 RGPD).
    • Information des adhérents si risque pour leurs droits et libertés.
  8. Post-mortem détaillé + plan d'action sécurité (durcissement, MFA, patch policy).

Prévention


RB-11 — Migration Doctrine échouée en prod

Sévérité : HIGH — état schéma incohérent = app KO.

Symptômes

Vérifications

# 1. État des migrations
php bin/console doctrine:migrations:status --env=prod
php bin/console doctrine:migrations:list --env=prod

# 2. Diff schéma réel vs entités
php bin/console doctrine:schema:validate --env=prod

# 3. Quelle migration ?
mysql kohinos -e "SELECT * FROM doctrine_migration_versions ORDER BY executed_at DESC LIMIT 5;"

Procédure

  1. Stopper le trafic (maintenance mode nginx).
  2. Backup immédiat avant toute manipulation (le DB est dans un état précis, on veut pouvoir y revenir).
  3. Tentative de rollback :
    # Reculer d'une version
    php bin/console doctrine:migrations:migrate prev --env=prod --no-interaction
    
    # Ou viser une version précise
    php bin/console doctrine:migrations:migrate <version> --env=prod --no-interaction
  4. Si le rollback échoue (down() non implémenté correctement) : restore depuis backup pre-deploy (cf. backup-restore).
  5. Corriger la migration en dev : tester sur copie de prod (mysqldump → copie locale) avant de relancer.
  6. Écrire un test d'intégration sur la migration corrigée.
  7. Redéployer.

Prévention


RB-12 — IBAN chiffré illisible

Sévérité : high — empêche tout prélèvement SEPA.

Symptômes

Vérifications

# 1. La clé courante est-elle bien chargée ?
php bin/console debug:dotenv | grep -i iban
php bin/console secrets:list | grep -i iban

# 2. Tester le déchiffrement sur un compte connu
php bin/console app:migrate-iban-encryption --dry-run | head -20

# 3. Comparer les IBAN chiffrés (longueur, prefix) avant/après
mysql kohinos -e "SELECT id, LENGTH(iban_encrypted), \
    LEFT(iban_encrypted, 16) FROM sepa_mandate LIMIT 5;"

Procédure

  1. Récupérer l'ancienne clé (gestionnaire de secrets, backup chiffré). Si perdue, restauration depuis backup pre-rotation obligatoire.
  2. Configurer une double-validation temporaire : l'app essaye la nouvelle clé, en cas d'échec retombe sur l'ancienne (variable KOHINOS_IBAN_SECRET_PREVIOUS).
  3. Lancer la re-chiffrement de tous les IBAN avec la nouvelle clé :
    php bin/console app:migrate-iban-encryption --dry-run
    # Vérifier la sortie, puis :
    php bin/console app:migrate-iban-encryption
  4. Une fois 100 % migré et vérifié → retirer KOHINOS_IBAN_SECRET_PREVIOUS et révoquer définitivement l'ancienne clé.
  5. Test SEPA en mode --test avant de relancer un cycle réel.

Prévention


Conventions de rédaction d'un runbook

Tout nouveau runbook doit suivre la même structure :

  1. Titre : RB-XX — <phrase courte décrivant l'incident>.
  2. Sévérité : low / medium / high / critical. Indique la priorité d'intervention et l'obligation de communication.
  3. Symptômes : ce que l'on voit (utilisateur, logs, monitoring). Permet l'identification rapide.
  4. Vérifications : commandes copy/paste-friendly, sans risque (lecture seule). Confirment le diagnostic.
  5. Procédure : liste numérotée d'actions, dans l'ordre. Chaque étape doit être atomique et idempotente quand possible.
  6. Prévention : ce qu'on met en place pour que l'incident ne revienne pas (monitoring, test, invariant).
  7. Post-mortem : lien (à créer après l'incident).

Post-mortem — exigence

Post-mortem obligatoire pour tout incident high/critical

Format : blameless (le but est d'améliorer le process, pas de désigner un responsable). Stocké dans docs/postmortems/YYYY-MM-DD-titre.md.

Structure type d'un post-mortem :

Voir aussi