Naar de hoofdinhoud

Integriteit van de auditketen

Elke beheeractie die de toestand van je fremforge-organisatie verandert, zoals wijzigingen in de SSO-configuratie, het overschrijven van push-bescherming, updates van branchbescherming, overgangen in de facturatie en SCIM-provisioning, wordt vastgelegd in een auditlog. fremforge gaat een stap verder dan een gewone log: elk item is cryptografisch geketend aan het vorige, en de kop van de keten wordt elke 2 minuten WORM-verankerd (Write-Once-Read-Many) in opslag met OBS Object Lock in T Cloud Public.

Het resultaat: je kunt elke manipulatie van de auditlog ontdekken, en je kunt dat zelf verifiëren zonder fremverk op zijn woord te hoeven geloven.

Deze pagina beschrijft wat de keten garandeert, wat niet, en hoe je de verificatie van begin tot eind uitvoert. Het technische contract staat in DPA-bijlage A.7.

De garantie

Voor elk item in de auditlog dat voor je organisatie wordt vastgelegd, slaat fremforge op:

  • De gebeurtenis zelf (actor, actie, tijdstempel, velden).

  • Een prev_hash, de hash van het vorige item voor je tenant.

  • Een hash, SHA-256 over de canonieke JSON van het item.

    De canonicalisering is exact, en de volgorde hieronder is alfabetisch op sleutel, niet de volgorde waarin de velden elders op deze pagina staan. Een verificatietool die ze in een andere volgorde hasht, krijgt een andere digest en concludeert ten onrechte dat de keten gebroken is:

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

    Geen witruimte. prev_hash is de letterlijke waarde null bij het begin van de keten. created_at_iso is het tijdstempel als YYYY-MM-DDTHH:mm:ss.sssZ. fields_json is de payload als JSON-string, ingebed als string en niet als genest object, zodat hij byte voor byte behouden blijft in plaats van opnieuw te worden geserialiseerd. Dat laatste detail is waar mensen op vastlopen: parse en serialiseer je de payload opnieuw, dan komt je digest niet overeen met de onze.

Omdat de hash van elk item de vorige hash bevat, breekt het wijzigen van een item de keten vanaf dat punt. GET /orgs/{org}/audit/chain geeft de ruwe rijen terug, zodat je dat zelf kunt opsporen; GET /orgs/{org}/audit/integrity rapporteert de controle die wij hebben gedaan.

Daarnaast schrijft een aparte verankeringsjob elke 2 minuten (tenant_id, latest_hash, count, timestamp) naar OBS in T Cloud Public, met een Object Lock-bewaartermijn van 1095 dagen (3 jaar), in COMPLIANCE-modus. Het ankerobject is binnen de bewaartermijn fysiek niet te verwijderen: de opslaglaag weigert verwijderingen van iedereen, ook van fremverk. Een aanvaller die op de een of andere manier de hele keten in de database zou wijzigen, krijgt nog steeds te maken met de ankers in OBS, die het daar niet mee eens zijn.

Openheid over hiaten. De frequentie van 2 minuten is het doel; we beloven contractueel geen RPO van ≤2 minuten voor de verankering van de keten. Is OBS in T Cloud Public onbereikbaar (regionaal onderhoud, een netwerkincident), dan probeert de verankeringsjob het bij de volgende tik opnieuw en legt hij het hiaat vast in de metadata van de keten. Het slechtste waargenomen venster voordat het alarm voor een gebroken keten afgaat, is 12 minuten (frequentie + drempel ANCHOR_STALE). Hiaten worden beschreven in het volgende incidentrapport op de statuspagina en genoemd in de clausule over auditlogging in DPA §A.7.

Wat dit opspoort

DreigingOpgespoord?
De inhoud van één auditrij is gewijzigd (actor, actie, velden)✓, hash_mismatch op die rij
Eén rij is midden uit de keten verwijderd✓, prev_hash_mismatch op de rij daarna
De hele keten is herschreven door een aanvaller met volledige schrijftoegang tot de database✓ als het herschrijven meer dan ~10 minuten geleden gebeurde (in het slechtste geval 12 minuten), dan wijkt het WORM-anker af
De hele keten is herschreven + ook de WORM-bucket is gemanipuleerdObject Lock in de opslaglaag weigert de schrijfactie naar OBS fysiek; daarvoor zou T Cloud Public zelf gecompromitteerd moeten zijn
Een beheeractie die helemaal geen auditgebeurtenis oplevertDit is een hiaat in de dekking, geen breuk in de keten, zie “Wat dit niet opspoort”

