The membrane UI has a fast tail-verify indicator good enough for a live health signal. This runbook is what an auditor uses to prove tamper-evidence against a snapshot, without trusting any endpoint the audited entity controls.
Independence is the point. Every recipe runs against the audit database file directly. None call an endpoint the audited entity controls. The entity cannot choose what the auditor sees.
Every fasten audit row carries prev_hash and hash, chained by SHA-256 over a versioned canonical form of the row. A verifier reads the rows in monotonic_seq order and confirms each row's prev_hash equals the previous row's hash. When the chain verifies clean, one of two things is true:
When it does not verify, first_break_at names the first offending monotonic_seq. Every row before that seq remains trustworthy; every row at or after must be re-ingested from the upstream source before it can be relied on.
fasten installed. pip install fasten or from source at github.com/nerdapplabs/fasten.Three recipes. Each is a collapsed card. Click to expand.
The auditor's default. Reads the whole chain, no windowing.
python -c 'import fasten; from fasten.store.sqlite import SQLiteStore; \
s=SQLiteStore("./membrane-audit.db"); \
rows=s.query(limit=10_000_000); \
r=fasten.verify_chain(rows); \
print({"ok": r.ok, "rows": r.total_rows, "first_break_at": r.first_break_at, "reason": r.reason})'
Output on a clean chain:
{'ok': True, 'rows': 1284, 'first_break_at': None, 'reason': None}
Output when a row has been altered:
{'ok': False, 'rows': 1284, 'first_break_at': 512, 'reason': 'hash mismatch at seq 512'}
For CI or an audit job. Exits 0 on clean, 2 on a break, 3 on any other failure (DB missing, permission, corrupt page).
#!/usr/bin/env python3
"""Offline verifier for a fasten audit DB. Exit 0 clean, 2 broken, 3 error."""
import argparse, sys
import fasten
from fasten.store.sqlite import SQLiteStore
p = argparse.ArgumentParser()
p.add_argument("--db", required=True, help="path to the audit sqlite file")
p.add_argument("--limit", type=int, default=10_000_000)
args = p.parse_args()
try:
store = SQLiteStore(args.db)
rows = store.query(limit=args.limit)
result = fasten.verify_chain(rows)
except Exception as exc:
print(f"error: {exc.__class__.__name__}: {exc}", file=sys.stderr)
sys.exit(3)
print(f"rows checked: {result.total_rows}")
if result.ok:
print("chain: CLEAN")
sys.exit(0)
print(f"chain: BROKEN")
print(f"first_break_at: {result.first_break_at}")
print(f"reason: {result.reason}")
sys.exit(2)
Run it against a real audit snapshot:
python verify_chain.py --db ./snapshots/2026-07-09/membrane-audit.db # rows checked: 1284 # chain: CLEAN echo $? # 0
Once you have a scripted verifier, run it every night against the latest audit snapshot and fail the build on non-zero exit. Example GitHub Actions step:
jobs:
verify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: {python-version: "3.12"}
- run: pip install fasten
- name: Snapshot the audit DB from prod
run: aws s3 cp s3://fasten-prod-audit/latest.db ./membrane-audit.db
- name: Verify the chain
run: python verify_chain.py --db ./membrane-audit.db
Snapshot to your own storage first so the verifier reads a file the audited entity cannot touch. A supervisor who receives the snapshot can rerun the same verifier locally.
The recovery path depends on where the break is and what caused it.
first_break_at. Rows with monotonic_seq less than this value remain trustworthy.(request_id, code, target), so replaying the source stream from first_break_at onward will re-seal the chain from a known-good prefix.| Class | Caught | Notes |
|---|---|---|
| Row content modified in place | yes | hash no longer matches the canonical form; break at that seq |
| Row inserted between two existing rows | yes | the next row's prev_hash will not match the inserted row's hash |
| Row deleted from the middle | yes | the following row's prev_hash refers to a hash no longer present |
| Tail rows truncated | partial | chain still verifies as a shorter prefix; compare row count against a witnessed baseline |
| Entire DB replaced with a rewritten chain | partial | a self-consistent rewrite verifies clean; defeat by publishing periodic Merkle roots to a third party (see below) |
A linear chain proves that no one has tampered with the rows the chain names. It does not prove that no one has replaced the whole DB with a fresh, self-consistent one. That threat is defeated by publishing a Merkle root of the audit window to a party outside the audited entity. The same construction Certificate Transparency uses.
fasten's signed evidence-pack export ships an RFC 6962 Merkle root and per-row inclusion proofs, so an auditor can hold a single 32-byte commitment and verify any individual row's presence without downloading the whole log. The linear chain in this runbook is the substrate under that. The evidence-pack API is the layer above.
Questions or a stuck verifier? Open an issue at github.com/nerdapplabs/fasten/issues or reach us via nerdapplabs.com.