S3 Proxy Encryption Protocol¶
The CloudTaser S3 proxy provides transparent client-side encryption for S3-compatible object storage. It runs as a sidecar container, intercepting S3 API calls on localhost:8190. Data is encrypted before it leaves the pod, so the cloud provider stores only ciphertext. The encryption keys are held in an EU-hosted Vault Transit engine and never leave EU jurisdiction.
Envelope Encryption¶
The proxy uses envelope encryption: each object is encrypted with a unique data encryption key (DEK), and each DEK is wrapped (encrypted) by a key encryption key (KEK) managed in Vault Transit.
| Key | Type | Location | Lifecycle |
|---|---|---|---|
| KEK (Key Encryption Key) | AES-256 | Vault Transit engine (EU-hosted) | Long-lived, rotated quarterly |
| DEK (Data Encryption Key) | AES-256-GCM | Generated per object, stored wrapped in S3 metadata | Per-object, never reused |
This design means:
- The KEK never leaves Vault. The proxy sends DEKs to Vault for wrapping/unwrapping but never handles the KEK directly.
- Each object has a unique DEK. Compromising one DEK exposes only that single object.
- Key rotation of the KEK does not require re-encrypting existing objects. Old wrapped DEKs remain decryptable as long as the old KEK version is retained in Vault.
Encryption Flow (PutObject)¶
When the workload writes an object through the proxy:
1. Workload calls PutObject on http://localhost:8190/bucket/key
2. Proxy generates a random 256-bit DEK
3. Proxy encrypts the object body:
- Algorithm: AES-256-GCM
- Key: the DEK
- Nonce: 96-bit random
- Output: nonce || ciphertext || GCM tag
4. Proxy sends the DEK to Vault Transit for wrapping:
POST /v1/transit/encrypt/cloudtaser
{ "plaintext": base64(DEK) }
Response: { "ciphertext": "vault:v1:..." }
5. Proxy stores the wrapped DEK and metadata as S3 object metadata headers
6. Proxy re-signs the request with its own credentials (SigV4)
7. Proxy forwards the encrypted body + metadata headers to the upstream S3 endpoint
The plaintext DEK exists only in the proxy's memory for the duration of the request. After encryption, it is discarded. The wrapped DEK (which is useless without access to Vault) is stored alongside the encrypted object.
Decryption Flow (GetObject)¶
When the workload reads an object through the proxy:
1. Workload calls GetObject on http://localhost:8190/bucket/key
2. Proxy forwards the request to the upstream S3 endpoint
3. Proxy reads the metadata headers from the response
4. If CloudTaser metadata is present:
a. Proxy extracts the wrapped DEK from metadata
b. Proxy sends the wrapped DEK to Vault Transit for unwrapping:
POST /v1/transit/decrypt/cloudtaser
{ "ciphertext": "vault:v1:..." }
Response: { "plaintext": base64(DEK) }
c. Proxy decrypts the object body using the unwrapped DEK (AES-256-GCM)
d. Proxy returns the plaintext body to the workload
5. If CloudTaser metadata is NOT present:
a. Proxy returns the response body as-is (pass-through for non-encrypted objects)
Metadata Headers¶
The proxy stores encryption metadata as custom S3 object metadata headers:
| Header | Value | Description |
|---|---|---|
x-amz-meta-cloudtaser-encrypted |
true |
Indicates the object is CloudTaser-encrypted |
x-amz-meta-cloudtaser-wrapped-dek |
vault:v1:... |
The Vault Transit-wrapped DEK (base64-encoded ciphertext) |
x-amz-meta-cloudtaser-algorithm |
AES-256-GCM |
Encryption algorithm used |
x-amz-meta-cloudtaser-key-name |
cloudtaser |
Vault Transit key name used for wrapping |
x-amz-meta-cloudtaser-key-version |
1 |
Vault Transit key version used for wrapping |
These headers are stored as standard S3 user-defined metadata. They are visible in S3 HeadObject responses and AWS Console, but they contain no sensitive data -- the wrapped DEK is useless without access to the Vault Transit key.
Multipart Upload Handling¶
For large objects uploaded via S3 multipart upload, each part is encrypted independently:
- CreateMultipartUpload: Proxy initiates the multipart upload on the upstream endpoint. No encryption happens at this stage.
- UploadPart: Each part is encrypted with its own unique DEK. The wrapped DEK for each part is stored in a temporary mapping keyed by part number.
- CompleteMultipartUpload: Proxy writes the per-part wrapped DEKs as a JSON structure in the object metadata header
x-amz-meta-cloudtaser-wrapped-dek. The metadata includes an array of part entries, each with its wrapped DEK, nonce, and size.
On download, the proxy reassembles the object by decrypting each part in sequence using its corresponding DEK.
Part size matters
Each part is encrypted and authenticated independently using AES-256-GCM. This means parts can be decrypted in parallel and the integrity of each part is verified separately.
Backward Compatibility¶
The proxy handles mixed encrypted and non-encrypted objects transparently:
- Encrypted objects (with
x-amz-meta-cloudtaser-encrypted: true): Decrypted on read, re-encrypted on overwrite - Non-encrypted objects (no CloudTaser metadata): Passed through as-is on read. If overwritten through the proxy, the new version will be encrypted
This allows incremental adoption -- you can enable the S3 proxy without needing to re-encrypt all existing objects. New writes go through the proxy and are encrypted; old objects remain readable.
Limitations¶
Range Requests¶
Range requests (Range: bytes=N-M) are not supported for encrypted objects. AES-256-GCM is an authenticated encryption scheme -- you cannot decrypt a subset of the ciphertext without processing the entire block.
When a range request is made for an encrypted object, the proxy:
- Downloads the full encrypted object from S3
- Decrypts the entire object
- Returns only the requested byte range to the workload
Performance impact for range requests
For large encrypted objects, range requests will incur the latency and bandwidth cost of downloading and decrypting the full object. If your workload relies heavily on range requests (e.g., video streaming, database files), consider whether client-side encryption is appropriate for that data.
Object Size¶
The maximum object size is limited by the S3 API limits for multipart upload (5 TB with 10,000 parts). Each part can be up to 5 GB. The encryption overhead per part is minimal (12-byte nonce + 16-byte GCM authentication tag).
Server-Side Encryption Interaction¶
If the S3 bucket has server-side encryption (SSE-S3, SSE-KMS) enabled, objects will be double-encrypted: once by the CloudTaser proxy (client-side) and once by S3 (server-side). This is safe but redundant. The CloudTaser encryption is the sovereignty-relevant layer -- SSE uses keys controlled by the cloud provider.
Key Rotation¶
See Key Rotation for Transit KEK rotation procedures.
After rotating the KEK in Vault Transit:
- New PutObject calls automatically use the latest KEK version for wrapping
- Existing objects remain decryptable because Vault retains previous KEK versions
- No re-encryption needed unless you want to advance the minimum decryption version
To re-encrypt existing objects with the latest KEK version, read and re-write them through the proxy:
# Re-encrypt a single object
aws s3 cp s3://bucket/key s3://bucket/key \
--endpoint-url http://localhost:8190 \
--metadata-directive REPLACE
Vault Transit Configuration¶
The proxy requires a Vault Transit key named cloudtaser (configurable). Create it during initial setup:
# Enable Transit engine (if not already enabled)
vault secrets enable transit
# Create the encryption key
vault write -f transit/keys/cloudtaser
# Verify
vault read transit/keys/cloudtaser
The proxy authenticates to Vault using the same Kubernetes auth mechanism as the wrapper. It needs a policy that allows: