Skip to main content

Documentation Index

Fetch the complete documentation index at: https://s2.dev/docs/llms.txt

Use this file to discover all available pages before exploring further.

S2 supports stream encryption with a key that you supply at request time. Configure a basin with a cipher for new streams, then provide the same key on append and read. S2 stores the stream’s algorithm in metadata, but not the key material itself.

Determined at stream creation

A stream captures its cipher when it is created, through its basin’s configuration.

Key supplied on data plane requests

Encrypted streams require the s2-encryption-key header on REST requests, or pass it using the corresponding CLI flags.

Key custody stays with you

S2 does not persist or log the encryption key.

How it works

New streams inherit the stream_cipher configured on the basin at the moment they are created. Reconfiguring the basin later only affects streams created after that change.

What is encrypted

For regular records, S2 encrypts headers and body before storage, and decrypts it only when serving a read with the correct key. Command records stay plaintext because S2 must interpret them to apply service-side operations such as fencing and trimming.
Encrypted records are cryptographically bound to the stream they were written to. S2 includes the stream’s unique identifier as associated data, so ciphertext from one stream cannot be accepted as a record in another stream.

Supported ciphers

CipherAPI / CLI valueKey length
AEGIS-256 (recommended)aegis-25632 bytes
AES-256-GCM (NIST-approved)aes-256-gcm32 bytes
Keys are base64-encoded when provided in the s2-encryption-key header or to the CLI. SDKs allow providing either a string or bytes.
A stream configured with aes-256-gcm can have up to 2^32 records (~4.3 billion), and subsequent appends will be rejected. Otherwise, a stream can have up to 2^64 records (effectively unlimited) – so aegis-256 is generally preferable.

Where keys are required

OperationKey required?Notes
Create or reconfigure basinNoSet stream_cipher here for future streams.
Create streamNoThe stream inherits the basin’s current stream_cipher.
AppendYes, if the stream has a cipherProvide the key on every request or session.
ReadYes, if the stream has a cipherUse the same key that encrypted the records you want to read.
Check tailNoTail only returns positions, not payloads.
List streamsNoEach stream’s cipher is returned in metadata.

Generate a key

openssl rand -base64 32
Store the resulting value as a secret to be used for one or more streams. Maintain a mapping of which key was used for which stream, so that it may be specified consistently for appends and reads.

Key management

For coarse-grained access, manage a shared key in your existing Key Management System (KMS) or secrets infrastructure. For finer-grained access, such as a key per stream, use envelope encryption:
  • Keep a Key Encryption Key (KEK) in your KMS.
  • Generate a Data Encryption Key (DEK) per stream, and store the wrapped DEK alongside your stream metadata.
  • At request time, your application unwraps the DEK and passes it to S2 as the stream encryption key.
If you need to rotate the key, create a new stream and cut writers and readers over deliberately. Similarly, to change the cipher used, update the basin configuration and then create a new stream.
Do not rotate a stream key in place by simply changing the request header. S2 will accept the append, but later reads with only one key will fail once they reach records encrypted under a different key.

Configure encryption for new streams

s2 create-basin secure-events \
  --stream-cipher aegis-256

# or change the default for streams created later
s2 reconfigure-basin secure-events \
  --stream-cipher aes-256-gcm
Stream create endpoints do not take a per-stream encryption field. A stream inherits the basin default when it is created, and that cipher is immutable for the lifetime of the stream.

Append and read with the key

export S2_ENCRYPTION_KEY="$(openssl rand -base64 32)"

printf 'top secret\n' | s2 append s2://secure-events/audit-log
s2 read s2://secure-events/audit-log -s 0 -n 1
# Alternatively, read the key from a file
s2 read s2://secure-events/audit-log \
  --encryption-key-file ./stream-key.b64 \
  -s 0 -n 10
The same keying pattern applies to unary operations, append/read sessions, SSE reads, and S2S sessions. The encryption key is just an HTTP header on the wire.

Inspect cipher config

Errors

SituationResult
Missing key on an encrypted stream422 invalid
Key is not valid base64 or not 32 bytes422 invalid
Wrong key400 decryption_failed