proven on the hosted testnet instance · 2026-09-13 · QA-14
Encryption at rest
Objects stored through a single PUT are encrypted at rest with AES-256-GCM, server-side, under a
key derived for the owning tenant. It is on by default and takes an explicit header to turn off for one object. There is
nothing to configure and no key for you to hold.
Server-side is the deliberate shape, not a stage on the way to something else: the instance holds the decryption boundary, which is what makes serving, byte-range reads and auditable deletion possible at all. A design where the bytes arrive already opaque cannot do any of those things.
The per-object opt-out
Section titled “The per-object opt-out”x-amz-meta-permafrost-encrypt |
Effect |
|---|---|
absent, empty, or default |
Encrypt. |
none |
Store this one object as plaintext. |
Case is folded over the whole value, so NONE and None are the same word. Anything else fails with
400 InvalidArgument, and the check runs before any byte is written — a rejected PUT leaves nothing
behind.
There is no always value. The instance cannot encrypt without its key configured, so a setting that
promised it would be a promise rather than a setting.
What was asked for and what happened are two records
Section titled “What was asked for and what happened are two records”The policy the header requested is recorded on the object. Whether the bytes were actually encrypted is recorded separately, and the read path branches on the second one.
That separation is load-bearing, because the first record cannot answer the question on its own. “Not encrypted” is ambiguous on its face — it can mean the caller asked for plaintext, or that the instance had no key configured when the write happened — and the posture is instance-wide and can change between a write and a later read. Storing the request beside the outcome is what keeps a read correct across that change.
For a caller the practical form is: do not infer what happened from what you sent. If you need to know, read the object back.
A read that cannot select a key refuses
Section titled “A read that cannot select a key refuses”An object recorded as encrypted is an envelope, not the object. When no key on the instance matches
the fingerprint recorded on that row, the read answers 403 AccessDenied instead of serving the
envelope at 200.
HEAD is refused on the same terms. A HEAD that reports a size the GET can never serve is worse
than a consistent refusal, so both take the same answer. The object stays visible in a bucket
listing; what is refused is the bytes.
This is the fail-closed direction on purpose. The alternative — handing back the envelope with a
200 — looks like success to every client, and a client that writes those bytes to disk has silently
stored something it cannot read and will not discover until it tries.
Two things this does not cover
Section titled “Two things this does not cover”Multipart parts are not encrypted by this instance. Single objects are; the multipart path is not, and the encryption-policy header is accepted at initiate and then does nothing there. A team that needs its large media encrypted at rest encrypts the bytes before upload. This is stated plainly rather than buried because it is the single most likely place for an assumption to go wrong: the same bucket, the same tenant, the same header, a different answer. See The S3 surface and Upload media in parts.
Encryption is not access control. The decryption key is selected from the row’s owner, not
from the caller — so if the access check were absent, encryption alone would hand one tenant
another’s object, decrypted and at 200. Tenancy is the layer that decides
who may read a row, and it runs first.
Custody objects are keyed differently
Section titled “Custody objects are keyed differently”A document held on the custody surface is not covered by the scheme above. It is encrypted under a key generated for that one object, held on the instance, and destroyed when you ask for it to be destroyed — which is what makes deletion there provable in a way that deleting a row is not. The stored ciphertext remains; nothing on the instance can turn it back into the document.
The mechanics and the claim boundary are on Deletion by key destruction, and the surface itself is in Hold, serve, incinerate.
- Tenancy — the check that runs before a key is ever selected.
- Headers, status codes, 401 and 429 — the header table and the statuses.
- On-chain objects — the receipt that records whether an object was encrypted.
Permafrost runs on Sui testnet and Walrus testnet. Everything here describes a shipped testnet instance, not a production service.