Audit & Verification

A Server exposes two separate audit surfaces. They answer different questions, and an auditor treats them differently. Read this page as two runbooks under one cover.

| Surface | What it holds | Who reads it | Question it answers | |---|---|---|---| | system_audit_log | Operational events — orgs created, records written, keys rotated, admin reads | Your SIEM / SOC, continuously | "What is happening on this Server?" | | audit_vault | The signed, hash-chained records themselves | An auditor, offline | "Is this record authentic and unaltered?" |

The first surface is operational telemetry: it tells you the Server is behaving. The second is the proof: it stands on its own cryptography, so an auditor can verify the chain without trusting — or even reaching — the Server that produced it. Stream the first into your SIEM. Hand the second to an auditor. Do not substitute one for the other.

Surface 1 — Stream operational events to your SIEM

Poll GET /v1/siem/stream on an interval and forward the result to your SIEM. The endpoint merges the event stream and the operational system_audit_log into one time-ordered feed. It requires a key with the audit:read scope (see the API reference for scope details).

Parameters: since (ISO-8601, required), limit (default 100), format (ocsf default, or raw).

curl -s -H "Authorization: Bearer $AGLEDGER_KEY" \
  "$AGLEDGER_URL/v1/siem/stream?since=2026-05-01T00:00:00Z&limit=5&format=raw"
{"type":"admin.org_bootstrapped","payload":{"name":"Default","reason":"single-org-install","actorId":"00000000-0000-0000-0000-000000000000","actorRole":"platform","targetType":"org","targetId":"019e61d3-c074-73cf-b14b-50b5e94c6845"},"timestamp":"2026-05-26T01:08:47.859Z","id":"019e61d3-c075-731e-ad1b-8ccae604b91f"}
{"type":"record.created","payload":{"type":"notarize-generic-v1","orgId":"019e61d3-c074-73cf-b14b-50b5e94c6845","status":"RECORDED","recordId":"019e61d4-fbf7-7205-8e4c-b17baf4b6f64","performerAgentId":"019e61d4-fbb9-780f-b110-8a64ab46920f","agentId":null},"timestamp":"2026-05-26T01:10:08.629Z","id":"019e61d4-fc13-73c8-b852-ff69696f61b3"}

Set format=ocsf to emit OCSF 1.4.0 events your SIEM maps natively (Splunk, Elastic, Sentinel):

curl -s -H "Authorization: Bearer $AGLEDGER_KEY" \
  "$AGLEDGER_URL/v1/siem/stream?since=2026-05-01T00:00:00Z&limit=1&format=ocsf"
{"metadata":{"product":{"name":"AGLedger","vendor_name":"AGLedger","version":"1.0.0"},"version":"1.4.0","log_name":"audit","uid":"9f0e1748-c6e8-46a7-8e25-9eee76486de0"},"time":"2026-05-26T01:08:47.859Z","severity_id":1,"class_uid":3004,"category_uid":3,"type_uid":300403,"activity_id":3,"status_id":1,"message":"admin.org_bootstrapped","actor":{"user":{"uid":"00000000-0000-0000-0000-000000000000","type":"platform"}},"entity":{"uid":"019e61d3-c074-73cf-b14b-50b5e94c6845","type":"org","data":{"name":"Default","reason":"single-org-install"}}}

To run a continuous poller, advance since to the time of the last event you forwarded on each pass and keep limit modest. The feed is operational telemetry — it is not the tamper-evident proof. A record that appears here has not been independently verified by appearing here; that is the job of Surface 2.

Surface 2 — Verify the chain offline (the real audit)

The audit of record is performed off the Server, against only the published public keys. The verifier has no database, no network, and no AGLedger engine in its dependency tree — so it remains trustworthy even if the Server that produced the chain is later compromised. This is the default posture for a serious audit, and it is fully air-gapped.

Three steps: publish the keys, produce a dump, verify it.

Step 1 — Publish the verification keys

The public signing keys are served unauthenticated and always on. An auditor needs only these.

curl -s "$AGLEDGER_URL/v1/verification-keys"
{"data":[{"keyId":"affc2b9bfb22144e","algorithm":"Ed25519","publicKey":"MCowBQYDK2VwAyEAV5QgmxAZx9R+DaE1BOhPqt4JQ/c7gDUJ1bQ4zdwiCoE=","publicKeyRaw":"V5QgmxAZx9R+DaE1BOhPqt4JQ/c7gDUJ1bQ4zdwiCoE=","status":"active","activatedAt":"2026-05-26","retiredAt":null}],"envelope":"COSE_Sign1","payloadFormat":"application/vnd.in-toto+cbor","canonicalization":"RFC8949-CDE","coseAlgorithm":-8,"signatureAlgorithm":"Ed25519"}

The same key set is also published at GET /.well-known/agledger-vault-keys.json. Retired keys stay in the set with a retiredAt date so records signed before a rotation still verify — the key registry is part of the dump in Step 2, so the auditor never has to ask which key signed what.

Step 2 — Produce a dump

pnpm vault:dump is the one component that touches Postgres. It writes a self-contained set of NDJSON files the verifier consumes. Run it on the Server (or anywhere with DATABASE_URL reach), then hand the directory to the auditor.

DATABASE_URL=postgresql://… pnpm vault:dump ./dump
{
  "outDir": "./dump",
  "orgId": null,
  "counts": {
    "audit_vault": 4,
    "vault_checkpoints": 0,
    "vault_signing_keys": 1,
    "org_admin_reads": 0,
    "org_admin_reads_checkpoints": 0
  }
}

