Verschlüsselung
Object-Storage-Payloads unterstützen Client-Side-AES-GCM-Verschlüsselung — das Framework verschlüsselt vor dem Put, entschlüsselt beim Get, Schlüssel werden als einzelner Master-Key oder als versionierter Key-Ring für Rotation verwaltet.
import { ObjectStorageDurableStateStore, ObjectStorageDurableStateStoreOptions, S3ObjectStorageBackend, S3ObjectStorageOptions,} from 'actor-ts';
const masterKey = Buffer.from(process.env.MASTER_KEY_V1!, 'base64'); // 32 Bytes
const objectStorageDurableStateStoreOptions = ObjectStorageDurableStateStoreOptions.create() .withBackend(new S3ObjectStorageBackend(S3ObjectStorageOptions.create() /* .withRegion(...).withBucket(...) */)) .withEncryption({ mode: 'client-aes256-gcm', masterKey, info: 'acme/prod/durable-state/v1' });const store = new ObjectStorageDurableStateStore(objectStorageDurableStateStoreOptions);Jetzt wird jeder persistierte State vor dem Upload mit AES-GCM
verschlüsselt — unter einem Per-persistenceId-Subkey, der aus
dem Master-Key abgeleitet wird. Reads entschlüsseln transparent.
info ist Pflicht und deployment-spezifisch. Es ist keine
Dekoration — siehe info wählen weiter unten.
Warum Client-Side-Verschlüsselung
Abschnitt betitelt „Warum Client-Side-Verschlüsselung“Object Stores (S3, GCS, Azure Blob) bieten eingebaute Server-Side-Encryption. Warum auch Client-Side verschlüsseln?
| Bedrohung | Server-Side | Client-Side |
|---|---|---|
| Storage-Kompromittierung (jemand liest von der Disk) | ✓ | ✓ |
| Account-Kompromittierung (jemand hat S3-Credentials) | ✗ | ✓ |
| Cloud-Provider-Kompromittierung | ✗ | ✓ |
| Audit / Compliance, die “wir halten die Schlüssel” verlangt | ✗ | ✓ |
Client-Side-Verschlüsselung schützt vor mehr Bedrohungen, kostet aber mehr (CPU pro Op, Schlüsselverwaltungs-Overhead). Die meisten Apps sollten beides verwenden: Server-Side als Baseline + Client-Side für sensible Payloads.
Konfiguration
Abschnitt betitelt „Konfiguration“type EncryptionConfig = | { mode: 'none' } | { mode: 'sse-s3' } // serverseitig, S3-verwaltet | { mode: 'sse-kms'; kmsKeyId: string } // serverseitig, KMS-verwaltet | { mode: 'client-aes256-gcm'; masterKey: Uint8Array; info: string } | { mode: 'client-aes256-gcm'; masterKeys: MasterKeyRing; info: string };
type MasterKeyRing = { active: MasterKeyRingEntry; // neue Writes verschlüsseln hierunter retired?: MasterKeyRingEntry[]; // ältere Schlüssel, für Entschlüsselung behalten};type MasterKeyRingEntry = { version: number; // 0..255, im Body-Manifest eingebettet key: Uint8Array; // 32 Bytes (AES-256)};Client-Side-Verschlüsselung verwendet mode: 'client-aes256-gcm'
mit entweder einem einzelnen masterKey (32 Bytes) oder
einem masterKeys-Ring. Der Ring trägt:
active— den Schlüssel, unter dem neue Writes verschlüsseln.retired— ältere Schlüssel, behalten, um historische Blobs zu entschlüsseln.
Das version-Byte jedes Eintrags wird im Body-Manifest
mitgeführt, damit die Entschlüsselung den passenden Schlüssel
wählen kann. Den aktiven Schlüssel plus die retired Schlüssel
zu führen, ist das Fundament der
Schlüsselrotation.
info wählen
Abschnitt betitelt „info wählen“info ist der Kontext-Bindungs-Eingang von HKDF (RFC 5869
§3.2). Der Subkey eines Blobs wird aus drei Dingen abgeleitet:
dem Master-Key, der persistenceId (als HKDF-Salt) und info.
Ändert sich eines davon, entsteht ein völlig anderer Schlüssel.
Das Feld ist Pflicht, und es gibt bewusst keinen Default. Ein
gemeinsamer Default hätte bedeutet, dass zwei Deployments mit
demselben Master-Key für dieselbe persistenceId Byte für Byte
denselben Subkey ableiten — ein Staging-System, das aus einem
Produktions-Dump wiederhergestellt wurde, oder eine DR-Region
hätte damit Produktions-Blobs lesen können, ohne dass die
Konfiguration das irgendwo erwähnt. Das ist eine Entscheidung,
die nur der Betreiber treffen kann — also erzwingt das Framework
sie.
Kodiere Umgebung + Zweck + Version, das Spezifischste zuerst:
'acme/prod/snapshot/v1''acme/staging/snapshot/v1''acme/prod/durable-state/v1'- Verschiedene Umgebungen MÜSSEN sich unterscheiden, auch bei identischem Master-Key. Genau darum geht es.
- Verschiedene Payload-Arten SOLLTEN sich unterscheiden (Snapshots vs. Durable State), damit ein kompromittierter Ableitungskontext nicht auf den anderen übergreift.
- Eine angehängte Version gibt einer späteren Kontext-Rotation ein Ziel.
info steht nicht auf der Wire. Anders als die
Schlüsselversion hält kein Manifest-Byte fest, unter welchem
info ein Blob geschrieben wurde. Eine Änderung macht jeden
bestehenden Blob unlesbar, bis ein Sweep ihn neu schreibt — siehe
Kontext rotieren.
Wähle den Wert vor dem ersten Write.
Wie es funktioniert
Abschnitt betitelt „Wie es funktioniert“Beim Put: Wert serialisieren → komprimieren → Per-pid-Subkey ableiten (HKDF aus active key) → AES-GCM(bytes, subkey, iv) → Ciphertext → Body-Manifest "ATS1" { flags, keyVersion, iv, ciphertext } → S3.put(body) // kein Key-ID-Metadaten-Header
Beim Get: S3.get → Body-Manifest { flags, keyVersion, iv, ciphertext } → Master-Key nach keyVersion wählen (active oder ein retired-Eintrag) → Per-pid-Subkey ableiten (HKDF) → AES-GCM entschlüsseln → dekomprimieren → deserialisierenDie Schlüsselversion ist im Body-Manifest eingebettet — jeder Blob hält fest, unter welcher Schlüsselversion er verschlüsselt wurde. Das lässt das Framework alte Payloads mit dem richtigen Schlüssel entschlüsseln, auch nachdem der active Schlüssel rotiert wurde (retired Schlüssel bleiben im Ring).
Was es verschlüsselt
Abschnitt betitelt „Was es verschlüsselt“- Den Body — serialisierter State / Event / Snapshot.
- Nicht — den Object-Key, Object-Metadaten-Header, den Bucket-Namen.
Für Object-Metadaten, die nicht durchsickern sollten (sensible persistenceIds), verwende ein separates Naming-Schema (hashe IDs, bevor sie zu Object-Keys werden).
Schlüsselquellen
Abschnitt betitelt „Schlüsselquellen“Die Master-Key-Bytes kommen irgendwoher. Häufige Muster:
Env-Vars
Abschnitt betitelt „Env-Vars“const masterKey = Buffer.from(process.env.MASTER_KEY_V1!, 'base64'); // 32 Bytes// → .withEncryption({ mode: 'client-aes256-gcm', masterKey, info: 'acme/prod/snapshot/v1' })Am einfachsten. Jeder Schlüssel ist ein 32-Byte-Buffer (für AES-256-GCM), base64-kodiert in der Env.
Risiko: Env-Vars sind für alles sichtbar, das die Prozess-Umgebung lesen kann. Verwende nur, wenn die Env selbst gesichert ist (K8s-Secrets, etc.).
KMS-on-Load
Abschnitt betitelt „KMS-on-Load“import { KMS } from '@aws-sdk/client-kms';
const kms = new KMS();const decrypted = await kms.decrypt({ KeyId: 'alias/master', CiphertextBlob: Buffer.from(process.env.WRAPPED_KEY!, 'base64'),});
const masterKey = decrypted.Plaintext!; // 32 Bytes, im Speicher gehalten// → .withEncryption({ mode: 'client-aes256-gcm', masterKey, info: 'acme/prod/snapshot/v1' })Der Master-Key wird verschlüsselt unter einem Cloud-KMS-Key gespeichert. Die App holt ihn beim Start, entschlüsselt über KMS, hält ihn im Speicher.
Besser als rohe Env-Vars — nur KMS-Zugriff ist erforderlich, um Schlüssel wiederherzustellen.
HashiCorp Vault
Abschnitt betitelt „HashiCorp Vault“Ähnliches Muster: Master-Keys beim Start aus Vault ziehen.
CPU-Kosten
Abschnitt betitelt „CPU-Kosten“AES-GCM ist schnell — moderne CPUs haben Hardware-Unterstützung.
Pro 100 KB Verschlüsseln + Entschlüsseln:
- ~0,5-1 ms auf modernem x86 / Apple Silicon.
- Auf kleinen Objekten effektiv kostenlos.
Für die meisten Workloads ist Verschlüsselung in Profilen unsichtbar.
Verschlüsselung + Kompression
Abschnitt betitelt „Verschlüsselung + Kompression“verschlüsseln → komprimieren → S3.put # ✗ kein Kompressionsnutzen auf Ciphertextkomprimieren → verschlüsseln → S3.put # ✓ das macht das FrameworkDie Reihenfolge des Frameworks ist zuerst komprimieren, dann verschlüsseln — komprimierte Bytes sind immer noch komprimierbar (nicht zufällig); nach der Verschlüsselung sind sie effektiv zufällig und unkomprimierbar.
Wenn du sowohl Kompression als auch Verschlüsselung setzt, bekommst du diese Reihenfolge automatisch.
Alte Payloads lesen
Abschnitt betitelt „Alte Payloads lesen“Nachdem du Verschlüsselung auf einem zuvor unverschlüsselten Bucket aktiviert hast:
state/cart-42 ← alt, Plaintext (Encrypted-Flag im Manifest nicht gesetzt)state/cart-43 ← neu, verschlüsselt (Encrypted-Flag gesetzt, Schlüsselversion 0)Das Framework erkennt jedes Pro-Payload über das Body-Manifest:
- Encrypted-Flag nicht gesetzt → Plaintext-Pfad.
- Encrypted-Flag gesetzt → mit der Schlüsselversion entschlüsseln, die das Manifest festhält.
Bedeutet, du kannst Verschlüsselung schrittweise aktivieren — neue Writes werden verschlüsselt, alte Reads funktionieren immer noch, und ein Hintergrund-Re-Encryption-Sweep kann den Rest migrieren.
Siehe Schlüsselrotation für den Rotation-Flow.
Wie geht’s weiter
Abschnitt betitelt „Wie geht’s weiter“- Object Storage im Überblick — das größere Bild.
- Schlüsselrotation — der Online-Rotations-Flow.
- Master-Key-Rotation (Operations) — die operative Seite.
- Per-Actor-Policies — Per-Actor-Verschlüsselungskonfiguration.
