Concevoir un journal d'audit utile sans exposer les données sensibles
Designing useful audit logs without exposing sensitive data
Un journal d'audit doit reconstruire une action. Il ne devrait pas devenir une deuxième base de données contenant les mêmes renseignements qu'il cherche à protéger.
An audit log should reconstruct an action. It should not become a second database containing the same information it is meant to protect.
Abdessamad Cherifi14 juillet 2026
Le bon objectif n'est pas « tout enregistrer ». C'est « conserver assez de contexte pour rendre les actions vérifiables, avec le moins de données sensibles possible ».
Commencer par les questions auxquelles le journal doit répondre
Dans une application métier, l'équipe administrative veut généralement savoir qui a consulté un dossier, qui l'a créé, quel champ a été modifié, quand une exportation a été produite ou pourquoi un accès a changé. Ces questions définissent le modèle d'audit. Elles ne nécessitent pas automatiquement une copie de chaque ancienne et nouvelle valeur.
Le modèle que j'utilise sépare donc l'événement de son contenu métier: type d'action, auteur, dossier concerné, nom du champ, chemin applicatif et horodatage. Les métadonnées sont limitées à des valeurs contrôlées par le code, par exemple le résultat d'une tentative de connexion ou le nombre de résultats retournés.
Le journal d'audit est une responsabilité distincte. Il observe les opérations sans devenir la source de vérité des dossiers.
Tracer le changement, pas nécessairement la valeur
Pour une modification de dossier, l'application prend un instantané avant et après la validation du formulaire. Elle compare les deux états et crée une entrée par champ modifié. L'entrée indique donc que statut_couple ou telephone a changé, mais les colonnes old_value et new_value restent vides pour ce flux.
for field_name, _old, _new in diff_snapshots(before, after):
log_event(
request,
EventType.UPDATE,
client=client,
field_name=field_name,
)
Ce compromis conserve l'imputabilité: auteur, moment, objet et champ touché. Il réduit en même temps le risque qu'une sauvegarde de journaux, un écran administratif ou une erreur de diagnostic révèle une ancienne adresse, un numéro de téléphone ou une information clinique.
Les recherches sont un piège discret
Une boîte de recherche accepte souvent un nom, un numéro de téléphone ou un identifiant. Enregistrer la requête brute dans les métadonnées recréerait exactement les données que l'on essaie de limiter. L'application journalise plutôt la longueur de la requête et le nombre de résultats. On peut alors constater qu'une recherche a eu lieu et évaluer son comportement sans conserver ce qui a été saisi.
La même règle s'applique aux erreurs. Un message utile décrit le contexte technique et un identifiant de corrélation; il évite d'imprimer tout le formulaire ou l'objet sérialisé.
Prévoir l'anonymisation dès le modèle
L'anonymisation d'un dossier serait incomplète si ses anciennes valeurs restaient accessibles dans le journal. Le flux de rétention retire donc le lien vers le client, l'identifiant public, l'identifiant d'objet, les anciennes et nouvelles valeurs ainsi que les métadonnées associées. Une entrée d'anonymisation distincte permet encore de prouver que l'opération a eu lieu.
Cette décision doit être prise tôt. Plus les journaux contiennent de structures libres, plus il devient difficile de garantir qu'aucune donnée personnelle ne survit dans un coin inattendu.
Restreindre la lecture est aussi important que l'écriture
Les intervenants peuvent travailler sur les dossiers, mais la vue globale du journal reste administrative. Le panneau d'administration Django n'est pas exposé quand l'application tourne en production. Les événements sont indexés par type et date, client et date, puis auteur et date afin que les investigations courantes restent rapides sans donner accès à davantage d'information.
Tester les absences, pas seulement les présences
Les tests les plus importants affirment parfois qu'une donnée n'existe pas. Ici, un test vérifie qu'une recherche ne stocke pas la requête brute. Un autre confirme qu'une modification ne conserve pas les valeurs du champ. Un troisième anonymise un dossier et vérifie que les anciennes entrées ont été expurgées.
Ces assertions protègent une propriété de sécurité qui serait invisible dans un test fonctionnel classique. L'écran peut continuer de fonctionner même si un futur changement recommence accidentellement à stocker trop d'information.
La règle pratique
Définir les questions d'audit avant les colonnes.
Utiliser des types d'événements et métadonnées contrôlés.
Éviter les charges utiles, formulaires et requêtes brutes.
Séparer les permissions de modification et de consultation du journal.
Concevoir la rétention et l'anonymisation en même temps que le modèle.
Ajouter des tests qui prouvent que les valeurs sensibles ne sont pas conservées.
Un journal d'audit est réussi quand il rend une action compréhensible sans élargir inutilement la surface de données à protéger.
The right goal is not “record everything.” It is “retain enough context to make actions verifiable while collecting as little sensitive data as possible.”
Start with the questions the log must answer
In a business application, administrators usually need to know who viewed a record, who created it, which field changed, when an export was produced or why access was modified. Those questions define the audit model. They do not automatically require a copy of every old and new value.
The model I use separates the event from business content: action type, actor, related record, field name, application path and timestamp. Metadata is restricted to values controlled by the code, such as the result of a login attempt or the number of search results.
The audit log is a distinct responsibility. It observes operations without becoming the source of truth for records.
Record the change, not necessarily the value
When a record is edited, the application captures a snapshot before and after form validation. It compares the two states and creates one entry per changed field. The entry therefore states that statut_couple or telephone changed, while old_value and new_value remain empty for that workflow.
for field_name, _old, _new in diff_snapshots(before, after):
log_event(
request,
EventType.UPDATE,
client=client,
field_name=field_name,
)
This trade-off preserves accountability: actor, time, object and field. It also reduces the chance that a log backup, administrative screen or diagnostic mistake exposes an old address, telephone number or clinical detail.
Search is a quiet trap
A search box often accepts a name, telephone number or identifier. Saving the raw query in metadata would recreate the very data we are trying to limit. The application records the query length and result count instead. We can prove a search happened and assess its behaviour without retaining what was entered.
The same rule applies to errors. A useful message describes technical context and a correlation identifier; it avoids printing the entire form or serialized object.
Design anonymization into the model
Anonymizing a record would be incomplete if its previous values remained available in the audit log. The retention workflow therefore removes the client relationship, public identifier, object identifier, old and new values, and related metadata. A separate anonymization event can still prove that the operation took place.
This decision needs to happen early. The more free-form structures a log contains, the harder it becomes to guarantee that personal data does not survive in an unexpected corner.
Restricting reads matters as much as controlling writes
Practitioners can work with records, but the global audit view remains administrative. Django's administration panel is not exposed when the application runs in production. Events are indexed by type and date, client and date, then actor and date so common investigations remain fast without granting access to more information.
Test absences, not only presences
The most valuable tests sometimes assert that data does not exist. One test verifies that searches never store the raw query. Another confirms that edits do not retain field values. A third anonymizes a record and checks that previous entries have been redacted.
These assertions protect a security property that would be invisible in a conventional functional test. The screen could keep working even if a future change accidentally started storing too much information again.
The practical rule
Define audit questions before defining columns.
Use controlled event types and metadata.
Avoid raw payloads, forms and search queries.
Separate record-editing permissions from audit-reading permissions.
Design retention and anonymization alongside the model.
Add tests proving that sensitive values are not retained.
An audit log succeeds when it makes an action understandable without unnecessarily expanding the data surface that must be protected.