Skip to content

feat(security): WORM audit log with cryptographic hash chain + verify endpoint #368

Description

@Yanstart

Contexte

Le log audit actuel (migration 002) est une table PostgreSQL standard. Un DBA avec les bons droits peut éditer ou supprimer une ligne sans laisser de trace. Pour la prod en milieu hospitalier réglementé (FDA 21 CFR 11 §11.10(e), EU MDR Annexe VIII), l'audit doit être immuable : append-only, signé, vérifiable.

Référence : docs/architecture/ANNOTATION_DATA_MODEL_AUDIT.md — gap G6.

Cette issue complète #349 (documentation AI Act) en fournissant l'implémentation technique de l'auditabilité.

Objectif

  1. Table audit_log transformée en WORM via triggers PostgreSQL qui REFUSENT UPDATE et DELETE
  2. Chaque ligne contient prev_hash (hash de la ligne précédente) et row_hash — chaîne cryptographique
  3. Endpoint GET /api/v1/audit/verify qui re-calcule la chaîne et détecte toute altération
  4. Signature optionnelle (HMAC ou Ed25519) par lot pour exiger une clé externe en cas de vérification forensique

Approche technique proposée

Migration 013_audit_immutable.py

ALTER TABLE audit_log
  ADD COLUMN prev_hash TEXT,
  ADD COLUMN row_hash TEXT NOT NULL DEFAULT '';

CREATE FUNCTION audit_log_compute_hash() RETURNS TRIGGER AS $$
DECLARE last_hash TEXT;
BEGIN
    SELECT row_hash INTO last_hash FROM audit_log
      ORDER BY created_at DESC, id DESC LIMIT 1;
    NEW.prev_hash = COALESCE(last_hash, '');
    NEW.row_hash = encode(sha256(
        (NEW.prev_hash || NEW.actor || NEW.action ||
         NEW.resource_type || NEW.resource_id ||
         COALESCE(NEW.payload::text, '') || NEW.created_at::text)::bytea
    ), 'hex');
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER audit_log_hash_chain BEFORE INSERT ON audit_log
    FOR EACH ROW EXECUTE FUNCTION audit_log_compute_hash();

CREATE FUNCTION audit_log_immutable() RETURNS TRIGGER AS $$
BEGIN RAISE EXCEPTION 'audit_log is append-only (WORM)'; END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER audit_log_no_update BEFORE UPDATE ON audit_log
    FOR EACH ROW EXECUTE FUNCTION audit_log_immutable();
CREATE TRIGGER audit_log_no_delete BEFORE DELETE ON audit_log
    FOR EACH ROW EXECUTE FUNCTION audit_log_immutable();

Endpoint de vérification

@router.get("/audit/verify")
async def verify_audit_chain(
    since: datetime | None = None,
    current_user: CurrentUser = Depends(require_role("ADMIN_TECHNIQUE")),
):
    """Re-calcule la chaîne et retourne le premier point de divergence."""
    ...

Signature externe (optionnelle, prod)

Périodiquement (cron ou manuel), un job signe le row_hash le plus récent avec une clé privée externe (HSM, Vault). Signature stockée hors-DB. Permet de prouver à un auditeur que la chaîne n'a pas été reconstruite a posteriori.

Acceptance criteria (fonctionnel)

  • Migration 013_audit_immutable.py créée et testée (up + down)
  • Trigger audit_log_compute_hash actif
  • Triggers UPDATE/DELETE refusent les opérations (test explicite)
  • Endpoint GET /api/v1/audit/verify opérationnel
  • Endpoint POST /api/v1/audit/sign (manuel ou cron) pour signer le tail
  • Documentation docs/architecture/AUDIT_LOG_INTEGRITY.md

Review checklist (conditions de validation pour le reviewer PR)

Code

  • Tests : UPDATE direct via raw SQL doit raise (assert_raises)
  • Tests : DELETE direct via raw SQL doit raise
  • Tests : altération du payload directement (via dump-restore) détectée par verify
  • Test de charge : verify sur 1M de lignes < 60 s

Cryptographie

  • SHA-256 utilisé (pas MD5/SHA-1)
  • Champ unique par row pour éviter rainbow tables (timestamp + UUID)
  • Documentation explicite : ce n'est PAS un blockchain (pas de PoW), c'est une hash chain auditable

Performance

  • Trigger ajoute < 5 ms par INSERT
  • Index created_at, id créé pour lecture chronologique
  • Vérification par chunks (pas de chargement complet en RAM)

Sécurité opérationnelle

  • Rôle DB varuna n'a PAS DROP TRIGGER ni ALTER TABLE audit_log (à acter avec l'équipe ops)
  • Backup strategy : backup PostgreSQL inclut triggers + données (test restore-then-verify)
  • Signature externe : clé hors-DB (Vault/HSM), pas dans le repo

Documentation

Dépendances

Hors-scope

  • Migration vers Hyperledger / Quorum (overkill)
  • Stockage hors-DB du log (sink Kafka → S3 immutable = issue séparée)

Références

  • docs/architecture/ANNOTATION_DATA_MODEL_AUDIT.md §3 (G6)
  • FDA 21 CFR 11 §11.10(e) — audit trail computer-generated
  • EU MDR Annexe VIII — Documentation technique
  • EU AI Act art. 12 — Logging

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions