The audit ledger#
The audit ledger is the append-only, hash-chained record of every decision the gateway makes: every allowed, denied and held call, every approval, revocation, taint change, session result and administrative action. One entry is an audit record. A checkpoint is a signed statement of the ledger head at a point in time. To verify the ledger is to recompute the hash chain and signatures and prove nothing was changed, removed or reordered.
The ledger is evidence, not a log: it is designed to support the record-keeping and human-oversight duties of the EU AI Act (Art. 12, 14 and 19) and security logging under the Cyber Resilience Act. It helps you document; it does not make you compliant on its own.
What a record holds#
{"seq":1042,"time":"2026-10-02T12:03:11.204Z","session":"fix-4821","namespace":"team-a","agent":"coder",
"user":"alice@acme.eu","server":"kubernetes","tool":"delete_pod","argsSha256":"9f2c…","decision":"allow",
"rule":"prod-guard/k8s-delete","reason":"approval:approved by bob@acme.eu (apr_…)","approver":"bob@acme.eu",
"latencyMs":412.3,"costUsd":0.0004,"prevHash":"ab31…","hash":"77e0…","schema":2,
"ext":{"approvers":"bob@acme.eu,carol@acme.eu","traceId":"4bf92f3577b34da6a3ce929d0e0e4736"}}
- Raw arguments are never stored. Only
argsSha256, the SHA-256 of the arguments, which proves what was called without keeping personal data. Prompts and completions are never stored either. - Who, for whom, under which rule. Agent, session, the person the agent acts for (
user), the decidingrule(<policy>/<rule>) and theapproverwhen a human decided. - Governance facts in
ext: DLP detector counts, taint labels, the tool definition hash, approvers and their issuers, the revocation ID, A2A hops and delegation chain, trace IDs, tenant and fork lineage. See the ledger record format.
How tampering is detected#
flowchart LR
G["genesis<br/>prevHash = 64 zeros"] --> R1["record seq 1<br/>hash = SHA-256(canonical JSON)"]
R1 -- "prevHash" --> R2["record seq 2"]
R2 -- "prevHash" --> R3["record seq 3"]
R3 -- "prevHash" --> RN["… record seq N"]
RN -.-> C["Checkpoint<br/>Ed25519 JWS over {seq, hash, time, kid}<br/>signed with your key"]
- Each record's
hashis the SHA-256 of its canonical JSON (sorted keys, no whitespace, UTC timestamps), withhashleft out. Each record names the previous record's hash inprevHash. - Changing, removing or reordering any record breaks the chain from that point;
maqpna audit verifyreports the first bad sequence number. - A forger who rewrites the whole chain consistently is caught by checkpoints: Ed25519-signed JWS statements of the head (
auditCheckpointKey, a key you hold), written every 60 seconds or 1,000 records, or HMAC checkpoints (MAQPNA_AUDIT_HMAC_KEY). Auditors verify signed checkpoints offline with the public keys fromGET /v1/audit/jwks; MAQPNA never holds the key. - With the Postgres state backend, an append-only trigger also refuses
UPDATEandDELETEon theaudit_chaintable, and every gateway replica keeps a verified, byte-identical local mirror. A mirror that diverges from the database stops the replica rather than re-sealing tampered rows.
Where the ledger goes#
| Destination | How | Notes |
|---|---|---|
| Local file | JSONL at auditPath, fsynced per append (group commit) |
Default; the local mirror in HA mode |
| PostgreSQL | Table audit_chain, one chain under an advisory lock |
stateBackend: postgres; all replicas share one chain |
| SIEM | auditSinks[]: syslog (TCP or TLS), HTTP or JSONL; formats json, rfc5424-json, cef, ocsf |
At-least-once from a durable cursor; one elected replica ships |
| Write-once storage (WORM) | S3-compatible store with Object Lock COMPLIANCE, SigV4, retention from auditRetentionDays |
In-country through the residency dialer |
| Tenant ledgers | One independent chain per tenant | Each tenant verifies its own chain |
| Evidence bundle | maqpna audit export or GET /v1/audit/export |
Schema maqpna.io/audit-evidence/v1, with signed checkpoints and the public keys |
Audit failure policy#
auditFailurePolicy decides what happens when the ledger cannot be written:
open(default): the call proceeds; the failure is logged and counted inmaqpna_gateway_audit_errors_total.closed(production and sovereign profiles): a durable intent record must be written before an allowed call is forwarded, or the call is refused withaudit_unavailable; a held (non-streamed) result is released only after the completion record is durable./readyzfails so the replica is drained.
What you see#
maqpna audit verify checks a ledger file offline and exits 3 when it was tampered with (formats from cmd/maqpna/audit.go; values illustrative):
$ maqpna audit verify audit.jsonl --jwks jwks.json
OK: 18204 records, hash chain intact
OK: 19 Ed25519-signed checkpoints verified (last seq 18000, kid 6YkqgC1nL0Vt3pXb8m2QfJ4sR9wZ7aHdE5uK1yN0cTo, 2026-10-02T14:05:09Z)
$ maqpna audit verify audit.jsonl
TAMPERED: chain breaks at seq 1043 after 1042 valid records: audit: hash chain broken: seq 1043 hash mismatch (content modified)
maqpna audit verify --gateway URL asks a running gateway to verify its view, and --postgres DSN-FILE reads the shared chain directly. See maqpna audit verify, maqpna audit export, maqpna audit tail and maqpna evidence register.