Aller au contenu principal

Intégrité de la chaîne d’audit

Chaque action d’administration qui modifie l’état de votre organisation fremforge (changements de configuration SSO, contournements de la protection des push, mises à jour de la protection de branche, transitions de facturation, provisionnement SCIM) est enregistrée dans un journal d’audit. fremforge va un cran plus loin qu’un simple journal : chaque entrée est chaînée cryptographiquement à la précédente, et la tête de chaîne est ancrée en WORM (Write-Once-Read-Many) dans un stockage T Cloud Public OBS Object Lock toutes les 2 minutes.

Résultat : vous pouvez détecter toute altération du journal d’audit, et vous pouvez le vérifier vous-même sans avoir à croire fremverk sur parole.

Cette page décrit ce que la chaîne garantit, ce qu’elle ne garantit pas, et comment exercer la vérification de bout en bout. Le contrat technique figure dans l’annexe A.7 du DPA.

La garantie

Pour chaque entrée du journal d’audit enregistrée pour votre organisation, fremforge stocke :

  • L’événement lui-même (acteur, action, horodatage, champs).

  • Un prev_hash, l’empreinte de l’entrée précédente de votre tenant.

  • Un hash, SHA-256 calculé sur le JSON canonique de l’entrée.

    La canonicalisation est exacte, et l’ordre ci-dessous est alphabétique par clé, et non l’ordre dans lequel les champs sont présentés ailleurs sur cette page : un vérificateur qui les hache dans un autre ordre obtient une autre empreinte et conclut, à tort, que la chaîne est rompue :

    sha256( {"action":…,"actor":…,"created_at_iso":…,"fields_json":…,"prev_hash":…,"tenant_id":…} )

    Aucun espace. prev_hash vaut le littéral null à l’époque de la chaîne. created_at_iso est l’horodatage au format YYYY-MM-DDTHH:mm:ss.sssZ. fields_json est la charge utile sous forme de chaîne JSON, intégrée comme chaîne et non comme objet imbriqué, afin qu’elle soit restituée octet pour octet au lieu d’être resérialisée. C’est ce dernier détail qui piège les gens : analysez puis resérialisez la charge utile, et votre empreinte ne correspondra pas à la nôtre.

Comme l’empreinte de chaque entrée inclut l’empreinte précédente, modifier une entrée rompt la chaîne pour toutes les suivantes. GET /orgs/{org}/audit/chain renvoie les lignes brutes afin que vous puissiez le détecter vous-même ; GET /orgs/{org}/audit/integrity rend compte du parcours que nous avons effectué.

En outre, toutes les 2 minutes, une tâche d’ancrage distincte écrit (tenant_id, latest_hash, count, timestamp) dans T Cloud Public OBS avec une conservation Object Lock de 1095 jours (3 ans), en mode COMPLIANCE. L’objet d’ancrage est physiquement impossible à supprimer pendant la période de conservation : la couche de stockage rejette les suppressions de quiconque, fremverk compris. Un attaquant qui aurait réussi à modifier toute la chaîne en base de données devrait encore composer avec les ancrages côté OBS, qui le contrediraient.

Transparence sur les lacunes. La cadence de 2 minutes est un objectif ; nous ne revendiquons pas de RPO contractuel ≤ 2 min pour l’ancrage de la chaîne. Si T Cloud Public OBS est injoignable (maintenance régionale, incident réseau), la tâche d’ancrage réessaie au cycle suivant et consigne la lacune dans les métadonnées de la chaîne. La fenêtre maximale observée avant le déclenchement de l’alarme de rupture de chaîne est de 12 minutes (cadence + seuil ANCHOR_STALE). Les lacunes sont documentées dans le rapport d’incident suivant de la page de statut et mentionnées dans la clause de journalisation d’audit du DPA §A.7.

Ce que cela détecte

MenaceDétectée ?
Le contenu d’une ligne d’audit modifié (acteur, action, champs)✓, hash_mismatch sur cette ligne
Une ligne supprimée au milieu de la chaîne✓, prev_hash_mismatch sur la ligne suivante
Toute la chaîne réécrite par un attaquant disposant d’un accès complet en écriture à la base✓ si la réécriture remonte à plus de ~10 min (12 minutes au pire), l’ancrage WORM la contredit
Toute la chaîne réécrite + le bucket WORM également altéréL’Object Lock de la couche de stockage rejette physiquement l’écriture OBS ; cela exigerait une compromission côté T Cloud Public
Une action d’administration qui n’émet aucun événement d’auditIl s’agit d’une lacune de couverture, pas d’une rupture de chaîne ; voir « Ce que cela ne détecte pas »