Pass --org <id> to scope the dump to a single org. The output directory holds five files:

audit_vault.ndjson                 the per-record hash chains
vault_checkpoints.ndjson           periodic signed checkpoints over the chains
vault_signing_keys.ndjson          the public-key registry, with rotation history
org_admin_reads.ndjson             the cross-party admin-read log
org_admin_reads_checkpoints.ndjson signed tree heads over the read log

The dump is database-independent. Keep a copy alongside your database backup — it is the artifact an auditor verifies, and it does not depend on a live Server to be meaningful. (See the backup runbook for where this fits in a backup schedule.)

Step 3 — Verify with zero dependencies

The @agledger/verify offline verifier (binary agledger-verify) re-derives every hash and link and checks every Ed25519 signature against the dumped key registry. It exits 0 on a clean chain, 1 on any failure.

agledger-verify ./dump
[PASS] AGLedger offline verification

audit_vault chain
  records    : 4
  entries     : 4
  checkpoints : 0
  failures    : 0

org_admin_reads chain
  orgs       : 0
  leaves            : 0
  checkpoints       : 0
  witness cosigned  : 0
  failures          : 0

--report-format json emits the machine-readable form for a CI gate:

agledger-verify ./dump --report-format json
{
  "ok": true,
  "vault": { "recordCount": 4, "entryCount": 4, "checkpointCount": 0, "failures": [] },
  "orgAdminReads": { "orgCount": 0, "leafCount": 0, "checkpointCount": 0, "witnessCosignedCheckpoints": [], "failures": [] }
}

records: 4 counts chains, not API records: one chain per record plus the schema-registration chain. For an air-gapped audit, vendor the @agledger/verify package onto the auditor's machine once — it pulls nothing at runtime.

When verification fails

A failure is the verifier doing its job. Each failure carries a code naming the integrity class, the chain it occurred on, and the position. For example, altering a single stored hash:

{
  "ok": false,
  "failures": [
    {
      "code": "CHAIN_HASH_MISMATCH",
      "message": "RecordRow 019e61d4-fbf7-7205-8e4c-b17baf4b6f64 pos 1: sha256(cose_sign1) does not match stored payload_hash",
      "scopeId": "019e61d4-fbf7-7205-8e4c-b17baf4b6f64",
      "position": 1
    }
  ]
}

Integrity classes on the per-record chain:

| Code | Means | |---|---| | CHAIN_GENESIS_INVALID | The first entry of a chain does not link to genesis | | CHAIN_POSITION_GAP | A chain position is missing — an entry was removed | | CHAIN_LINK_BROKEN | An entry's previous_hash does not match the prior entry | | CHAIN_HASH_MISMATCH | The stored hash does not match sha256(cose_sign1) | | CHAIN_SIGNATURE_INVALID | The Ed25519 signature does not verify | | CHAIN_SIGNATURE_MISSING_KEY | The signing key is not in the published registry | | CHAIN_COSE_DECODE_FAILED | The signed envelope is not decodable | | CHAIN_COSE_HEADER_MISMATCH | Chain mechanics in the protected header disagree with the row | | CHAIN_PAYLOAD_DRIFT | The denormalized row diverges from the signed payload | | CHAIN_OIDC_ACTOR_MISMATCH | The recorded actor identity disagrees with the signed claim |

Checkpoint and admin-read chains report their own classes (CHECKPOINT_*, TENANT_READ_*, TENANT_CHECKPOINT_*) on the same model.

Note what does not fail verification: editing the convenience JSON in audit_vault.ndjson without touching the signed envelope. The verifier trusts the signed cose_sign1 artifact as the source of truth, not the denormalized columns — so a privileged-database edit of the readable payload is caught as CHAIN_PAYLOAD_DRIFT, not silently accepted.

On-box reads versus the off-box handoff

An operator can read the chain on the Server through the admin vault endpoints (see the API reference for /v1/admin/vault/*). Every cross-party admin read is itself notarized into the org_admin_reads chain — reading the chain is an accountable act, and it appears in the dump above.

An auditor does the opposite: they take the dump off the Server and verify it on their own machine with only the public keys. On-box reads are for operations. The off-box handoff is the audit. Keep the two roles distinct.

Optional — the SCITT transparency checkpoint

If you operate the SCITT transparency surface (POST /v1/scitt/entries), GET /v1/scitt/checkpoint returns the current signed tree head for an org's transparency log — a stable anchor an external monitor can pin and re-check for consistency over time.

curl -s -H "Authorization: Bearer $AGLEDGER_KEY" "$AGLEDGER_URL/v1/scitt/checkpoint"
{"treeSize":0,"rootHex":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","logId":"019e61d3-c074-73cf-b14b-50b5e94c6845","iat":1779757937,"kid":"affc2b9bfb22144e","signature":"c7c1b03e98287883b01d6daec7623b8297658100cab7ce644bbd353268b20d89ba06ca4c9486b7ef196f2ec3ef8f3bacc6c271eaa85d3920d11847ac3bc94e04"}

A treeSize of 0 with the empty-tree root above is a fresh log with no SCITT entries registered yet. This surface is independent of the audit_vault chain in Surface 2: the offline verifier is the proof of the record chain; the SCITT checkpoint is the anchor for the separate SCRAPI transparency log.


Validated against API v0.25.4 on 2026-05-25.