Wat dit niet opspoort

  • Hiaten in de dekking. Verandert een codepad de toestand zonder een auditgebeurtenis op te leveren, dan zwijgt de keten. We dekken de routes die de toestand veranderen in bijlage A.7; elk kwartaal controleren we de routes om te bevestigen dat de dekking standhoudt.
  • Geschiedenis van vóór bijlage A.7. Auditgebeurtenissen die zijn vastgelegd voordat de verankering van de keten voor je tenant was ingeschakeld, hebben de markering pre-chain-epoch en maken uitdrukkelijk GEEN deel uit van de verifieerbare keten.
  • Leesgebeurtenissen. De keten legt alleen wijzigingen vast. Een lek van leestoegang (iemand die gegevens ziet die hij niet zou mogen zien) verschijnt niet als item in de keten. Het betreffende deel van de auditlog legt de leesactie wel vast als de route er een oplevert, maar de dekking van alleen-lezenroutes is een aparte garantie, geen garantie van de keten.

De keten zelf verifiëren

Dit is een lokale herberekening, geen uitgelezen status. fremforge audit-verify downloadt de ruwe rijen van GET /orgs/{org}/audit/chain, elk met haar hash en prev_hash, en berekent elke digest en elke schakel opnieuw op jouw machine. Daarna haalt het ons eigen oordeel op en vergelijkt het. De exitcode is jouw resultaat; een verschil tussen de twee wordt gemeld en laat het commando mislukken.

Dat onderscheid is belangrijker dan het lijkt. Alles boven deze sectie beschrijft een garantie tegen iemand die bij de database kan maar niet bij de API: het WORM-anker staat in Object Lock in COMPLIANCE-modus, dus een gemanipuleerde database kan er niet mee in overeenstemming worden gebracht. Niets daarvan legt de API zelf beperkingen op, en tot augustus 2026 werd de kruiscontrole tussen de controle van de keten en het anker uitgevoerd door dezelfde partij die het antwoord rapporteerde. Lokale verificatie maakt van de garantie iets wat jij kunt controleren, in plaats van iets wat wij beweren.

Het anker is ondertekend, en jij verifieert de handtekening, niet wij. Elk anker wordt geschreven met een DSSE-envelop, ondertekend met dezelfde Ed25519-sleutel als onze SLSA-buildprovenance, waarvan de publieke helft is gepubliceerd op www.frem.sh/.well-known/slsa-trust-root.json.

fremforge audit-verify haalt die trust root rechtstreeks op van www.frem.sh, niet van de API, en valt nooit terug op een sleutel die de API aanleverde: een handtekening die is gecontroleerd met een sleutel die de gecontroleerde partij je gaf, bewijst niets. Daarna controleert het of de subject-digest van de envelop gelijk is aan sha256(anchor_head), zodat een envelop die cryptografisch geldig is voor een ander anker wordt geweigerd in plaats van geaccepteerd.

Twee eerlijke kanttekeningen:

  • Ankers die vóór 2026-08-04 zijn geschreven, zijn niet ondertekend en melden NOT CHECKED. Er wordt niet achteraf ondertekend: de waarde van een anker is dat het op een bepaald moment onder Object Lock is geschreven, en de geschiedenis herschrijven om er een handtekening aan toe te voegen zou dat tenietdoen.
  • Ondertekenen is aan onze kant bewust niet fataal. Mislukt de ondertekening, dan wordt het anker toch geschreven: een niet-ondertekend anker is veel beter dan geen anker, want het anker is het laatste vangnet om manipulatie zichtbaar te maken. We slaan daarop alarm (AUDIT_ANCHOR_UNSIGNED, Major), zodat de achteruitgang luid is in plaats van stil, en audit-verify meldt NOT CHECKED in plaats van te doen alsof.

Gebruik de fremforge-CLI:

# 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

De uitvoer ziet er zo uit:

✓ 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.

