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
- Table
audit_log transformée en WORM via triggers PostgreSQL qui REFUSENT UPDATE et DELETE
- Chaque ligne contient
prev_hash (hash de la ligne précédente) et row_hash — chaîne cryptographique
- Endpoint
GET /api/v1/audit/verify qui re-calcule la chaîne et détecte toute altération
- 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)
Review checklist (conditions de validation pour le reviewer PR)
Code
Cryptographie
Performance
Sécurité opérationnelle
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
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.
Cette issue complète #349 (documentation AI Act) en fournissant l'implémentation technique de l'auditabilité.
Objectif
audit_logtransformée en WORM via triggers PostgreSQL qui REFUSENT UPDATE et DELETEprev_hash(hash de la ligne précédente) etrow_hash— chaîne cryptographiqueGET /api/v1/audit/verifyqui re-calcule la chaîne et détecte toute altérationApproche technique proposée
Migration
013_audit_immutable.pyEndpoint de vérification
Signature externe (optionnelle, prod)
Périodiquement (cron ou manuel), un job signe le
row_hashle 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)
013_audit_immutable.pycréée et testée (up + down)audit_log_compute_hashactifGET /api/v1/audit/verifyopérationnelPOST /api/v1/audit/sign(manuel ou cron) pour signer le taildocs/architecture/AUDIT_LOG_INTEGRITY.mdReview checklist (conditions de validation pour le reviewer PR)
Code
assert_raises)payloaddirectement (via dump-restore) détectée parverifyverifysur 1M de lignes < 60 sCryptographie
Performance
created_at, idcréé pour lecture chronologiqueSécurité opérationnelle
varunan'a PASDROP TRIGGERniALTER TABLE audit_log(à acter avec l'équipe ops)Documentation
docs/architecture/AUDIT_LOG_INTEGRITY.mdcomplet (théorie + procédure ops + procédure audit)docs/architecture/ANNOTATION_DATA_MODEL_AUDIT.md: G6 marquéRESOLVEDDépendances
Hors-scope
Références
docs/architecture/ANNOTATION_DATA_MODEL_AUDIT.md§3 (G6)