Aller au contenu

TechniqueExpert

Chaîne d'intégrité sur les traces d'audit : pourquoi un log signé n'est pas un log auditable

Pour : ingénieurs et SRE · architectes · finance, risque et conformitéPrérequis : Notions de base sur le hachage, les signatures numériques et le Collector OpenTelemetry.

Mode de lecture

En environnement régulé, la conversation sur l’intégrité des journaux risque de s’arrêter à une phrase : « les logs sont signés ». La case est cochée, le sujet est clos, l’auditeur repart avec une capture d’écran.

Ce raisonnement contient une erreur de niveau. Une signature est une propriété locale : elle porte sur un enregistrement pris isolément. Un audit pose une question globale : que manque-t-il dans ce journal.

Ces deux questions ne se recouvrent pas. Un journal peut être intégralement signé, chaque signature étant parfaitement valide et être malgré tout un faux. C’est le sujet de cet article : ce que la signature prouve réellement, ce qu’elle ne prouve pas et ce qu’il faut construire au-dessus pour obtenir un journal qu’un tiers hostile puisse vérifier.


Une signature numérique sur un enregistrement r établit deux choses :

  • Authenticité d’origine : r a été produit par le détenteur de la clé privée.
  • Intégrité de l’enregistrement : r n’a pas été modifié depuis sa signature.

C’est tout. La signature ne dit rien sur les enregistrements voisins, sur l’ordre, sur la complétude, ni sur ce qui aurait dû être écrit et ne l’a pas été.

JOURNAL SIGNÉ LIGNE À LIGNE
+----------------+ +----------------+ +----------------+
| r1 | sig(r1) | | r2 | sig(r2) | | r3 | sig(r3) |
+----------------+ +----------------+ +----------------+
[OK] [OK] [OK]
Chaque enregistrement est valide.
Aucun lien cryptographique n'existe entre eux.
L'ensemble n'est pas une entité vérifiable.

La signature garantit la qualité des maillons. Elle ne garantit pas qu’il existe une chaîne.


Trois manipulations produisent un journal dont toutes les signatures restent valides.

L’attaquant retire les enregistrements gênants. Les enregistrements restants n’ont pas été touchés : leurs signatures vérifient.

AVANT APRÈS SUPPRESSION
r1 [OK] r1 [OK]
r2 [OK] <- accès non autorisé r3 [OK]
r3 [OK] r5 [OK]
r4 [OK] <- élévation privilège
r5 [OK] Vérification globale : 3/3 signatures valides
Verdict de l'outil : conforme

C’est le scénario le plus simple pour qui veut effacer ses traces et c’est précisément celui contre lequel la signature par enregistrement est inopérante.

La chronologie est une propriété de l’ensemble. Un horodatage inclus dans l’enregistrement signé n’aide pas si l’horloge est celle du producteur compromis. Deux enregistrements permutés restent deux enregistrements valides.

L’attaquant coupe la fin du journal, typiquement le moment où sa présence devient visible. Rien dans le journal résiduel ne signale qu’il existait une suite.

ManipulationSignature par ligneChaînage de hachageChaînage + ancrage externe
Modification d’un enregistrementDétectéeDétectéeDétectée
Insertion d’un enregistrementNon détectée si clé compromiseDétectéeDétectée
Suppression au milieuNon détectéeDétectéeDétectée
RéordonnancementNon détectéeDétectéeDétectée
Troncature en queueNon détectéeNon détectéeDétectée
Réécriture complète par le détenteur de la cléNon détectéeNon détectéeDétectée
Source silencieuse (rien n’est écrit)Non détectéeNon détectéeDétectée si heartbeat

À mon avis, ce sont les deux dernières lignes qu’on oublie le plus facilement quand on présente une solution de journalisation.


Chaque enregistrement porte le condensat de l’état de la chaîne au moment de son écriture.

h(0) = H(chain_id) // graine
h(n) = H( h(n-1) || chain_id || seq(n) || H(payload(n)) )

Structure d’un enregistrement :

