Aller au contenu

Runbooks on-call

Pour qui : SRE / astreinte / support N2 VitaKYC. Doctrine : chaque runbook doit permettre à un ingénieur non spécialiste de résoudre l’incident en suivant les étapes.


SevDéfinitionSLA réponseEscalade immédiate
SEV-1Service inaccessible pour ≥ 10 % des tenants OU perte de données confirmée OU violation de sécurité active5 minCTO + CEO + Compliance officer
SEV-2Fonctionnalité critique dégradée (KYC / AML / TCR impossible) OU > 2× SLO breach15 minTech Lead + CTO
SEV-3Dégradation partielle, contournement possible1 hTech Lead
SEV-4Impact mineur, remédiation planifiable1 j ouvré

Pour chaque incident : ouvrir un channel dédié #inc-YYYYMMDD-short-name, désigner un Incident Commander, un Communications Lead, et un Scribe. Horodater chaque action dans le channel.

NiveauRôleDélai max avant escalade
L1On-call engineer15 min de diagnostic sans progrès → L2
L2Tech Lead / Architecte30 min sans remédiation → L3
L3CTOSEV-1 dès confirmation
L4CEO + Compliance officerSEV-1 impactant clients ou régulateurs

Code alerteCondition PrometheusSev
ApiHighLatencyP95histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m])) > 0.6 pendant 5 minSEV-3
ApiErrorRateHighrate(http_requests_total{status=~"5.."}[5m]) / rate(http_requests_total[5m]) > 0.02SEV-2
OcrServiceDownup{job="ocr-svc"} == 0 pendant 2 minSEV-2
KafkaConsumerLagHighkafka_consumer_lag > 10000 pendant 10 minSEV-3
PostgresConnectionsExhaustedpg_stat_activity_count > 0.85 × max_connectionsSEV-2
PostgresReplicationLagpg_replication_lag_bytes > 100 MBSEV-2
RneBalanceLowrne_subscription_balance < 1000 pour un tenantSEV-4
IdesSubmissionFailuretcr_ides_submission_failed_total incrémentéSEV-3
AmlListIngestStaleaml_list_last_ingested_seconds_ago > 86400SEV-3
CertificateExpiringSooncert_expiry_days < 14SEV-4
AuditHashChainBrokenaudit_chain_integrity_failed_total > 0SEV-1
MassBiometricFailurerate(liveness_fail_total[15m]) > 10 × baselineSEV-1 (possible attaque)
LicenseQuotaExceededtenant_checks_month / license_limit > 0.95SEV-4

Symptômes : alerte ApiHighLatencyP95, Grafana API Latency ≥ 0,6 s.

Diagnostic en 5 minutes :

Fenêtre de terminal
# 1. Identifier le service coupable
kubectl top pods -n vitakyc | sort -k3 -r | head -10
# 2. Voir les 10 traces les plus lentes de la dernière heure
# Grafana Tempo → service.name=~"kyc-svc|aml-svc|tcr-svc"
# + duration > 600ms
# 3. Vérifier saturation DB
kubectl exec -n vitakyc deploy/postgres-primary -- psql -c \
"SELECT state, count(*) FROM pg_stat_activity GROUP BY state"
# 4. Vérifier file Kafka
kubectl exec -n vitakyc deploy/kafka-0 -- kafka-consumer-groups.sh \
--bootstrap-server localhost:9092 --describe --all-groups | \
awk '$5 > 1000 {print}'

Mitigations rapides :

  • Si saturation DB → scaler le pool HikariCP (ConfigMap + restart) OU activer read replica routing.
  • Si saturation CPU service métier → HPA monte en charge automatiquement ; vérifier limits et passer en quickFix : kubectl scale deployment/<svc> --replicas=+2.
  • Si downstream IA saturé (OCR) → activer fallback commercial via feature flag ocr.fallback_commercial.force=true.

Investigation approfondie :

  • Requêtes SQL lentes : pg_stat_statements → top 10 mean_exec_time.
  • Regex Java blowing up : dernier déploiement ? rollback via ArgoCD si corrélé.
  • Pic de trafic ? vérifier les tenants top sur le dashboard cost-per-check.

Symptômes : OcrServiceDown ; dossiers KYC bloqués en étape « OCR pending ».

Diagnostic :

