Encryption at rest, and an audit trail a third party can check
Klarum makes two promises about customer data and they are not equally strong. Confidentiality rests on encryption at rest under keys we hold: an operational control, not a mathematical one, and it leaves us able to read customer documents. Verifiability rests on a tamper-evident record and a signed root, and part of it can be checked by an auditor who trusts us with nothing at all. This note summarises where the line between the two falls, and what is still missing on each side of it.
Published
Confidentiality and Verifiability, v1.3 (PDF)The full document this note summarises, including the key-management position, the defect we have not yet fixed, and a revision history recording a claim we retracted after external review.Contents
Two promises, and why they are not equally strong
A procurement team evaluating us is really asking two questions, and they tend to arrive as one. The first is who can read our documents. The second is whether the record of what we did can be altered afterwards. Both are reasonable, both have an answer, and the answers have very different strength, so the useful thing we can do is keep them apart.
Confidentiality is the first question. Ours rests on encryption at rest under keys Klarum holds, plus tenant isolation in the database. That is a strong operational control against the threats it exists for, and it is not a guarantee against Klarum itself. Verifiability is the second question. Ours rests on a tamper-evident record, a signature, and a root published to a public blockchain, and this is the one a customer can check without taking our word for anything. Conflating the two is the most common way a security statement stops being true, so the whitepaper this note summarises[1] puts them in separate sections and states the boundary of each. We do the same here.
What is encrypted, and who holds the key
Document bytes are encrypted with AES-256-GCM using envelope encryption, and the encryption sits inside the storage client rather than in any one feature, so every read and every write inherits it. Each object gets its own key, which is itself wrapped under a key we hold, and the recipe travels in the object metadata. The practical consequence for a customer is that turning it on or off is a setting rather than a migration: objects written before and after the change stay readable side by side. It is offered per organisation, not imposed, and there is a separate operator switch for incidents that does not overwrite the preference a customer has stated.
One detail is worth naming because it is the one most often left out. The storage location of an object is bound into the ciphertext itself. Without that binding, an object copied wholesale onto the storage location of another tenant would decrypt cleanly under the wrong identity. With it, the copy does not decrypt at all, and there is a test that performs exactly that copy and asserts the failure. A misdirected object is unreadable rather than merely misfiled.
What this is not. Klarum holds the key that unwraps the others, so Klarum can read customer documents. This is not zero-knowledge encryption, it is not end-to-end, and no key material is generated in or held by the browser. What it defends against is exposure of the object store: a stolen storage credential, a stray backup, an object that ends up somewhere it should not. Those are the realistic threats and this is the control for them. It is not a control against an attacker who already holds the running platform, and we do not offer it as one.
Plaintext also exists in places encryption at rest does not reach, and the whitepaper lists them rather than drawing a diagram that omits them: in transit to us under TLS, in the platform memory while a document is being processed, and briefly in a temporary file on the two remaining paths where a document parser will only accept a filesystem path. That temporary area is now a bounded, memory-backed mount that the service verifies at start-up, so a staged document never reaches a disk. That closes the disk copy and nothing more. Process memory holds plaintext on every request either way.
What an auditor can check without us
Auditable events are appended to a hash chain, where each entry commits to the hash of the one before it. Editing or removing an entry after the fact breaks every entry that follows, which is what makes the record tamper-evident rather than merely stored. Batches of entries are then hashed into a single root following the construction in RFC 6962[2], the same one Certificate Transparency uses, and each root is signed with Ed25519 under a key held outside the database. The public half of that key is published at a fixed URL, on the reasoning that a signature nobody can fetch the key for proves nothing.
Order is preserved in that construction, and that is the point of choosing it. A receipt does not say only that an event exists somewhere in the set. It says the event was the third one, unaltered, with nothing inserted before it. For an audit question of the form "was this approval recorded before that submission", the difference between those two statements is the whole answer.
A receipt is self-contained: the entry, its path through the tree, the root, the recipe and the signature. It is checked with a standalone script that uses only the Python standard library, makes no network calls, and shares no code with the platform on purpose, so a defect in our implementation cannot hide inside the tool that checks it. A customer can hand the receipt and the script to an auditor, and the auditor runs the check without contacting us.
What that check establishes is inclusion: the entry is one of those the root was built over, at that position, unaltered. Whether the root itself is legitimate is a second question, and the answer differs by chain. Roots for one of the three chains are also published to a public blockchain, where the contract that stores them verifies proofs on-chain, and we have run that end to end: a genuine proof passes, an entry altered by one byte fails, and so does a correct entry claimed at the wrong position. That chain is on a public test network today, which is a real limit rather than a detail, and the limitations below say why.
Where documents go to be processed
Search and matching turn document text into vectors, which means the text is sent to an inference provider. That provider runs on an EU inference profile, so text sent for embedding does not cross out of the EU, and the models behind drafting, extraction and summarisation are pinned the same way. The conversational assistant is the exception and we state it rather than bury it: its model selection can currently fall back to a profile that may route outside the EU, and it is tracked as its own question.
Region is not retention, and the two get conflated routinely. Choosing an EU profile answers where inference happens. It does not answer what the provider keeps, and that answer has to come from written processor terms and from service configuration an auditor can be shown, not from our own source code. Until those are cited and confirmed, we make no retention claim in either direction.
The vectors themselves are stored, not discarded after use. They live in Postgres through pgvector[3], scoped to the tenant that owns the record they describe, and deleting the record removes the basis for its vector. We treat a vector as personal data rather than as anonymous telemetry, because partial reconstruction of source text from an embedding is a documented attack class[4] rather than a hypothetical one. An earlier edition of our own whitepaper described an embedding as not reversible to its source text. That was wrong, it was retracted, and the retraction is recorded in the revision history instead of being quietly deleted.
| Property | Status |
|---|---|
| AES-256-GCM envelope encryption of documents at rest | Implemented, opt-in per organisation |
| Storage location bound into the ciphertext | Implemented |
| Zero-knowledge or client-side encryption | Not implemented; Klarum can read customer documents |
| Encryption key in a managed key service, with rotation | Not implemented; the key is in application configuration |
| Encryption policy fails closed if it cannot be resolved | Defect: it currently fails open |
| Embedding inference stays within the EU | Implemented |
| Conversational assistant inference pinned to the EU | Not yet |
| Provider retention position, with terms cited | Not verified |
| Vectors encrypted at column level | Not implemented |
| Tamper-evident hash chains | Implemented |
| RFC 6962 roots, Ed25519 signed | Implemented |
| Offline verification of a receipt by a third party | Implemented |
| Root publication to a public chain | Testnet only; not durable evidence |
| Per-workspace audit trail independently verifiable | Not yet; we hold the only signing key |
| Mainnet anchoring, qualified electronic signatures | Roadmap |
Discussion
Limitations
The weakest part of the confidentiality story is key management, not the cipher. The key that wraps every document key lives in application configuration rather than in a managed key service, anyone who can read that configuration can read the key, and there is no path today for re-wrapping objects under a new one. The mechanism for holding several keys at once exists, so rotation is possible in principle, but without a re-wrap path retiring a key would mean losing access to everything encrypted under it. The honest way to say that is that the current key lifetime is unbounded, and closing it is engineering work we have not done.
There is also a defect we would rather state than have found. If the platform cannot read the setting that says whether an organisation asked for encryption, it currently proceeds with an unencrypted write. The original reasoning was that an upload should not fail because an unrelated lookup did, and that reasoning was wrong: a confidentiality control has to fail closed, and uncertainty about a customer preference is exactly when it should refuse rather than guess. The planned change is to refuse the upload. Until it ships, we do not claim this control fails secure.
On the verifiability side, the limitation is narrower than the mechanism suggests. Only one chain is published to a public blockchain today, and only to a public test network. A test network has no continuity commitment and its state can be reset, so a root anchored there is not a durable third-party timestamp, which is precisely the horizon over which an audit trail matters. It demonstrates that the mechanism works end to end. It is not evidence, and we do not present it as evidence.
That leaves the trust assumption, which is the most important sentence in the whitepaper and the easiest to lose in a summary. For a per-workspace audit trail, Klarum holds the records and the only signing key. An actor with both could rewrite a history, recompute a root over the rewritten version, sign it, and produce something internally consistent. The chain and the signature force such a rewrite to be wholesale rather than piecemeal, which is worth having, and they do not remove the assumption. So for those chains the verifiable claim is that Klarum signed a batch containing this entry, not that the customer performed this action at this time, and nothing we publish may describe the per-workspace audit trail as independently verifiable.
What we would do next
Two items, in the order they change what a customer can rely on. A signature the customer controls, generated in their own browser and never exportable, would mean authorship no longer depends on our key at all, which is the only thing that actually removes the assumption above. Moving the key that wraps documents into a managed key service, with a defined lifetime and a re-wrap path, would close the confidentiality gap. Anchoring to a network with continuity guarantees is a business decision about irreversibility rather than an engineering one, and it has not been taken.
Conclusion
The short version, for someone deciding whether to send us their documents: documents are encrypted at rest under keys we hold, at the option of each customer, and we can read them. The audit trail is tamper-evident and signed, and for one chain a receipt can be verified by an auditor with a standalone script and a public blockchain, with no involvement from us. Everything between those two positions is in the table above, with the qualification attached rather than footnoted.
The better question to put to any vendor here is not whether data is encrypted, because the answer is always yes. It is who holds the key, and what you can check without them. The attached document answers both for us at greater length, including the parts that do not flatter us, and those parts are the reason to believe the rest of it.
References
- [1]Confidentiality and Verifiability, v1.3. Klarum. Our own security whitepaper: every control, its status, the claims we decline to make, and a revision history that records what earlier editions got wrong.
- [2]RFC 6962: Certificate Transparency. IETF. The Merkle tree construction our audit roots follow, including the leaf and node hash prefixes.
- [3]pgvector: open-source vector similarity search for Postgres. pgvector contributors. The extension the corpus is retrieved from by cosine similarity.
- [4]LLM08:2025 Vector and Embedding Weaknesses. OWASP Top 10 for LLM Applications. Why an embedding is treated as personal data here: inversion back to source text is a catalogued attack class, not a hypothetical.