Audit chain integrity
Every state-changing admin action on your fremforge org, SSO config changes, push-protection overrides, branch-protection updates, billing transitions, SCIM provisioning, is recorded in an audit log. fremforge goes one step further than a plain log: each entry is cryptographically chained to the previous one, and the chain head is WORM-anchored (Write-Once-Read-Many) to T Cloud OBS Object Lock storage every 2 minutes.
The result: you can detect any tampering of the audit log, and you can verify it yourself without trusting fremverk’s word for it.
This page documents what the chain guarantees, what it doesn’t, and how to exercise the verification end-to-end. The technical contract is in DPA Annex A.7.
The guarantee
For every audit-log entry recorded for your org, fremforge stores:
- The event itself (actor, action, timestamp, fields).
- A
prev_hash, the hash of the previous entry for your tenant. - A
hash, SHA-256 over canonical JSON of (tenant_id, actor, action, fields_json, created_at_iso, prev_hash).
Because each entry’s hash includes the previous hash, modifying any entry breaks the chain forward. A /audit/integrity walk catches the break and reports the offending row.
In addition, every 2 minutes a separate anchor job writes (tenant_id, latest_hash, count, timestamp) to T Cloud OBS with Object Lock retention of 1095 days (3 years). The anchor object is physically un-deletable within retention, the storage layer rejects deletes from anyone, including fremverk. An attacker who somehow modified the entire DB chain would still have to reckon with the OBS-side anchors that disagree.
Gap disclosure. The 2-minute cadence is the target; we don’t claim a contractual ≤2min RPO on chain anchoring. If T Cloud OBS is unreachable (regional maintenance, network event), the anchor job retries on the next tick and records the gap in the chain metadata. Worst-case observed window before the chain-break alarm fires is 12 minutes (cadence + ANCHOR_STALE threshold). Gaps are documented in the next status-page incident report and called out in the DPA §A.7 audit-logging clause.
What this catches
| Threat | Caught? |
|---|---|
| A single audit row’s content modified (actor, action, fields) | ✓, hash_mismatch on that row |
| A single row deleted from the middle of the chain | ✓, prev_hash_mismatch on the row after |
| The whole chain rewritten by an attacker with full DB write | ✓ if the rewrite happened > 5 min ago, the WORM anchor disagrees |
| The whole chain rewritten + the WORM bucket also tampered | Storage-layer Object Lock physically rejects the OBS write; would require T Cloud-side compromise |
| An admin action that doesn’t emit an audit event at all | This is a coverage gap, not a chain break, see “What this doesn’t catch” |
What this doesn’t catch
- Coverage gaps. If a code path mutates state without emitting an audit event, the chain is silent. We cover the state-changing routes in Annex A.7; we run a quarterly audit on the route surface to confirm coverage holds.
- Pre-Annex-A.7 history. Audit events emitted before chain anchoring was enabled on your tenant carry a
pre-chain-epochsentinel and are explicitly NOT part of the verifiable chain. - Read events. The chain only records mutations. A leak of read access (someone seeing data they shouldn’t) won’t appear as a chain entry, the audit-log slice does record the read where the route emits one, but read-only route coverage is a separate guarantee, not a chain guarantee.
Verifying the chain yourself
Use the 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>'
# Walk the chain for your org
fremforge audit-verify acme --humanOutput looks like:
✓ integrity: ok
walked_rows: 1284
verified_count: 1280
pre_chain_epoch_count: 4
chain_broken_count: 0
last_verified_hash: a2b3c4...
✓ WORM anchor (OBS Object Lock):
anchor_head: f8e9d0...
count_at_anchor: 1280
timestamp: 2026-05-05T18:00:00.000Z
age_seconds: 120
agrees_with_db_walk: trueWithout --human, the CLI emits raw JSON for scripting:
# Exit 0 if integrity is ok; non-zero on partial / broken / anchor_mismatch
fremforge audit-verify acme || echo "integrity failed: $?"Exit codes:
0, chain integrity is ok2, partial: there arechain-brokensentinel rows (we wrote the audit row but the chain transaction failed; the row is preserved for visibility but the chain is missing a link there)3, broken: a row’s hash doesn’t match the recomputed hash, OR a row’sprev_hashdoesn’t match the running chain. The output includes thefirst_breakrow id + reason.4, anchor mismatch: the in-DB chain disagrees with the WORM anchor. Either the DB chain was tampered (and the OBS anchor is the truth), or the anchor is just stale, checkworm_anchor.age_secondsto disambiguate.
What we do internally
Two CronJobs run continuously in our fremforge-prd cluster:
audit-chain-anchor, every 2 minutes. For each tenant with new chain rows, computesSHA256(prev_anchor_head || latest_chain_hash || count)and PUTs the anchor to OBS ataudit-chain-anchors/<tenant_id>/<timestamp>.json. Also overwrites alatest.jsonpointer.audit-chain-anchor-verifier, every hour. Walks each tenant’s anchor chain forward from genesis, recomputes the head at each step, asserts continuity. Any mismatch pages oncall onfremverk-oncall-critical(SMS-tier).
Plus the keyword alarm fremforge-prd-audit-chain-anchor-stale (Sev-1) fires if any tenant’s anchor is older than 30 minutes, a leading indicator that anchor writes have stopped. Customer-facing impact is captured by DPA §11A, gaps are documented in our status page post-incident.
Pseudonymisation under GDPR
When you exercise Right-to-Erasure, audit events for your tenant are pseudonymised, not deleted: the actor field becomes [erased] and fields_json is reset to {}. The chain link (the hash) is retained so the chain remains verifiable; the PII is removed.
This means a fremforge audit-verify run against an erased tenant will show tenant_erased_count > 0 rows and verified_count + pre_chain_epoch_count + tenant_erased_count = walked_rows. The chain is intact; the contents are scrubbed. After the 3-year WORM-anchor retention window expires, the rows themselves are permanently deleted.
Independent verification
We’re happy to walk auditors through the chain end-to-end under NDA. Contact compliance@frem.sh for the technical assessment package.