Runbook: ledger divergence or tampering
What to do when the audit ledger fails verification, replicas disagree on its head, or a SIEM copy differs, from establishing what is intact to restoring without rewriting the chain.
Use this runbook when the audit ledger may be damaged or changed. The ledger is append-only and hash-chained: every record holds the hash of the one before it, and signed checkpoints sign the head at points in time. A break in the chain, or a record that no longer matches a signed checkpoint, means a crash, a storage fault, a misconfiguration or tampering. The rule: never rewrite the chain. Restore into a new ledger and keep the damaged copy as evidence.
Background: Audit ledger and Ledger format.
Symptoms#
maqpna audit verifyprintsTAMPERED: chain breaks at seq N after M valid recordsand exits 3.- Gateway replicas report different ledger heads.
- A gateway pod refuses to start because its local ledger is not a prefix of the shared chain
(
state.backend: postgres). MaqpnaAuditLedgerUnavailablefires: audit appends fail (maqpna_gateway_audit_errors_totalrising).- A SIEM or WORM copy has records the ledger does not have, or the other way round.
Impact#
- With
gateway.auditFailurePolicy: closed, calls are refused while audit appends fail: agents stop, evidence is kept. - With
open, calls continue without an audit record; you lose evidence for that time. - A chain break does not stop new appends by itself, but every evidence export over the broken range will fail verification until you restore.
Detection#
| Check | Command |
|---|---|
| The gateway verifies its own ledger | maqpna audit verify --gateway "$GW" |
The shared chain in PostgreSQL, including the indexed seq, hash, prev_hash columns |
maqpna audit verify --postgres dsn.txt --schema maqpna |
| A downloaded copy, with Ed25519-signed checkpoints | maqpna audit verify now.jsonl --jwks now.jsonl.jwks.json |
| Sink positions | maqpna audit sinks --gateway "$GW" |
| Append errors | MaqpnaAuditLedgerUnavailable, MaqpnaGatewayAuditCommitSlow |
GW is the gateway admin URL; you need the auditor or admin role. dsn.txt holds the state database DSN; never
put it on the command line.
Decide#
flowchart TD
A["verify fails or heads differ"] --> B["Step 1: what is intact?<br/>first bad seq, last good head"]
B --> C["Step 2: compare with backup,<br/>WORM, SIEM, checkpoints"]
C --> D{"Finding"}
D -->|"torn final line"| E["Crash during append:<br/>restart, verify again"]
D -->|"break mid-chain,<br/>checkpoints fail"| F["Security incident:<br/>restore, keep copy"]
D -->|"replicas differ,<br/>file backend"| G["Run 1 replica or<br/>move to postgres"]
D -->|"audit_chain columns<br/>disagree"| H["Restore schema,<br/>restrict DB role"]
D -->|"appends failing"| I["Fix storage or DB"]
Respond#
Step 1: Establish what is intact#
maqpna audit verify --gateway "$GW" # exit 3 if broken
maqpna audit verify --postgres dsn.txt --schema maqpna # state.backend=postgres only
maqpna audit fetch ledger --gateway "$GW" --out now.jsonl # also writes now.jsonl.checkpoints.jws and .jwks.json
maqpna audit verify now.jsonl --jwks now.jsonl.jwks.json
Write down the first bad sequence number and the last good head (sequence number and hash). Keep now.jsonl and its
companion files unchanged as evidence.
Step 2: Compare with independent copies#
- The last backup.
maqpna backup verify /backups/<latest>prints its head; every record up to it must be identical. - WORM segments (
audit.sink: worm) and SIEM sinks hold copies with their own cursors (maqpna audit sinks --gateway "$GW"). - Signed checkpoints. A checkpoint signs a sequence number and hash. A record before the last checkpoint that no longer hashes to it was changed after signing; a missing tail after it was truncated.
Step 3: Act on the finding#
| Finding | Likely cause | Action |
|---|---|---|
| Torn final line | Crash or full disk during an append | The gateway truncates a torn tail at start; restart the pod and verify again. |
| Chain break in the middle, checkpoints fail | The file or table was edited | Treat as a security incident. Restore (step 4) and keep the tampered copy. |
Two replicas with different heads, file backend |
Independent per-replica ledgers, expected with state.backend: file and more than one replica |
Run one replica, or move to state.backend: postgres (Scaling and HA). |
| A replica refuses to start: local ledger not a prefix | Its local mirror diverged from the shared chain | Keep a copy of the pod's ledger volume as evidence, then restore the mirror (step 4). |
audit_chain columns disagree with stored records |
Manual database writes | Restore the schema from backup; restrict the database role to the gateway. |
| Appends failing | Full or read-only volume, database down | Fix the storage, or follow the PostgreSQL outage runbook. |
For a suspected tampering, also open a security incident and follow the compromised agent runbook steps for preserving evidence.
Step 4: Restore#
Restore into a new ledger file or database, never on top of the damaged one. Always dry-run first:
maqpna restore /backups/<latest> --components ledger --ledger-out /restore/audit.jsonl --dry-run
maqpna restore /backups/<latest> --components ledger --ledger-out /restore/audit.jsonl
maqpna restore /backups/<latest> --components postgres --postgres-dsn-file dsn.txt
--components ledgerwrites the restored ledger to--ledger-out; copy it onto the gateway's audit volume. It refuses to replace a longer live ledger.--components postgresrunspg_restoreof the state schema. It refuses to roll back a database whoseaudit_chainis ahead of the backup.--forceoverrides both checks. Use it only when you have decided that the newer records are the damaged ones, and you have kept a copy.- Add
--verify-key backup-signer.pubto refuse a backup whose signature does not verify.
Records written after the backup and lost with the corruption cannot be recreated. The signed checkpoints and the WORM and SIEM copies document them.
Verify#
maqpna audit verify --gateway "$GW"
maqpna audit verify --postgres dsn.txt --schema maqpna
maqpna audit sinks --gateway "$GW" --max-lag 1000 # exit 3 if a sink lags or has errors
maqpna doctor -n maqpna-system --gateway "$GW" --component audit --strict
Troubleshooting#
Common problems while you run this runbook:
| Symptom | Cause | Fix |
|---|---|---|
TAMPERED: ... checkpoint file cannot be read on a downloaded copy |
MAQPNA_AUDIT_HMAC_KEY is set in your shell, so the HMAC .checkpoints file is required |
Unset it and verify with --jwks, or fetch the .checkpoints file too. |
audit verify --gateway returns HTTP 403 |
Your token has no auditor or admin role |
maqpna whoami; map your group in adminAuth.roles. |
restore refuses: live ledger is ahead of the backup |
The live ledger has records the backup does not | Expected. Restore into a new file with --ledger-out, or pass --force only after you kept a copy. |
--gateway/--postgres take no FILE |
A file argument together with --gateway or --postgres |
Use maqpna audit verify FILE for a file, --gateway or --postgres without one. |
| Replicas show different heads for a few seconds, then agree | Normal catch-up with state.backend: postgres (pollMillis) |
Nothing to do; investigate only if they keep differing. |
| Sinks report errors after a restore | Their cursors point past the restored head | Check maqpna audit sinks; dead-lettered records are in <auditPath>.sinks/<name>.deadletter.jsonl. |
Follow-up#
- File an incident record with the heads from steps 1 and 2, the first bad sequence number and the cause.
- If records were lost, attach the WORM or SIEM copies and the checkpoints that cover the gap.
- If the cause was storage, add alerts on volume fill and fsync latency;
MaqpnaGatewayAuditCommitSlowwarns early. - If it was tampering, review who can write to the ledger volume and the state schema.
- Run
maqpna dr drillto confirm backups still restore.
Next steps#
- Audit ledger guide
- Backup, restore and DR drill
- Audit ledger flow
- Command reference:
maqpna audit verify,maqpna restore