Zonder --human geeft de CLI ruwe JSON voor scripts, inclusief beide oordelen en elk verschil daartussen:

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

Exitcodes, allemaal afgeleid van de lokale controle:

  • 0: de keten is geverifieerd en ons oordeel kwam overeen.
  • 2: gedeeltelijk: er zijn rijen met de markering chain-broken (we hebben de auditrij geschreven, maar de transactie voor de keten mislukte, dus de rij is bewaard om zichtbaar te blijven en de keten mist daar een schakel), of de maximale hoeveelheid rijen heeft de controle afgekapt, zodat een deel van de keten niet is gecontroleerd. Let op: wij melden ok bij een afgekapte controle en de CLI niet. Instaan voor rijen die niemand heeft gecontroleerd, is precies het soort overdrijving dat dit commando moet voorkomen.
  • 3: gebroken: de herberekende hash van een rij komt niet overeen met haar opgeslagen hash, OF de prev_hash van een rij komt niet overeen met de lopende keten. De uitvoer bevat de id van de rij first_break en de reden.
  • 4: afwijkend anker: de top die in het WORM-anker staat, komt niet voor in je keten, wat betekent dat de verankerde rij is verwijderd of gewijzigd. Let op: dit is een controle op aanwezigheid: een actieve tenant waarvan de nieuwste rijen nog niet zijn verankerd, is normaal en slaagt.
  • 5: wij waren het oneens: jouw controle was schoon, maar ons oordeel niet. Meestal is er tussen de twee aanroepen een rij toegevoegd. Voer het opnieuw uit om het te bevestigen; blijft het zo, dan zien wij iets wat jij niet ziet.

De richting van een verschil is niet symmetrisch, en de CLI zegt welke hij vond. Is ons oordeel slechter dan het jouwe, dan is dat meestal een kwestie van timing. Is ons oordeel beter dan het jouwe (wij zeggen ok waar je eigen berekening broken zegt), dan is dat precies het geval waarvoor dit hele mechanisme bestaat, en het wordt gemeld als incident in plaats van als waarschuwing.

Wat we intern doen

De kop van de keten wordt elke 2 minuten in WORM-opslag verankerd, en de ankerketen wordt doorlopend onafhankelijk opnieuw geverifieerd: de ankerketen van elke tenant wordt vanaf het begin doorlopen en gecontroleerd op continuïteit. Bij elke afwijking, of bij elke tenant waarvan het anker verouderd raakt (ouder dan ~10 minuten; in het slechtste geval 12 minuten), wordt de dienstdoende engineer direct opgeroepen, als vroege aanwijzing dat de verankering is gestopt. De gevolgen voor klanten vallen onder DPA §11A, en hiaten worden na een incident beschreven op onze statuspagina.

Pseudonimisering onder de AVG

Maak je gebruik van het recht op vergetelheid, dan worden de auditgebeurtenissen van je tenant gepseudonimiseerd, niet verwijderd: het veld actor wordt [erased] en fields_json wordt teruggezet naar {}. De schakel in de keten (de hash) blijft behouden, zodat de keten verifieerbaar blijft; de persoonsgegevens worden verwijderd.

Een run van fremforge audit-verify voor een gewiste tenant toont daarom tenant_erased_count > 0, en die rijen worden apart gemeld in plaats van als breuken: een gepseudonimiseerde rij komt bewust niet meer overeen met haar eigen hash, terwijl haar schakel naar de volgende rij nog wel wordt gecontroleerd. De vijf tellers (verified_count, pre_chain_epoch_count, chain_broken_count, tenant_erased_count, payload_redacted_count) tellen niet op tot rows_walked: één rij kan zowel gewist als van haar payload ontdaan zijn, en wordt dan onder beide geteld. De keten is intact; de inhoud is opgeschoond. De rijen zelf worden niet verwijderd wanneer de bewaartermijn van 3 jaar voor het WORM-anker afloopt; er is vandaag geen job die ze verwijdert (gecorrigeerd op 2026-10-08: deze pagina zei eerder van wel).

Onafhankelijke verificatie

Auditors kunnen onder NDA een volledige doorloop van de keten aanvragen. Mail compliance@frem.sh voor het pakket voor de technische beoordeling.