Fenêtre de terminal
kubectl -n vitakyc get pods -l app=ocr-svc
kubectl -n vitakyc describe pod ocr-svc-xxx
kubectl -n vitakyc logs -l app=ocr-svc --tail=200
# Check GPU (si applicable)
kubectl -n vitakyc describe node <gpu-node>

Mitigations :

  1. Rebascule sur fallback commercial (feature flag) — immédiat :
Fenêtre de terminal
kubectl -n vitakyc set env deployment/kyc-svc \
OCR_PRIMARY=fallback_commercial OCR_FALLBACK_ENABLED=true

Les dossiers en ocr_pending redémarrent automatiquement via Temporal signal.

  1. Redémarrage propre du service interne :
Fenêtre de terminal
kubectl -n vitakyc rollout restart deployment/ocr-svc
kubectl -n vitakyc rollout status deployment/ocr-svc --timeout=3m
  1. Si pod en CrashLoopBackOff lié à la RAM → augmenter resources.limits.memory temporairement via ConfigMap et redéployer.

  2. Si lié à un pic de volume d’un tenant unique → activer rate limiting renforcé : kubectl -n vitakyc set env deployment/ocr-svc TENANT_RATE_LIMIT_DEFAULT=50 (50 rps au lieu de 200).

Post-incident :

  • Noter les dossiers impactés (SELECT case_id FROM kyc_case WHERE updated_at > '$incident_start' AND status = 'in_progress').
  • Webhook tenant kyc.case.delayed si temps de traitement > 10 min.

Symptômes : KafkaConsumerLagHigh, backlog de messages.

Diagnostic :

Fenêtre de terminal
# Identifier le groupe à la traîne
kubectl exec -n vitakyc deploy/kafka-0 -- kafka-consumer-groups.sh \
--bootstrap-server localhost:9092 --describe --all-groups
# Taille des topics
kubectl exec -n vitakyc deploy/kafka-0 -- kafka-log-dirs.sh \
--bootstrap-server localhost:9092 --describe --json | jq -r '.brokers[].logDirs[].partitions[] | "\(.partition) \(.size)"' | sort -k2 -nr | head

Mitigations :

  • Scale-out des consumers : kubectl scale deployment/<consumer-svc> --replicas=+3 — attention au nb de partitions (jamais > nb partitions sinon réplicas idle).
  • Si le ralentissement vient d’un downstream (DB, API externe) → traiter celui-ci d’abord.
  • Pour les batchs massifs (batch screening de 10 M subjects), augmenter temporairement les partitions du topic avec kafka-topics.sh --alter --partitions N.
  • Ne jamais purger un topic en production sans accord CTO (perte de messages = perte d’audit).

RB-004 · PostgreSQL connections exhausted (SEV-2)

Section intitulée « RB-004 · PostgreSQL connections exhausted (SEV-2) »

Symptômes : erreurs too many connections, services métier en 503.

Diagnostic :

-- Qui utilise les connexions ?
SELECT application_name, state, count(*)
FROM pg_stat_activity GROUP BY 1, 2 ORDER BY 3 DESC;
-- Requêtes bloquantes
SELECT pid, now() - pg_stat_activity.query_start AS duration, query, state
FROM pg_stat_activity
WHERE state != 'idle' AND now() - query_start > '30 seconds'::interval
ORDER BY duration DESC;

Mitigations :

  1. Tuer les requêtes bloquantes anciennes :
SELECT pg_terminate_backend(pid) FROM pg_stat_activity
WHERE state = 'idle in transaction'
AND now() - state_change > interval '5 minutes';
  1. Augmenter max_connections (nécessite redémarrage) — dernier recours.
  2. PgBouncer si pas déjà en place — configurer pool transaction mode.
  3. Vérifier qu’aucun service n’a un leak de connexions (métrique hikaricp_active_connections flat non décroissante).

RB-005 · RNE quota épuisé pour un tenant (SEV-4)

Section intitulée « RB-005 · RNE quota épuisé pour un tenant (SEV-4) »

Symptômes : erreur 400 du RNE « Le solde disponible est insuffisant » ; dossiers KYB en mode dégradé.

Diagnostic :

SELECT tenant_id, balance_remaining, updated_at
FROM rne_subscription
WHERE tenant_id = '<tenant>'
ORDER BY updated_at DESC LIMIT 1;

