Skip to main content
Private preview. fremforge is in private preview — invited customers only. Content is still subject to change. Request access →
Audit chain

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

ThreatCaught?
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 tamperedStorage-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 allThis 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-epoch sentinel 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 --human

Output 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:   true

Without --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 ok
  • 2, partial: there are chain-broken sentinel 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’s prev_hash doesn’t match the running chain. The output includes the first_break row 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, check worm_anchor.age_seconds to 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, computes SHA256(prev_anchor_head || latest_chain_hash || count) and PUTs the anchor to OBS at audit-chain-anchors/<tenant_id>/<timestamp>.json. Also overwrites a latest.json pointer.
  • 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 on fremverk-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.