← Back to blog
Launch·9 min read

PostQ Ledger is live

A per-org, hash-chained, hybrid-signed Merkle audit log for every key, signature, rotation, and policy event. Tamper-evident by construction. Verifiable offline with a 1.4 MB Go binary.

Yesterday we shipped PostQ Vault. Vault gave you a managed home for hybrid (ML-DSA + Ed25519) signing keys, BYOK into AWS KMS or Azure Key Vault, rotation, revocation, and a dashboard. Good. But every regulated buyer asked the same follow-up: can you prove that nothing was changed retroactively?

Today we’re shipping the answer: PostQ Ledger — a per-organization, append-only, hash-chained, hybrid-signed transparency log of every security-relevant event in your Vault. It is live in the dashboard now at /ledger.

What gets logged

Every Vault operation now writes a ledger entry, automatically:

  • key.created — new hybrid signing key minted
  • key.rotated — new key version generated, prior version retained for verify
  • key.revoked — key disabled (signs blocked, history preserved)
  • signature.issued — one hybrid signature produced (key id + payload digest, never the payload)
  • signature.verified — verify call against a stored or supplied key
  • vault.settings_changed — KMS provider swap, AWS / Azure config update, secret rotated

Each entry stores the actor (which API key), the subject (which Vault object), an issuedAt timestamp, and a JSON data bag. We deliberately do not store the cleartext you signed — only its content digest. The ledger proves that you signed, not what you signed.

The hash chain

Every entry has a seq(0, 1, 2, …) unique within your org and a prev_hashpointing at the previous entry. The entry’s own entry_hash is:

entry_hash = SHA-256( prev_hash || canonical_json(payload) )

canonical_jsonis RFC 8785-style: keys sorted at every level, no whitespace. Identical input always produces an identical hash, in any language. Genesis entry has prev_hash = 0x00…00 (32 zero bytes).

Insertion is concurrency-safe via a Postgres unique constraint on (org_id, seq)with a bounded retry loop — two racing signatures don’t collide; the loser just re-reads the tail and tries again.

Merkle checkpoints (signed tree heads)

A hash chain alone is fine, but to prove inclusion of an old entry without shipping the whole log we use an RFC 6962 Merkle tree over the entry hashes. Whenever you click “Seal now” (or call POST /v1/ledger/seal), PostQ:

  1. computes the Merkle root over entry_hash[0..tree_size-1],
  2. builds an STH = canonical-json of { treeSize, merkleRoot, issuedAt },
  3. signs that STH with your org’s ledger key — an mldsa65+ed25519 hybrid key auto-minted on first seal,
  4. persists the checkpoint and returns it.

The signing key is itself a normal Vault key, BYOK-eligible. If you rotate your KEK, you rotate your ledger’s root of trust too, and prior STHs remain verifiable against the prior public bytes embedded in the bundle.

Inclusion proofs

For any entry, the dashboard exposes a copy-proof button. Under the hood it calls:

curl -H "Authorization: Bearer $POSTQ_API_KEY" \
  https://api.postq.dev/v1/ledger/proof/<entry_id>

and you get back the entry, the smallest checkpoint that covers it, and the sibling-hash list that provesentry → merkle_root in O(log n) hashes. You can hand that to your auditor along with the signed STH and they can verify it without ever talking to PostQ.

Verify the entire log offline

From the dashboard, hit Download verifiable bundle. You get a single JSON file: every entry, every checkpoint, the public bytes of every signing key, and metadata. Then:

# v0.5.0+ of the postq CLI
postq ledger verify bundle.json

# PostQ Ledger bundle — org=acme generated=2026-04-27T18:14:22Z
#   entries:     1842
#   checkpoints: 17
#   signers:     1
# ✓ hash chain intact (1842 entries)
# ✓ all 17 checkpoints' merkle roots match the entry chain
# bundle intact.

The verifier is pure Go stdlib — no liboqs, no CGO, no network. It re-walks the entire chain, recomputes the Merkle root for every checkpoint, and exits non-zero if a single byte was changed. The ML-DSA half of the STH signature is left to the SDKs / API to keep the CLI a single 1.4 MB static binary you can drop into any CI runner or air-gapped review machine.

Why this matters

Auditors don’t want to trust your access logs. They want a structure where retroactively editing a row is mathematically detectable. With PostQ Ledger, splicing a fake signature.issued entry into your history three months back would change every downstream entry_hash, every subsequent prev_hash, and every Merkle root in every STH issued since. You’d need to re-sign every checkpoint with the org’s ML-DSA key — which lives in your KEK’d Vault.

That is the same architecture used by Certificate Transparency, by Sigstore’s Rekor, by Git itself. We just wrapped it around the things a security team actually cares about: who signed what, with which key, when, and under whose policy.

What’s next

  • Consistency proofs (STHn → STHm) so you can prove the log only ever grew.
  • Optional witness co-signing — ship STHs to a third-party witness service so even PostQ can’t silently fork your log.
  • Webhook fan-out: get a POST every time a new STH is sealed, so your SIEM has a live monitor.

Open the dashboard, head to Ledger, click Seal now, download the bundle, run postq ledger verify. That’s the whole demo.