Evidence trust model
An audit trail you could quietly rewrite is a story, not evidence. This page explains how the admission ledger is sealed, what an attacker with database access can and cannot do to it, and — because a trust page that hides its own limits is worthless — exactly where the guarantees end.
The problem
Every admission decision lands in PostgreSQL. A database is the right place to query them and the wrong place to trust them: anyone with write access could flip a denied to allowed after the fact, and a plain table would carry no trace. If your auditor asks "how do you know these records are what the webhook actually decided?", a backup schedule is not an answer.
How the ledger is sealed
- Every record carries its own hash. At the moment a decision is written, the operator computes a SHA-256 over the record's content — timestamp, namespace, pod, image, digest, verdict, reason, policy name and policy hash, break-glass flag — under a fixed, versioned field order. The hash is computed by the operator, not the database, so a verifier can always recompute it.
- Records are sealed under signed checkpoints. On a schedule (hourly by default, or every 10,000 records, whichever comes first) the leader replica takes the unsealed range, builds a Merkle root over the record hashes in order, and signs a checkpoint: range, count, root, timestamp — and the hash of the previous checkpoint, so the seals form a chain back to the first one ever written.
- The signing key does not live in the database. Checkpoints are signed with an Ed25519 key that the operator generates inside your cluster and keeps in the
attestkeep-evidence-keySecret. That split is the core of the design: rewriting ledger rows needs database write access, re-signing the seals needs cluster access to the Secret. One access is not enough — an attacker needs both, and those are different doors with different audit trails. - The same key signs your evidence documents (as DSSE envelopes, under a different signature domain so one signature can never be replayed as the other), and our licence server's certificate names your key — so a third party can tie a document to your installation without trusting us to say so.
What verification checks
When an evidence document is generated, the operator re-verifies the whole ledger — not just the report period — and the document's ledger section states the result:
- Chain: every checkpoint links to its predecessor's hash; a broken link is reported with the sequence number where it breaks.
- Records: every sealed record's content hash is recomputed and compared; edited rows are listed by id in
tampered_record_ids. - Roots and counts: each checkpoint's Merkle root and record count are recomputed from the rows now present; a deletion inside a sealed range changes the count and lands in
root_mismatch_seqs. - Signatures: every checkpoint signature is verified against the installation's public key; failures are listed in
signature_bad_seqs.
intact is true only when all four pass. We have run this against a live tamper, not just in theory: flip one verdict with SQL and the next document names that record's id; delete a sealed row and the checkpoint's count betrays it.
Where the guarantees end
This is a tamper-evidence scheme, not a tamper-proofing one, and you should know its boundaries as precisely as its strengths:
- The unsealed tail is counted, not protected. Records written since the newest checkpoint have hashes but no seal over them yet — a window of at most the seal interval (default one hour) or 10,000 records. The document reports the tail's size; it cannot vouch for its contents.
intactspeaks about what is sealed, not about coverage. An installation with sealing disabled, or a ledger with few checkpoints, verifies trivially. Readintacttogether withsealing_enabled,checkpointsandunsealed_tail— a document showingintact: true, checkpoints: 0is telling you there was nothing to check, and an auditor should treat it that way.- An attacker with both accesses wins. Database write access plus the cluster access to read the signing key defeats the scheme — history can be rewritten and re-sealed. The design's claim is that this requires compromising two separately-audited surfaces, not that it is impossible.
- Records from before 0.3.4 are outside the scheme. Rows written by earlier versions have no content hash; they are reported separately as
legacy_unhashedand are never sealed. The scheme protects history from the moment it was turned on. - The auditor receives a signed verdict, not the raw seals. The evidence document carries the verification result under the installation's signature; the checkpoint rows themselves are not exported today. Independent re-verification of the DSSE envelope, the report digest and the certificate chain is fully supported (see Verifying a report); independent re-execution of the Merkle verification would require database access.
Operational notes
- The
attestkeep-evidence-keySecret belongs in your backup scope — see Backup and restore. If it is lost, a new key is generated: new checkpoints sign with the new key, but checkpoints and evidence documents signed by the old key no longer verify against the installation's current key. Losing the key does not lose history; it loses the ability to vouch for old signatures. - The operator's own RBAC cannot delete the Secret — the role set carries no
deleteverb on Secrets. Deleting it requires your cluster credentials, which is exactly the kind of action your cluster audit log exists to record. - Sealing is controlled by
LEDGER_SEAL_INTERVAL(default 1h; 0 disables sealing and, with it, the tamper evidence — the evidence document says so viasealing_enabled).