+----------------------------------------------------------------+
| chain_id : identifiant de flux (source, processus, hôte) |
| seq : entier monotone strict, propre à la chaîne |
| ts : horodatage producteur (indicatif, non probant) |
| prev_hash : h(n-1) |
| payload_hash : H(payload) |
| payload : l'événement lui-même |
+----------------------------------------------------------------+
| hash : h(n), calculé sur les champs ci-dessus |
+----------------------------------------------------------------+
CHAÎNAGE : DÉTECTION D'UNE SUPPRESSION
r1 --h1--> r2 --h2--> r3 --h3--> r4 --h4--> r5
|
| suppression de r3
v
r1 --h1--> r2 --h2--> ??? --h3--> r4
r4.prev_hash = h3
condensat recalculé jusqu'à r2 = h2
h2 != h3 -> RUPTURE DÉTECTÉE au rang 4

La vérification est un simple rejeu linéaire, en O(n), sans secret : n’importe qui disposant du journal peut la conduire. C’est la première propriété intéressante. La vérification devient publique.

Le chaînage repose sur du hachage, pas sur de la signature asymétrique. Les valeurs ci-dessous sont des ordres de grandeur, qui varient fortement selon le processeur et la bibliothèque ; mesurez-les sur votre matériel avec openssl speed sha256 ed25519 :

OpérationOrdre de grandeurDébit pratique
SHA-256 sur un enregistrement de 1 Ko~1 à 4 µsde quelques centaines de Mo/s à plus de 1 Go/s par cœur, selon la disponibilité des instructions SHA du processeur
Signature Ed25519~20 à 60 µs~15 à 50 k signatures/s par cœur
Vérification Ed25519~50 à 200 µs~5 à 20 k vérifications/s par cœur, plus coûteuse que la signature

Conclusion opérationnelle directe : on chaîne chaque enregistrement, on ne signe pas chaque enregistrement. Avec ces ordres de grandeur, signer ligne à ligne à 100 000 événements par seconde consommerait de 2 à 6 cœurs (100 000 × 20 à 60 µs) et vérifier ces signatures de 5 à 20 cœurs (100 000 × 50 à 200 µs), alors que le chaînage en consomme de 0,1 à 0,4 (100 000 × 1 à 4 µs) pour une garantie d’intégrité de la séquence. La signature se porte sur les points de contrôle, pas sur les lignes.


À mon avis, c’est ici que se situe la partie la plus facile à oublier.

Une chaîne tronquée reste une chaîne cohérente. Le vérificateur qui ne connaît pas la véritable tête de chaîne n’a aucun moyen de savoir que la chaîne continuait.

CHAÎNE COMPLÈTE
r1 -> r2 -> r3 -> r4 -> r5 -> r6 -> r7 head = h7
CHAÎNE TRONQUÉE PAR L'ATTAQUANT
r1 -> r2 -> r3 -> r4 head = h4
Vérification linéaire : cohérente de bout en bout.
Verdict : valide.
Sans référence externe à h7, la troncature est indétectable.

5.2 La réécriture complète par le détenteur de la clé

Section intitulée « 5.2 La réécriture complète par le détenteur de la clé »

C’est le point structurant. Si l’entité auditée détient la clé de signature et contrôle le stockage, elle peut reconstruire intégralement un journal alternatif : recalculer toute la chaîne depuis la graine, resigner les points de contrôle, remplacer le fichier. Le résultat est parfaitement cohérent.

Autrement dit : une preuve d’intégrité produite et conservée par l’entité auditée n’a de valeur que pour cette entité. Elle prouve l’absence d’accident, pas l’absence de fraude. Elle protège contre la corruption de disque et l’erreur d’exploitation, pas contre un administrateur hostile ni contre un attaquant ayant obtenu les privilèges du processus producteur.

Un HSM déplace le problème sans le résoudre : la clé n’est plus exfiltrable, mais le processus compromis peut toujours demander au HSM de signer ce qu’il veut. Le HSM protège la clé, pas la vérité.

