Audit ledger and evidence
Read the audit ledger, verify its hash chain and signed checkpoints, detect tampering, export evidence bundles and stream records to a SIEM or WORM storage.
flowchart LR A[Governed call] --> B[Gateway decision] B --> C[(Audit ledger<br/>hash-chained JSONL)] C --> D[maqpna audit tail<br/>maqpna dev timeline] C --> E[maqpna audit verify<br/>chain + checkpoints] C --> F[maqpna audit export<br/>evidence bundle] C --> G[auditSinks<br/>SIEM] C --> H[audit.worm<br/>write-once storage]
Goal#
Read what your agents did, prove that the audit ledger has not been changed, hand an auditor a verifiable evidence bundle, and ship the ledger to your SIEM and to write-once storage (WORM).
The audit ledger is the append-only record of every decision, approval, revocation and result. Each audit record carries the SHA-256 hash of the previous one, so changing or removing a record breaks the chain. A checkpoint is a signed statement of the ledger head; with an Ed25519 checkpoint key that you hold, a third party can verify the ledger offline without trusting MAQPNA.
Prerequisites#
- The
maqpnaCLI. - A local MAQPNA from your first governed agent (
maqpna dev up), or an installation and an access token with theauditororadminrole. - For signed checkpoints: an Ed25519 signing key that you hold (step 4).
Steps#
1. Make a few governed calls#
On a local MAQPNA, make one allowed and one denied call so the ledger has something to show:
maqpna dev up --stub-llm
maqpna dev run -- maqpna call --server echo --tool echo --arg text=hello
maqpna dev run -- maqpna call --server echo --tool delete_resource --arg id=db-1 --arg namespace=kube-system
The local ledger is the file .maqpna/audit.jsonl. In a cluster it is the gateway's auditPath (a volume, or the PostgreSQL table audit_chain with state.backend: postgres).
2. Read the ledger#
Each line is one audit record. This is a real record of a denied call from a local run:
{"seq":3,"time":"2026-10-03T03:47:22.578756Z","session":"dev-c4d65a9e","namespace":"dev","agent":"coder","user":"you@localhost","server":"echo","tool":"delete_resource","argsSha256":"72cfc439561889f817e4d59f78457a8f9c8e2f4e0d68e856f148f0500b6dd0c4","decision":"deny","rule":"baseline-guardrails/never-touch-system-namespaces","reason":"system namespaces are off-limits to agents","approver":"","latencyMs":0,"costUsd":0,"prevHash":"f0e3d5a5b1fb681de7…","hash":"7b7c2025…","schema":2}
| Field | Meaning |
|---|---|
seq |
Position in the chain, from 1 |
session, namespace, agent, user |
Who made the call, and on whose behalf |
server, tool |
The tool (echo.delete_resource), or llm/<route> and the operation for a model call |
argsSha256 |
Hash of the arguments. The arguments themselves are not stored |
decision, rule, reason |
What the gateway did and which policy and rule decided it |
approver |
Who approved or denied a held call |
prevHash, hash |
The hash chain |
To follow the ledger of a running gateway, use maqpna audit tail. It reads GET /v1/audit/records with the admin API:
eval "$(maqpna dev env)" # local MAQPNA: gateway URL and admin token
maqpna audit tail --since 0
Output from a local run:
2026-10-03T03:47:22.425Z #1 dev/dev-10a2bb09 coder echo/echo allow baseline-guardrails default_action
2026-10-03T03:47:22.500Z #2 team-a/dev-d5c7b7ae reviewer echo/get_time allow baseline-guardrails default_action
2026-10-03T03:47:22.578Z #3 dev/dev-c4d65a9e coder echo/delete_resource deny baseline-guardrails/never-touch-system-namespaces system namespaces are off-limits to agents
2026-10-03T03:47:22.856Z #4 dev/dev-ae793f67 coder llm/stub/chat.completions allow - scope:models:stub; model:stub; usage:prompt=8,completion=44
Filter with --session, -n, --agent, --user, --decision or --trace (an OpenTelemetry trace ID), follow with -f, and print one JSON record per line with -f -o json. For one session's story, use the timeline (see sessions):
maqpna dev timeline --session dev-c4d65a9e
TIME KIND SERVER/TOOL DECISION DETAIL
23:47:22 tool_call echo/delete_resource deny system namespaces are off-limits to agents
dev-c4d65a9e: 1 deny, cost $0.0000 (maqpna dev timeline --last)
3. Verify the hash chain#
Verify a ledger file offline:
maqpna audit verify .maqpna/audit.jsonl
OK: 4 records, hash chain intact
Ask a running gateway to verify its own ledger:
maqpna audit verify --gateway "$MAQPNA_GATEWAY_URL"
OK: http://127.0.0.1:8080: 4 records, hash chain intact (head seq 4, hash 613bb5787165c625f89a025e31fed31a2fbdb88eac9938e230e512ea299e5a38)
With the PostgreSQL state backend, verify the shared chain directly. --postgres takes a file that holds the DSN; --schema (default maqpna) and --chain (default audit) select the chain:
maqpna audit verify --postgres /run/secrets/maqpna-state-dsn
In the console, Audit → Verify chain runs the same check on the gateway's ledger and shows the head sequence and hash:

