Your data, your keys

S2 now supports client-supplied encryption keys: you hand S2 a key per request, S2 uses it to encrypt or decrypt records in memory, and then forgets it. The keys are never persisted, never logged, and zeroed when the request completes. Without the key, the stored records are unrecoverable — even by us.

This is a good fit for streams carrying tenant-scoped events, audit logs, observability telemetry, agent transcripts, or regulated data such as protected health information 1. Your application supplies keys from your own infrastructure, while S2 handles request-time encryption behind the durable stream API.

Where this fits

There are three places encryption operations can happen — in your application, in S2 with keys you supply per request, or in S2 with keys governed by a cloud key management system (KMS).

Client-side record encryption is the right model when your hard requirement is that S2 never sees plaintext. You encrypt each record before append and decrypt after read. This gives you end-to-end control, but every producer and consumer must handle encryption, decryption, and formats correctly. You also lose compression on the wire, since encrypted bytes don't compress.

KMS-integrated encryption (CMEK) gives you auditable key governance through your cloud provider's KMS, IAM policies, and audit logs you already manage. The tradeoff is portability since you are limited to that provider's KMS and the services that integrate with it.

Client-supplied encryption keys (CSEK) are the middle ground, in the same family as Google Cloud CSEK and Amazon S3 SSE-C. Encryption and decryption happen inside S2 at request time, so producers and consumers stay simple. Client↔S2 compression still works since it runs before encryption on writes and after decryption on reads. You decide where keys live and which workloads can use them, independent of any specific KMS.

The tradeoff is S2 sees plaintext and key material in memory while serving. If your compliance posture requires that the service operator never have access to plaintext, use client-side encryption instead.

How it works

Basins can now be configured with an encryption algorithm 2. Every new stream created in that basin then requires an encryption key to append or read records.

Keys are sent as base64-encoded key material in the s2-encryption-key request header over TLS. All SDKs – TypeScript, Python, Go, and Rust – also support specifying it at the stream level.

In the s2.dev data plane, writes are encrypted at the edge service before being forwarded downstream. On reads, encrypted records are decrypted only after they return to the edge service, after any caching and coalescing.

The core encryption path lives in the open source s2 repo, so you can verify how keys are handled. s2-lite, the self-hostable form factor of S2, supports the same mechanism.

There is no meaningful impact to performance, as the supported ciphers are designed for high throughput and are hardware-accelerated in practice.

For operational guidance on key management, including envelope encryption and rotation, see the encryption docs.

Try it

Let's configure a basin to encrypt new streams:

$ s2 create-basin logs-prod \
  --create-stream-on-append \
  --stream-cipher aegis-256

You can also reconfigure an existing basin – only new streams are affected:

$ s2 reconfigure-basin logs-prod --stream-cipher aegis-256

Now, when a new stream is created in this basin, it will use the configured cipher. An encryption key must be provided with all data plane requests that read or write records.

For the CLI, generate a key and pass it explicitly, or use the S2_ENCRYPTION_KEY environment variable.

$ export S2_ENCRYPTION_KEY="$(openssl rand -base64 32)"
 
$ printf 'hello from an encrypted stream\n' | \
    s2 append s2://logs-prod/app/node-foo
 
$ s2 read s2://logs-prod/app/node-foo --seq-num 0 --count 1
hello from an encrypted stream

With the TypeScript SDK, the key is scoped to the stream handle:

import { AppendInput, AppendRecord, S2 } from '@s2-dev/streamstore';
 
const s2 = new S2({ accessToken: process.env.S2_ACCESS_TOKEN! });
const stream = s2.basin('logs-prod').stream('app/node-foo', {
  encryptionKey: process.env.S2_ENCRYPTION_KEY!,
});
 
await stream.append(
  AppendInput.create([AppendRecord.string({ body: 'hello' })])
);

Client-supplied keys are available today across the s2.dev cloud service, the CLI, every SDK, and s2-lite, at no additional cost.

Footnotes

  1. Contact us for a HIPAA Business Associate Agreement (BAA).

  2. We recommend aegis-256 in general; aes-256-gcm is available for organizations that standardize on AES-GCM.