Mitigations :

  • Immédiat : activer le fallback documentaire (capture manuelle d’extrait RNE + OCR) pour ce tenant via feature flag : kubectl -n vitakyc set env deployment/connector-rne TENANT_<id>_RNE_FALLBACK=true.
  • Client : contacter le tenant pour recharger son abonnement RNE (interop@e-rne.tn).
  • Longer : activer une alerte proactive à 80 % de consommation pour anticiper.

Symptômes : webhook tcr.declaration.submit_failed ; l’IF ne peut pas déposer.

Diagnostic :

SELECT declaration_id, status, ides_reference, payload
FROM tcr_declaration
WHERE tenant_id = '<tenant>' AND status = 'rejected'
ORDER BY updated_at DESC LIMIT 5;
SELECT * FROM tcr_ides_acknowledgement
WHERE kind = 'error_report' AND received_at > NOW() - INTERVAL '24 hours';

Mitigations :

  1. Lire le compte-rendu d’erreur (RE) retourné par IDES :
    • Si erreur CF01 à CF15 → problème fichier (encodage, taille, compression, nommage). Relancer POST /v1/tcr/declarations/:id/xml pour regénérer et corriger.
    • Si erreur CV01, CV02 → non-conformité XSD. Revalider via xmllint --schema FatcaXML_v2.0.xsd localement.
    • Si erreur CM01 → DocRefID invalide. Regénérer la déclaration avec un nouveau GUID.
  2. Si IDES est indisponible (panne DGI), retenter dans 2 h avec backoff exponentiel. Alerter compliance officer du tenant si indisponibilité > 4 h.
  3. Pour les notifications ICMM (erreur 8008/8009/8011) reçues après transmission IRS : orchestrer automatiquement FATCA2/FATCA3 selon règle RG6.

RB-007 · Certificat Let’s Encrypt / TunTrust expirant (SEV-4)

Section intitulée « RB-007 · Certificat Let’s Encrypt / TunTrust expirant (SEV-4) »

Symptômes : CertificateExpiringSoon 14 jours avant expiration.

Mitigations :

Let’s Encrypt (NPM) : renouvellement automatique normalement. Si bloqué :

Fenêtre de terminal
# Vérifier état cert dans NPM
ssh root@192.168.100.143 "pct exec 131 -- docker exec nginx-proxy-manager_app_1 \
cat /etc/letsencrypt/live/docs.vitakyc.e-vitalis.com/fullchain.pem | \
openssl x509 -noout -dates"
# Forcer le renouvellement via API NPM
# (avec creds admin NPM)
curl -X POST .../api/nginx/certificates/43/renew -H "Authorization: Bearer $TOKEN"

TunTrust / ANCE (tenant par tenant) :

  • 30 j avant : email automatique au compliance officer du tenant avec instructions de renouvellement auprès de TunTrust.
  • 14 j avant : rappel + ouverture ticket support.
  • 7 j avant : escalade CSM.
  • Post-expiration : les signatures peuvent continuer à être valides (LTV) mais aucune nouvelle signature n’est possible jusqu’au renouvellement — bloquant pour production.

Symptômes : AuditHashChainBroken — un événement d’audit a un previous_hash qui ne correspond pas.

Gravité : critique — implique soit une corruption DB, soit une manipulation malveillante, soit un bug applicatif. Déclencher investigation immédiate.

Actions immédiates :

  1. Freeze écriture sur audit_event du tenant concerné (flag AUDIT_WRITE_FROZEN_<tenant_id>=true).
  2. Snapshot la DB : pg_dump → stockage offline chiffré.
  3. Ouvrir incident SEV-1 + escalade CTO + Compliance officer + légal.
  4. Notifier le client dans les 24 h (obligation contractuelle de transparence).
  5. Notifier la CNIL / régulateur si RGPD ou LCB-FT concerné, dans les 72 h (GDPR art. 33).

Investigation :

-- Trouver le point de rupture
WITH ordered AS (
SELECT event_id, event_hash, previous_hash, occurred_at,
LAG(event_hash) OVER (PARTITION BY tenant_id ORDER BY occurred_at) AS expected_prev
FROM audit_event
WHERE tenant_id = '<tenant>'
)
SELECT * FROM ordered
WHERE previous_hash IS DISTINCT FROM expected_prev
ORDER BY occurred_at LIMIT 10;