Une source qui cesse d’émettre produit un journal parfaitement valide : le journal vide est cohérent. L’absence d’événement est indistinguable de l’absence de source. C’est un mode de contournement simple : on ne falsifie pas le journal, on éteint le producteur.


La seule sortie est de faire sortir une information de la sphère de contrôle de l’audité, périodiquement, de manière irréversible.

+---------------------------------------------------------------+
| CHECKPOINT |
| chain_id |
| seq_from, seq_to (couverture) |
| head_hash = h(seq_to) (état de la chaîne) |
| count (nombre d'enregistrements couverts) |
| signature (clé du producteur) |
| tsa_token (horodatage qualifié, RFC 3161) |
+---------------------------------------------------------------+

Le point de contrôle est publié vers un tiers, à intervalle court et fixe. À partir de là, l’audité ne peut plus réécrire le passé antérieur au dernier ancrage sans que la divergence apparaisse.

Vue d’ensemble, du chaînage local à l’ancrage hors de la sphère de l’audité :

flowchart TB
  subgraph AUD["Sphère de l'audité"]
    G["Graine<br/>h0 = H(chain_id)"] --> RN["r1 à rN<br/>h1 à hN"]
    RN --> CP["Checkpoint signé<br/>head_hash = hN"]
  end
  subgraph EXT["Hors de son contrôle"]
    TSA["Horodateur qualifié<br/>RFC 3161"]
    W["Dépôt tiers<br/>WORM"]
  end
  CP -->|"empreinte de 32 octets"| TSA
  TSA -->|"tsa_token"| CP
  CP -->|"dépôt append only"| W
sequenceDiagram
    autonumber
    participant P as Producteur (audité)
    participant T as Horodateur qualifié (TSA)
    participant W as Dépôt tiers / WORM
    participant A as Auditeur
    P->>P: chaîne les enregistrements (hachage)
    P->>P: calcule head_hash toutes les N lignes ou T secondes
    P->>T: demande d'horodatage sur head_hash
    T-->>P: jeton horodaté signé
    P->>W: dépôt du checkpoint (append only, immuable)
    Note over W: L'audité ne peut plus modifier ce qui est déposé
    A->>W: récupère la suite des checkpoints
    A->>P: récupère le journal brut
    A->>A: rejoue la chaîne, compare aux head_hash ancrés
    A->>A: vérifie la continuité des seq et des couvertures
MécanismePropriété obtenue
Chaînage de hachageIntégrité et ordre internes au flux
Numéro de séquence monotone par sourceDétection des trous
Point de contrôle ancré chez un tiersDétection de la troncature et de la réécriture
Horodatage qualifiéDatation opposable, indépendante de l’horloge de l’audité
Heartbeat périodique scelléDétection du silence

Le heartbeat mérite une ligne : une chaîne doit émettre un enregistrement même quand il ne se passe rien. « Rien à signaler » est une information. Sans elle, éteindre le collecteur devient la méthode d’effacement la plus simple et la plus discrète.

Contre l’attaquant qui obtient la clé à l’instant t et veut réécrire ce qui précède, la parade est la sécurité vers l’avant : la clé évolue à chaque période (k(n+1) = KDF(k(n))) et la clé précédente est détruite. L’attaquant qui capture k(n) ne peut pas reconstruire k(n-1) et ne peut donc pas resigner le passé. Il contrôle l’avenir, jamais l’antériorité. Cette famille de constructions est documentée depuis les travaux de Schneier et Kelsey sur les journaux d’audit sécurisés et reste, à mon avis, peu utilisée en production.

Le rejeu linéaire devient coûteux sur de très gros volumes (en ordre de grandeur, à partir de centaines de millions d’enregistrements, selon le matériel et la fréquence des vérifications). La structure adaptée est l’arbre de Merkle, avec deux preuves compactes :

ROOT (ancrée)
/ \
N01 N23
/ \ / \
H(r1) H(r2) H(r3) H(r4)
| | | |
r1 r2 r3 r4
Preuve d'inclusion de r3 : {H(r4), N01} -> O(log n)
Preuve de cohérence C1->C2 : l'arbre n'a fait que croître,
aucun élément passé n'a été modifié

La preuve de cohérence est la propriété décisive : elle démontre qu’entre deux points de contrôle, le journal n’a été qu’ajouté, jamais réécrit. C’est le modèle des journaux de transparence, éprouvé à très grande échelle par Certificate Transparency (RFC 9162).


C’est la question d’ingénierie concrète et la réponse est contre-intuitive.

flowchart LR
    A["Application<br/>SDK OTel"] -->|OTLP| B["Collector<br/>receiver"]
    B --> C["processors<br/>batch, filter, transform"]
    C --> D["exporter"]
    D -->|OTLP| E["Backend<br/>stockage"]
    E --> F["Vérificateur<br/>hors ligne"]
    P1["Option 1<br/>chaîner dans l'application"] -.-> A
    P2["Option 2<br/>chaîner dans l'exporter"] -.-> D
    P3["Option 3<br/>chaîner à l'ingestion backend"] -.-> E

Option 1, dans l’application. Rejetée. Le chaînage devient une dépendance du code métier, à réimplémenter dans chaque langage, à maintenir dans chaque équipe. La couverture risque d’être partielle, donc sans valeur : un journal partiellement chaîné n’apporte aucune garantie d’ensemble.

Option 3, à l’ingestion backend. Rejetée. Tout le trajet entre le producteur et le backend reste hors périmètre. On scelle les données après le seul segment où elles pouvaient être manipulées et on demande à l’auditeur de faire confiance au backend, c’est-à-dire à l’audité.

Option 2, dans l’exporter. Retenue. C’est le dernier point où un écrivain unique possède une séquence faisant autorité pour une source donnée, avant la sortie du domaine de confiance. Un même Collector reçoit en général plusieurs sources : l’exporter tient alors une chaîne par source (par exemple par service.instance.id) et non une chaîne commune à tout ce qu’il reçoit. C’est ce qui concilie ce choix avec la règle de la section suivante : la chaîne est calculée dans le pipeline, mais elle appartient à la source.

Le Collector OpenTelemetry n’a pas été conçu pour préserver un ordre strict. Quatre comportements cassent une chaîne naïve :

ComportementEffet sur la chaîneParade
Processeur batch, ou batching de l’exporter (sending_queue.batch) : regroupementRupture d’ordreChaîner après le regroupement, dans l’exporter
File d’envoi et nouvelles tentatives de l’exporter (sending_queue, retry_on_failure) : livraison au moins une fois, plusieurs consommateurs en parallèle (num_consumers, 10 par défaut)Doublons, ordre d’envoi non garantiVérification idempotente sur (chain_id, seq) ; num_consumers: 1 si l’ordre d’envoi compte
Plusieurs instances de CollectorChaînes concurrentesUne chaîne par source, jamais de chaîne globale
Redémarrage, perte de bufferRupture légitimeEnregistrement de scellement et de rupture

Les options de file, de nouvelles tentatives et de batching citées sont celles de l’exporterhelper, qui a remplacé l’ancien processeur queued_retry. Le batching dans l’exporter est désactivé par défaut (batch: {} pour l’activer) et tend à remplacer le processeur batch.

La règle qui découle de tout cela tient en une phrase : la chaîne appartient à la source, pas au pipeline. Toute tentative de construire une chaîne globale, unique, à travers une infrastructure distribuée qui garantit au mieux une livraison au moins une fois, produira des ruptures permanentes que l’exploitation finira par désactiver. Le risque est classique : un contrôle d’intégrité qui crie si souvent qu’on l’éteint.

Un redémarrage de processus est une rupture normale. Elle doit être déclarée, pas subie.

FIN DE CHAÎNE DÉBUT DE CHAÎNE SUIVANTE
+-------------------------+ +----------------------------+
| type : SEAL | | type : GENESIS |
| chain_id : A | | chain_id : B |
| seq_to : 148 220 | ------> | prev_chain : A |
| head_hash : h(148220) | | prev_head : h(148220) |
| reason : shutdown | | seq : 0 |
| signature, tsa_token | | signature, tsa_token |
+-------------------------+ +----------------------------+

Une chaîne qui s’arrête sans enregistrement de scellement est un signal d’alerte, pas un incident d’exploitation. Cette distinction est ce qui sépare un dispositif d’intégrité utilisable d’un générateur de bruit.

Le vérificateur est un binaire autonome, sans accès à l’infrastructure de production, qui prend en entrée un journal exporté et une suite de points de contrôle. Il produit un verdict et une liste d’anomalies. Aucun outil n’est publié avec cet article : ce qui suit est une sortie illustrative du vérificateur à construire, avec des valeurs inventées pour l’exemple.

$ verify --journal export.jsonl --checkpoints anchors/ --pubkeys keys/
chain_id=svc-paiement-01
enregistrements : 148 220
chaînage : OK
couverture checkpoints: 100 % (612 points de contrôle)
horodatages qualifiés : 612/612 valides
continuité des seq : OK
scellement final : OK
chain_id=svc-auth-03
enregistrements : 91 004
chaînage : OK
couverture checkpoints: 98,7 %
ANOMALIE : seq 88 401 -> 88 460, 58 enregistrements manquants
ANOMALIE : dernier checkpoint 14:02:11, dernier enregistrement 14:19:40
-> 17 min non ancrées, troncature possible
ANOMALIE : aucun enregistrement de scellement
VERDICT : 1 chaîne conforme, 1 chaîne non conforme

Qui vérifie quoi : chaque contrôle du vérificateur couvre une famille de manipulations et aucun ne suffit seul.

flowchart LR
  V["Vérificateur hors ligne<br/>journal exporté,<br/>checkpoints ancrés,<br/>clés publiques"]
  V --> V1["Rejeu du chaînage"] --> D1["Modification, insertion,<br/>suppression, réordonnancement"]
  V --> V2["Continuité des seq"] --> D2["Trous"]
  V --> V3["head_hash comparés<br/>aux ancrages"] --> D3["Troncature, réécriture"]
  V --> V4["Jetons d'horodatage"] --> D4["Datation opposable"]
  V --> V5["Heartbeat et scellement"] --> D5["Silence, arrêt<br/>non déclaré"]

Ce niveau de sortie est le livrable à viser. Une console verte n’est pas une preuve.


Les textes n’imposent presque jamais un mécanisme cryptographique nommé. Ils imposent des propriétés et c’est à l’architecte de choisir la construction qui les tient.

TexteExigence pertinenteTraduction technique
DORA (UE 2022/2554)Traçabilité des opérations, protection et détection, capacité à reconstituer les événements ayant conduit à un incidentJournal complet, ordonné, non réécrit, opposable
NIS2Journalisation, chaîne de preuve pour la notification d’incidentPreuve de complétude, datation fiable
PCI DSS, exigence 10Protection des journaux contre la modification, détection d’altérationChaînage plus contrôle d’intégrité
ISO 27001 A.8.15Journaux protégés contre la falsification et l’accès non autoriséIntégrité et confidentialité
eIDAS (UE 910/2014), article 41, paragraphe 2L’horodatage électronique qualifié bénéficie d’une présomption d’exactitude de la date et de l’heure et d’intégrité des données associéesUn des leviers donnant une présomption légale en droit européen
eIDAS, article 35, paragraphe 2Le cachet électronique qualifié bénéficie d’une présomption d’intégrité des données et d’exactitude de leur origineSceller les points de contrôle avec un cachet qualifié
eIDAS, article 25, paragraphe 2La signature électronique qualifiée a l’effet juridique d’une signature manuscriteUtile pour un acte signé par une personne, moins pour un flux machine
eIDAS 2 (UE 2024/1183), article 45k, paragraphe 2 (présomption) et article 45l (exigences)Les données d’un registre électronique qualifié bénéficient d’une présomption d’unicité et d’authenticité, d’exactitude de leur date et de leur heure ainsi que de leur ordre chronologique séquentiel dans le registre ; l’article 45l fixe les exigences qu’un tel registre doit remplir, dont la détection immédiate de toute modification ultérieureDéposer les points de contrôle dans un registre qualifié

Les services qualifiés d’eIDAS sont, à mon avis, le levier le plus actionnable et le moins exploité. Ancrer les points de contrôle sur un horodatage qualifié, les sceller avec un cachet qualifié ou les déposer dans un registre électronique qualifié transforme une propriété technique en élément probant. Les textes sont sur EUR-Lex : règlement (UE) n° 910/2014, version consolidée et règlement (UE) 2024/1183. Le coût est marginal : on horodate une empreinte de 32 octets toutes les quelques secondes, pas chaque ligne.

Deux idées reçues à écarter au passage :

  • « Le WORM suffit. » Le stockage immuable protège contre la modification après écriture. Il ne protège ni contre ce qui n’a jamais été écrit, ni contre un producteur compromis qui écrit un faux dans un stockage immuable. Un mensonge gravé dans le marbre reste un mensonge.
  • « Le SIEM garantit l’intégrité. » Le SIEM est en aval. Il hérite de la qualité de ce qu’on lui envoie. Il ne peut pas certifier ce qu’il n’a jamais reçu.

Une seule question distingue un journal signé d’un journal auditable :

Un tiers qui ne me fait pas confiance peut-il, avec ses propres outils, sans mon infrastructure et sans ma coopération, détecter une suppression, une troncature ou une réécriture ?

La checklist qui en découle :

  • Chaque enregistrement porte le condensat du précédent, dans une chaîne propre à sa source.
  • Les numéros de séquence sont monotones stricts et les trous sont détectables.
  • Un heartbeat scellé est émis même en l’absence d’événement.
  • Un point de contrôle signé est publié à intervalle court, chez un tiers hors du contrôle de l’audité.
  • Les points de contrôle portent un horodatage qualifié.
  • La clé de signature évolue et le matériel antérieur est détruit.
  • Les ruptures de chaîne sont déclarées par un enregistrement de scellement.
  • Le vérificateur est un binaire autonome, exécutable hors ligne, dont la sortie est un rapport d’anomalies et non un voyant.
  • Un exercice de vérification adversariale est conduit périodiquement : on supprime volontairement un enregistrement et on mesure si le dispositif le voit.

Si le dernier point n’a jamais été exécuté, le dispositif n’a jamais été testé. Il a été déployé.


La signature répond à la question « cet enregistrement est-il authentique ». Personne ne pose cette question lors d’un audit.

La question posée est « ce journal est-il complet ». Elle est de nature globale et elle appelle trois constructions distinctes, empilées : le chaînage pour lier les enregistrements entre eux, l’ancrage externe pour retirer à l’audité le pouvoir de réécrire, l’horodatage qualifié pour donner une date opposable.

Sans ancrage, on obtient une preuve d’intégrité dont la valeur est nulle pour la seule personne qui en a besoin : celle qui ne nous fait pas confiance.

Le coût de cette architecture est faible et le calcul est simple : du hachage sur chaque ligne, une signature toutes les quelques secondes, un horodatage sur 32 octets. À mon avis, ce n’est pas le coût qui explique son absence. C’est le fait que la case « logs signés » soit cochable sans elle.

Révisé le 2 octobre 2026 : présomptions légales d’eIDAS complétées (cachet qualifié, registres électroniques qualifiés d’eIDAS 2), options du Collector mises à jour (sending_queue et retry_on_failure à la place de queued_retry), sortie du vérificateur présentée comme illustrative, ordres de grandeur de SHA-256 et d’Ed25519 élargis et calcul des cœurs refait, référence eIDAS 2 précisée (article 45k, paragraphe 2 et article 45l), Certificate Transparency sourcé.