4. Sign checkpoints with a key you hold#
A hash chain proves that records were not changed after the fact, but whoever holds the file could rewrite the whole chain. Signed checkpoints close that gap: the gateway signs the ledger head with an Ed25519 key, publishes the public key (JWKS) at /v1/audit/jwks, and anyone can check the signatures offline.
Generate a key:
maqpna keygen -out keys --name checkpoint
private key: keys/checkpoint.key
public key: keys/checkpoint.pub
kid: 9FHvKLplpeVjzWvxxFWPh1ehLcgi_pTY8-7Q7coiPjE
In a cluster, put the key in a Secret with the key audit-signing.pem and enable signing in your Helm values:
audit:
checkpointSigning:
enabled: true
secretName: maqpna-audit-signing
intervalSeconds: 60 # sign at least every 60 s
every: 0 # also sign after this many records (0 = gateway default 1000)
The gateway setting behind it is auditCheckpointKey, a key URI: file:///abs/path (PKCS#8 PEM, mode 0600 or 0400), pkcs11:… or kms://<provider>/<key-id>. The release binaries include only the file backend; pkcs11: and kms:// URIs are parsed but need a backend built into your gateway image (see sovereignty and confidential tiers).
Fetch a copy of the ledger with its checkpoints and public keys, then verify it offline:
maqpna audit fetch ledger --out ledger.jsonl
maqpna audit verify ledger.jsonl --jwks ledger.jsonl.jwks.json --checkpoints ledger.jsonl.checkpoints.jws
wrote ledger.jsonl (7 records, head 81dff220de43203a25b8270b39c4ad65bc6a1eafb00e46d521a4a8b87c8328c8)
wrote ledger.jsonl.checkpoints.jws and ledger.jsonl.jwks.json; verify with: maqpna audit verify ledger.jsonl --jwks ledger.jsonl.jwks.json
OK: 7 records, hash chain intact
OK: 2 Ed25519-signed checkpoints verified (last seq 7, kid 9FHvKLplpeVjzWvxxFWPh1ehLcgi_pTY8-7Q7coiPjE, 2026-10-03T03:48:49Z)
maqpna audit fetch jwks --out jwks.json and maqpna audit fetch checkpoints --out FILE fetch the parts separately. Give the auditor the JWKS through a channel you trust, not only next to the ledger.
The chart's older HMAC checkpoints (audit.hmacCheckpoints, key in MAQPNA_AUDIT_HMAC_KEY) are checked by audit verify when that variable is set (--checkpoint-key-env names a different one). HMAC checkpoints need the secret key to verify, so prefer Ed25519 for third parties.
5. Detect tampering#
Change one decision in a copy of the ledger and verify it:
sed 's/"decision":"deny"/"decision":"allow"/' .maqpna/audit.jsonl > tampered.jsonl
maqpna audit verify tampered.jsonl; echo "exit status $?"
TAMPERED: chain breaks at seq 3 after 2 valid records: audit: hash chain broken: seq 3 hash mismatch (content modified)
exit status 3
Removing a record is caught the same way:
TAMPERED: chain breaks at seq 5 after 4 valid records: audit: hash chain broken: seq 6, expected 5
audit verify exits 0 when the chain is intact, 3 when it is broken, and 1 on other errors, so you can run it in CI or a cron job.
6. Export an evidence bundle#
An evidence bundle is a verifiable package of audit records for one session, namespace, agent, user or time range:
# offline, from a ledger file
maqpna audit export .maqpna/audit.jsonl --session dev-c4d65a9e --out evidence.json
# from a running gateway (adds the signed checkpoints and JWKS when configured)
maqpna audit export --gateway "$MAQPNA_GATEWAY_URL" --session dev-c4d65a9e --out evidence.json
The bundle (schema maqpna.io/audit-evidence/v1) has these top-level fields: schema, generatedAt, frameworks, filter, range, recordCount, recordSchemas, ledgerRecordCount, chainHead, verified, decisionsByAction, toolsUsed, approvals, totalCostUsd and records. Exports from a gateway with checkpoint signing also carry signedCheckpoints and checkpointJwks. Filter with --session, --namespace, --agent, --user (gateway only), --since and --until (RFC 3339).
7. Stream records to a SIEM#
The gateway ships ledger records to SIEM sinks (auditSinks) with a durable cursor, at least once. Sink types are syslog (tcp:// or tls://), http and jsonl; formats are json, rfc5424-json, cef and ocsf. In Helm values:
audit:
sinks:
- name: siem
type: syslog
address: tls://siem.acme.eu:6514
format: rfc5424-json
tlsSecret: maqpna-siem-tls # ca.crt (and tls.crt/tls.key with mtls: true)
- name: ocsf
type: http
url: https://ingest.acme.eu/ocsf
format: ocsf
headersSecret: maqpna-siem-headers # key "headers": "Authorization: Splunk <token>"
Every sink connection goes through the residency-checking dialer. With sovereignty.enabled, sinks must use TLS and match sovereignty.allowedEgressHosts.
Check the sinks:
maqpna audit sinks
Output from a local run with one jsonl sink:
ledger head seq 7
NAME TYPE FORMAT CURSOR LAG DELIVERED ERRORS LAST ERROR
siem-file jsonl json 7 0 7 0
--max-lag N exits 3 when a sink lags by more than N records or reports an error; use it in monitoring. To backfill a sink from a ledger file, run the sink offline with the gateway's config:
maqpna audit stream .maqpna/audit.jsonl --config .maqpna/gateway.json --sink siem-file --from-seq 6
OK: sent 2 records from seq 6 to sink siem-file
8. Keep a write-once copy (WORM)#
The gateway can ship sealed ledger segments to an S3-compatible bucket with Object Lock in COMPLIANCE mode (MinIO, Ceph RGW or S3), so no one can delete or rewrite them during the retention period:
audit:
sink: worm
worm:
endpoint: https://minio.audit.svc:9000 # in-country
bucket: maqpna-audit # created with Object Lock enabled
region: eu-central-1
objectLockDays: 365
credentialsSecret: maqpna-audit-s3 # keys access-key and secret-key
Without WORM or gateway.auditStorage.persistent: true, the ledger lives on an emptyDir and is lost when the pod is rescheduled.
9. Build the third-party register#
For DORA Art. 28, list every MCP server, model route and agent-to-agent (A2A) peer that agents used, built from the ledger:
maqpna evidence register
maqpna evidence register --format csv --since 2026-01-01T00:00:00Z --out register.csv
Output from a local run:
KIND NAME HOST JURISDICTION RESIDENCY OK CALLS DENIED COST USD FIRST USE LAST USE
mcp-server echo 127.0.0.1 6 1 0 2026-10-03T03:47:22.425777Z 2026-10-03T03:48:45.514104Z
model-route stub 127.0.0.1 1 0 0 2026-10-03T03:47:22.856093Z 2026-10-03T03:47:22.856093Z
--include-unused also lists configured providers that were never called.
Verify#
maqpna audit verify FILEprintsOK: N records, hash chain intactand exits 0.- With checkpoint signing,
maqpna audit verify FILE --jwks JWKSalso printsOK: N Ed25519-signed checkpoints verified. maqpna audit sinks --max-lag 100exits 0.
Troubleshooting#
| Symptom | Cause | Fix |
|---|---|---|
TAMPERED: chain breaks at seq N |
A record was changed, removed or reordered, or the file was cut short | Compare with the WORM copy or a backup. Never rewrite the chain; see backup, restore and DR |
GET /v1/audit/jwks: HTTP 404: signed audit checkpoints are not configured (auditCheckpointKey) |
No checkpoint key on the gateway | Set audit.checkpointSigning (step 4) |
✗ expected exactly one FILE from audit export |
Neither a file nor --gateway given |
Pass the ledger file, or --gateway URL (the environment variable alone does not select gateway mode) |
HTTP 403 from audit tail |
Your token has no auditor or admin role, or a namespace-scoped auditor did not set -n |
Run maqpna whoami, then add -n NAMESPACE |
A sink shows ERRORS and its lag grows |
The SIEM is unreachable, or the residency dialer refused its host | Check LAST ERROR, the TLS Secret and sovereignty.allowedEgressHosts |
Next steps#
- Sessions: the timeline of one session.
- Usage metering and billing: reports signed with the same checkpoint key.
- Backup, restore and DR: back up the ledger and its checkpoints.
- Command reference:
maqpna audit verify,maqpna audit export,maqpna audit tail,maqpna audit sinks,maqpna evidence register.