Remédiation :

  • Restaurer le backup du point de rupture et rejouer les événements bien formés.
  • Publier le dernier hash valide dans le registre blockchain de vérité (roadmap V2) ou dans un email signé au DPO client.
  • Post-mortem public ou interne selon l’impact.

RB-009 · Attaque de masse sur la biométrie (liveness) (SEV-1)

Section intitulée « RB-009 · Attaque de masse sur la biométrie (liveness) (SEV-1) »

Symptômes : MassBiometricFailure — taux d’échec liveness anormal, ou détection d’un pattern de spoof systématique.

Actions immédiates :

  1. Activer mode défense renforcée sur le tenant :
Fenêtre de terminal
kubectl -n vitakyc set env deployment/liveness-svc \
TENANT_<id>_LIVENESS_LEVEL=3_STRICT \
TENANT_<id>_RATE_LIMIT_SELFIE_PER_IP=3_PER_MIN
  1. Activer challenge actif systématique (clignement + tête) même si passif suffirait.
  2. Identifier le pattern : même user-agent ? même plage IP ? même document template ?
SELECT device->>'ip' AS ip, device->>'user_agent' AS ua, count(*)
FROM kyc_biometric_check
WHERE tenant_id = '<tenant>' AND performed_at > NOW() - INTERVAL '1 hour'
AND passed = false
GROUP BY 1, 2 ORDER BY 3 DESC LIMIT 10;
  1. Geo-block ou rate-limit des IPs fautives via Cloudflare API.
  2. Notifier le tenant + Compliance officer : risque d’attaque ciblée de création de comptes frauduleux.

Post-incident :

  • Collecter les échantillons suspects pour enrichir le modèle anti-spoof.
  • Vérifier qu’aucun compte frauduleux n’a été créé avec succès → revue des 24 dernières heures.
  • Reporting BCT / CRF si applicable.

Symptômes : LicenseQuotaExceeded — tenant approche de sa limite mensuelle.

Mitigations :

  • Soft-limit à 95 % : notification automatique au Customer Success Manager + au compliance officer du tenant.
  • Hard-limit à 100 % : passage en mode queueing (les vérifications sont mises en attente, pas rejetées). Si persistance > 2 h → notification critique.
  • Upsell : contact commercial pour upgrade plan ou ajout de volume.

4. Template post-mortem (obligatoire pour SEV-1 et SEV-2)

Section intitulée « 4. Template post-mortem (obligatoire pour SEV-1 et SEV-2) »
# Post-mortem · <titre incident> · YYYY-MM-DD
## Résumé
<une phrase : quoi, qui impacté, durée>
## Impact
- Durée totale : X minutes
- Tenants impactés : N
- Vérifications KYC impactées : N
- Déclarations TCR impactées : N
- Violations de SLA : oui/non
- Violations réglementaires : oui/non
## Timeline
- HH:MM · Détection automatique via <alerte>
- HH:MM · Incident Commander désigné
- HH:MM · Mitigation appliquée (<quoi>)
- HH:MM · Cause identifiée (<quoi>)
- HH:MM · Correctif déployé
- HH:MM · Incident résolu
- HH:MM · Notification clients
## Cause racine
<5 whys>
## Ce qui a bien fonctionné
- ...
## Ce qui n'a pas bien fonctionné
- ...
## Actions correctives (avec responsable + échéance)
- [ ] Action 1 — @owner — date
- [ ] Action 2 — @owner — date
## Lessons learned
- ...

OutilUsageAccès
PagerDutyAlerting + astreinteSSO Keycloak
GrafanaDashboards + Loki + TempoSSO Keycloak
ArgoCDDéploiement + rollbackSSO Keycloak
VaultSecrets + rotationMFA obligatoire
Runbooks (ce document)ProcéduresPrivé tenant
Slack #ops-alertsCanal alertes en temps réelÉquipe SRE + Tech Lead
Slack #inc-*Channels d’incidentIncident Commander ouvre

RotationHoraire
Primary on-call24/7, rotation hebdomadaire, 5-7 ingénieurs SRE/backend
Secondary on-callBackup, mêmes horaires
Executive on-callTech Lead / CTO pour escalades SEV-1

Rémunération : prime astreinte + compensation incidents (aligné standard marché MENA + EU).


Document vivant. À réviser après chaque incident SEV-1/2 et trimestriellement.