Ce que cela ne détecte pas

  • Les lacunes de couverture. Si un chemin de code modifie l’état sans émettre d’événement d’audit, la chaîne reste muette. Nous couvrons les routes qui modifient l’état dans l’annexe A.7 ; nous auditons chaque trimestre la surface des routes pour confirmer que la couverture tient.
  • L’historique antérieur à l’annexe A.7. Les événements d’audit émis avant l’activation de l’ancrage de la chaîne sur votre tenant portent une sentinelle pre-chain-epoch et ne font explicitement PAS partie de la chaîne vérifiable.
  • Les événements de lecture. La chaîne n’enregistre que les modifications. Une fuite d’accès en lecture (quelqu’un qui voit des données qu’il ne devrait pas voir) n’apparaîtra pas comme entrée de chaîne ; la partie journal d’audit enregistre bien la lecture lorsque la route en émet une, mais la couverture des routes en lecture seule est une garantie distincte, pas une garantie de la chaîne.

Vérifier la chaîne vous-même

Il s’agit d’un recalcul local, pas d’une lecture de statut. fremforge audit-verify télécharge les lignes brutes depuis GET /orgs/{org}/audit/chain, chacune avec son hash et son prev_hash, et recalcule chaque empreinte et chaque maillon sur votre machine. Il récupère ensuite notre propre verdict et compare. Le code de sortie est votre résultat ; un désaccord entre les deux est signalé et fait échouer la commande.

La distinction compte plus qu’il n’y paraît. Tout ce qui précède cette section décrit une garantie contre quelqu’un qui atteint la base de données mais pas l’API : l’ancrage WORM est en mode Object Lock COMPLIANCE, si bien qu’une base altérée ne peut pas être mise en accord avec lui. Rien de cela ne contraint l’API elle-même, et jusqu’en août 2026 le recoupement entre le parcours de la chaîne et l’ancrage était effectué par la même partie qui rendait la réponse. La vérification locale est ce qui rend la garantie vérifiable par vous plutôt qu’affirmée par nous.

L’ancrage est signé, et c’est vous qui vérifiez la signature, pas nous. Chaque ancrage est écrit avec une enveloppe DSSE signée par la même clé Ed25519 que notre provenance de build SLSA, dont la moitié publique est publiée sur www.frem.sh/.well-known/slsa-trust-root.json.

fremforge audit-verify récupère cette racine de confiance directement sur www.frem.sh, et non via l’API, et il n’existe aucun repli sur une clé fournie par l’API : une signature vérifiée avec une clé remise par la partie auditée ne prouve rien. Il vérifie ensuite que l’empreinte du sujet de l’enveloppe est égale à sha256(anchor_head), de sorte qu’une enveloppe cryptographiquement valide pour un autre ancrage est rejetée au lieu d’être acceptée.

Deux réserves en toute honnêteté :

  • Les ancrages écrits avant le 2026-08-04 ne sont pas signés et sont signalés NOT CHECKED. Il n’y a pas de signature rétroactive : la valeur d’un ancrage tient au fait qu’il a été écrit à un moment donné sous Object Lock, et réécrire l’historique pour y ajouter une signature irait à l’encontre de cela.
  • La signature n’est volontairement pas bloquante de notre côté. Si le signataire échoue, l’ancrage est tout de même écrit : un ancrage non signé vaut bien mieux que pas d’ancrage du tout, car l’ancrage est le dernier rempart pour la détection d’altérations. Nous déclenchons une alarme dans ce cas (AUDIT_ANCHOR_UNSIGNED, Major) afin que la dégradation soit visible plutôt que silencieuse, et audit-verify affiche NOT CHECKED au lieu de faire semblant.

Utilisez la CLI fremforge :

# Install (one-liner — see /cli/ for manual download and SHA-256 verification)
curl -sSfL https://cli.frem.sh/cli/install.sh | sh

# Configure auth — generate a PAT at https://frem.sh/user/settings/applications
# with the audit:read scope.
export FREMFORGE_TOKEN='<your PAT>'

# Verify the chain for your org
fremforge audit-verify acme --human

La sortie ressemble à ceci :

✓ local verification: ok
  (hashes recomputed on this machine — the server was not asked)

  walk_mode:              tail
  rows_walked:            1284
  verified_count:         1280
  pre_chain_epoch_count:  4
  chain_broken_count:     0
  tenant_erased_count:    0
  payload_redacted_count: 0
  last_verified_hash:     a2b3c4...

