End users hold the keys
You operate the enclave, but the data belongs to your end users, so each user’s data is sealed under a key only that user holds. Keys cannot live inside the enclave: it has no persistent disk, so a key generated and kept there would be lost on the first restart, making everything stored under it unrecoverable. Instead, each end user generates a 32-byte AES-256 key on their own device (or derives one from a passphrase using a key derivation function such as HKDF) and sends it along with each request to your app. Your app authenticates the user, then forwards the key to the sidecar in a per-request header. The key exists inside the enclave only for the lifetime of the request and is never persisted, so neither you as the operator nor Tinfoil can decrypt the stored data.Tutorial
This walks through adding encrypted S3 storage to an existing container deployment. For a complete runnable deployment, see the persistent storage example.1. Create a bucket and IAM credentials
Create an S3 bucket and an IAM user whose credentials the sidecar will use. Attach a policy scoped to the bucket(s) the sidecar should reach — IAM is the enforcement point for which buckets are accessible:tinfoil secret create): AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY. These credentials only ever touch ciphertext.
2. Add the sidecar to your config
Run the sidecar alongside your app in multitenant mode and put both on a shared network. The sidecar needs egress to reach S3; your app reaches it athttp://buckets:9000 by container name:
tinfoil-config.yml
3. Have each end user generate a key
Client applications generate 32 random bytes per user, base64-encode them, and store the key durably on the user’s side. The equivalent of:4. Make per-user requests from your app
For each authenticated request, your app forwards two headers to the sidecar: the user’s key, and a tenant id your app derives from the user’s verified identity (never from user input). Any S3 SDK works, configured with the sidecar as the endpoint, path-style addressing, and sequential single-request transfers; credentials can be any throwaway values, since SDKs refuse to send unsigned requests:s3://bucket/key), so one sidecar can serve multiple buckets; reachability is enforced by the IAM policy from step 1. Equivalent Java and AWS CLI setups are in the sidecar’s guides.
How multitenancy works
- Objects are transparently namespaced under
<tenantId>/in the backing bucket, so one tenant can never address another’s objects. Listing and reads within a tenant see plain object keys with the prefix stripped. - The sidecar trusts the two headers — your app is the authentication layer, which is why the tenant id must come from the user’s verified identity.
- Key and tenant id are decoupled, so a user can rotate keys without moving data. The caller tracks which key decrypts which object; the wrong key returns
400 DecryptionFailed.
MULTITENANT and supply one ENCRYPTION_KEY secret instead. See the sidecar README.
Differences from normal S3
Because the sidecar maintains streaming AES-GCM cipher state, a few S3 behaviors differ:| Behavior | Constraint |
|---|---|
| Addressing | Path-style only (forcePathStyle or equivalent) |
| Multipart uploads | Sequential parts (max_concurrency=1); non-last parts must be multiples of 16 bytes |
| Ranged GETs | Not supported — download whole objects |
| GET responses | Buffered in memory to verify the authentication tag; 1 GiB default, configurable via BUFFER_SIZE up to 64 GiB |
| ETags | Synthetic — not usable for client-side integrity checks |
DANGEROUS_DELAYED_AUTH) that requires a verifying client; see the design notes before using it.
Relevant repositories
- tinfoil-buckets-sidecar — the sidecar itself, with CLI and SDK guides
- tinfoil-persistent-storage-example — minimal deployable example

