MAQPNADocs

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 deciding rule (<policy>/<rule>) and the approver when 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"]
  1. Each record's hash is the SHA-256 of its canonical JSON (sorted keys, no whitespace, UTC timestamps), with hash left out. Each record names the previous record's hash in prevHash.
  2. Changing, removing or reordering any record breaks the chain from that point; maqpna audit verify reports the first bad sequence number.
  3. 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 from GET /v1/audit/jwks; MAQPNA never holds the key.
  4. With the Postgres state backend, an append-only trigger also refuses UPDATE and DELETE on the audit_chain table, 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 in maqpna_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 with audit_unavailable; a held (non-streamed) result is released only after the completion record is durable. /readyz fails 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.