Skip to content
Caldyr
Audit & compliance

Cryptographically signed, tamper-evident audit trail.

Every action, from sessions and commands to secret access and policy edits, is hashed, linked to the previous entry, and signed with an Ed25519 key sealed in TPM hardware on self-host; on managed cloud, protected today by the same host-level controls as the rest of the service. A company admin verifies a range through GET /audit/verify, where the server recomputes every row's hash and checks its signature against the key on record; your company's public key is published at /audit/pubkey/:company_id.

prev_hash↓
prev_hash↓
prev_hash↓
verified↓

How it works

1
Action happens
A user spawns a session, an agent reads a file, an admin edits a policy.
2
Hashed & linked
Entry hashed (sha256) with a reference to the previous row's hash.
3
Ed25519 signed
On self-host, the signature is produced by a key sealed in TPM hardware that never leaves the chip. On managed cloud, by the same host-level controls as the rest of the service.
4
Verified on the server
A company admin asks GET /audit/verify for a row range; the server recomputes each row's hash and checks its Ed25519 signature against the signing key it holds for that chain.

Deep dives

Hash chaining
Each entry's prev_hash field is the sha256 of the prior row. Delete row N and rows N+1 onward all stop verifying: the chain is broken visibly.
Signing & TPM seal
On self-host, the signing key is sealed to your host's TPM hardware and unwrapped only inside the audit-signer process. On managed cloud, no hardware TPM or cloud KMS is configured yet: the key is protected by the same host-level controls as the rest of the service, not a dedicated key-management backend.
Paged verification
A verify without paging looks at 10,000 rows at most; if the range holds more it stops there, reports truncated and answers ok: false rather than passing a prefix off as the whole range. To cover a 1.2M-row chain, pass batch= and page through it: every page returns has_more, a next_after_id cursor and the prev_hash to thread into the next request, which is what moves the progress bar in the admin UI.

Verify the chain yourself

shell · verify CLI
$ caldyr audit verify --from 8400 --to 8500 chain intact ✓ entries verified : 101 total in range : 101 last hash : 9f8ce4d27a91…3a4e

Tamper-evident, not just tamper-resistant.

You don't get a promise that we won't change records; you get a chain that makes a change detectable. Every row is hash-linked and Ed25519-signed, so an edit inside a verified range breaks the link and the verify reports that row as tampered. Be clear about who runs the check: /audit/verify is our endpoint and we hold the signing key it checks against, so what a company admin gets is evidence to act on, not an independent proof. Your company's public key is published at /audit/pubkey/:company_id; no tool we ship verifies a chain with it yet.

Compare

CaldyrPlain DB logsAppend-only log
Tamper-evidentyesnono signature
Cryptographic proofEd25519 + sha256nonenone
Published public keyyes · /audit/pubkey/:company_idnono
Per-tenant signing keyTPM-sealed (self-host)noshared key
Paged verificationbatch paging · 10,000 capnoload all in memory

FAQ

A new key is generated. Pre-rotation rows stay verifiable with the old pubkey; post-rotation rows use the new one. Lost-without-rotation means the chain becomes append-only without future signatures, but everything signed up to that point is still verifiable.
An auditor verifies the chain with the Caldyr CLI: caldyr audit verify --from <n> --to <n>. The CLI needs an API token and calls our /audit/verify, which only a company admin of that company may call, so you either issue the auditor such a token or run the range for them; the signature check itself happens on the server, against the key on record. For long-term retention, export the log as CSV from the audit tab; on Enterprise the full signed log also streams to your SIEM or an S3 WORM bucket they control.
Signing is off the action's path. The audit row is written and committed first; a Node signer worker is then woken by a Postgres NOTIFY and signs it — one Ed25519 signature per row, serialised within each tenant chain, with a 30-second poll as a backstop. Until a row is signed, a verify reports it as pending.
Self-hosted on a machine with TPM 2.0: sealed to the chip, never exported in plaintext, unsealed only on that host. On the managed service the host has no TPM, so the seed is a file on that host readable by the signer's service account. Either way we hold the key and we run the verify, so a clean range is our attestation rather than an independent check you ran yourself; the public key for that chain is published at /audit/pubkey/:company_id, and nothing we ship today checks a chain against it without us.

Ready to govern your AI infrastructure?

Request access in minutes, or talk to us about your air-gap deployment.

Get started