✓ WORM anchor (OBS Object Lock):
    anchor_head:            f8e9d0...
    chain_tip_at_anchor:    a2b3c4...
    count_at_anchor:        1280
    timestamp:              2026-05-05T18:00:00.000Z
    age_seconds:            120
    agrees_with_local_walk: true
    signature:              VERIFIED (keyid fremforge-slsa-builder-v1)
    against the trust root fetched from www.frem.sh, not from this API —
    so the anchored tip is the tip that was anchored.

  server verdict:         ok
  server agrees with the local walk.

Sans --human, la CLI émet du JSON brut pour les scripts, y compris les deux verdicts et toute divergence entre eux :

# Exit 0 only if YOUR walk was clean and ours agreed
fremforge audit-verify acme || echo "verification failed: $?"

Codes de sortie, tous issus du parcours local :

  • 0 : la chaîne a été vérifiée et notre verdict concordait.
  • 2 : partiel : soit il existe des lignes sentinelles chain-broken (nous avons écrit la ligne d’audit mais la transaction de chaîne a échoué ; la ligne est donc conservée pour la visibilité et il manque un maillon à cet endroit), soit la limite de lignes a tronqué le parcours et une partie de la chaîne n’a pas été vérifiée. Notez que nous rendons ok sur un parcours tronqué et que la CLI ne le fait pas : attester des lignes que personne n’a parcourues est exactement le genre d’exagération que cette commande vise à éviter.
  • 3 : rompu : l’empreinte recalculée d’une ligne ne correspond pas à son empreinte stockée, OU le prev_hash d’une ligne ne correspond pas à la chaîne en cours. La sortie inclut l’identifiant de ligne first_break et la raison.
  • 4 : désaccord d’ancrage : la tête ancrée en WORM est absente de votre chaîne, ce qui signifie que la ligne ancrée a été supprimée ou modifiée. Notez qu’il s’agit d’une vérification d’inclusion : un tenant actif dont les lignes les plus récentes ne sont pas encore ancrées est normal et passe.
  • 5 : nous étions en désaccord : votre parcours était propre mais notre verdict ne l’était pas. Généralement une ligne ajoutée entre les deux appels. Relancez pour confirmer ; si cela persiste, nous voyons quelque chose que vous ne voyez pas.

Le sens d’un désaccord n’est pas symétrique, et la CLI indique lequel elle a trouvé. Un verdict de notre part pire que le vôtre est généralement un artefact de timing. Un verdict de notre part meilleur que le vôtre (nous disons ok là où votre propre calcul dit broken) est précisément le cas que tout ce mécanisme existe pour révéler, et il est signalé comme un incident plutôt que comme un avertissement.

Ce que nous faisons en interne

La tête de chaîne est ancrée toutes les 2 minutes dans un stockage WORM, et la chaîne d’ancrages est revérifiée de manière indépendante selon un calendrier continu : la chaîne d’ancrages de chaque tenant est parcourue depuis l’origine et sa continuité est vérifiée. Toute divergence, ou tout tenant dont l’ancrage devient périmé (plus de ~10 minutes ; 12 minutes au pire), alerte immédiatement l’astreinte, comme indicateur avancé que les écritures d’ancrage se sont arrêtées. L’impact pour les clients est couvert par le §11A du DPA ; les lacunes sont documentées sur notre page de statut après incident.

Pseudonymisation au titre du RGPD

Lorsque vous exercez le droit à l’effacement, les événements d’audit de votre tenant sont pseudonymisés, et non supprimés : le champ actor devient [erased] et fields_json est remis à {}. Le maillon de chaîne (l’empreinte) est conservé afin que la chaîne reste vérifiable ; les données personnelles sont supprimées.

Cela signifie qu’une exécution de fremforge audit-verify sur un tenant effacé affichera tenant_erased_count > 0, et ces lignes sont signalées à part plutôt que comme des ruptures : une ligne pseudonymisée ne correspond volontairement plus à sa propre empreinte, tandis que son lien avec la ligne suivante est toujours vérifié. Les cinq compteurs (verified_count, pre_chain_epoch_count, chain_broken_count, tenant_erased_count, payload_redacted_count) ne totalisent pas rows_walked : une même ligne peut être à la fois effacée et expurgée de sa charge utile, et elle est alors comptée dans les deux. La chaîne est intacte ; le contenu est nettoyé. Les lignes elles-mêmes ne sont pas supprimées à l’expiration de la période de conservation de 3 ans de l’ancrage WORM ; aucune tâche ne les supprime aujourd’hui (corrigé le 2026-10-08 : cette page affirmait le contraire).

Vérification indépendante

Les auditeurs peuvent demander une présentation complète de la chaîne sous accord de confidentialité : écrivez à compliance@frem.sh pour obtenir le dossier d’évaluation technique.