Master key rotation
Это содержимое пока не доступно на вашем языке.
For at-rest encryption
(object-storage encryption,
durable-DD-encryption), the framework’s keys live in a
MasterKeyRing — a versioned set of keys: one active
key plus any number of retired ones, each tagged with a
numeric version (a single byte, 0-255). Rotation is
online: new writes go under the active key; old reads
still work under whatever version they were encrypted with;
a background sweep eventually re-encrypts older data.
import type { MasterKeyRing } from 'actor-ts';
// Each key is 32 raw bytes (AES-256) — decode from a base64 env// var, a mounted secret, or a KMS unwrap (see "Storage of master// keys" below).const keyRing: MasterKeyRing = { active: { version: 2, key: Buffer.from(process.env.MASTER_KEY_V2!, 'base64') }, // new — current retired: [{ version: 1, key: Buffer.from(process.env.MASTER_KEY_V1!, 'base64') }],};MasterKeyRing is a type, not a class — you build the plain
object above; there is nothing to new. active is the key
used for new writes. Reads dispatch on the version byte
stored in each blob’s manifest, matching it against active or
one of the retired entries.
Because that byte is the reader’s only handle on the key, each entry needs its own version number. A ring with the same version on two entries is rejected — at registration, at the store, and at the start of a sweep — rather than left to be resolved by lookup order. See every version is used once.
The one-byte field caps how many versions can be live at once,
not how often you may rotate: once a sweep has moved the whole
corpus onto the active version, the retired entries go away and
their numbers become reusable. Registration warns from version
240 on so there is time to schedule that.
When to rotate
Section titled “When to rotate”Three triggers:
- Scheduled rotation — a security policy (every 90 days, yearly).
- Suspected compromise — leaked key material; rotate immediately.
- Compliance — regulatory requirements mandating periodic rotation.
Even without a specific trigger, periodic rotation is good practice — limits blast radius of an undetected leak.
The rotation flow
Section titled “The rotation flow” 1. Generate a fresh key with the next version number. Add it to keyRing.retired; do NOT yet promote it to active. 2. Roll out the updated ring to all nodes. Verify reads work — old data still decrypts, writes still use the current active key. 3. Promote the fresh key to active (move the previous active into retired). Roll out. New writes use the new key; old data still readable. 4. Run the re-encryption sweep. Old data is read, decrypted, re-encrypted under the active key. 5. Once the sweep completes, drop the old key from retired. Keep a rollback window of ~7 days where the old key is still available; after that, drop it.The framework supports each of these steps without downtime.
Step 1 — add the new key
Section titled “Step 1 — add the new key”const keyRing: MasterKeyRing = { active: { version: 1, key: Buffer.from(process.env.MASTER_KEY_V1!, 'base64') }, // still v1 — active unchanged retired: [{ version: 2, key: Buffer.from(process.env.MASTER_KEY_V2!, 'base64') }], // v2 added to the ring, not yet active};Roll out this config to every node. Reads of v1-encrypted data still work; writes still use v1 — but every node now holds v2 and can decrypt under it.
This step is safe and reversible — if v2 isn’t actually
needed yet, revert by removing it from retired.
Step 2 — promote the new key
Section titled “Step 2 — promote the new key”const keyRing: MasterKeyRing = { active: { version: 2, key: Buffer.from(process.env.MASTER_KEY_V2!, 'base64') }, // ← now v2 retired: [{ version: 1, key: Buffer.from(process.env.MASTER_KEY_V1!, 'base64') }],};Roll out. New writes go under v2. Reads consult the ring and find the right key (v1 or v2) based on the version byte in the stored blob’s manifest.
After this step, gradually new data accumulates under v2 as the workload writes. Old data stays under v1 until re-encrypted.
Step 3 — re-encrypt sweep
Section titled “Step 3 — re-encrypt sweep”import { reEncryptObjectStorage } from 'actor-ts';
// `backend` is the ObjectStorageBackend your store writes through// (a FilesystemObjectStorageBackend, S3ObjectStorageBackend, …).const result = await reEncryptObjectStorage(backend, { keyPrefix: 'snapshots/', // which objects to sweep keyring: keyRing, // active = v2, retired = [v1] info: 'acme/prod/snapshot/v1', // the store's HKDF context});
console.log(`re-encrypted ${result.rewrote} of ${result.scanned} objects`);info is required and must be the exact value the encrypting
store uses. It is the HKDF context, i.e. half of what the subkey
is derived from — a wrong value fails every decrypt rather than
silently producing wrong output.
The sweep lists every object under keyPrefix and, for each one
not already at the ring’s active version, decrypts it and
re-encrypts under active. Objects already current are skipped
without a write.
Useful options:
skip— a(key) => booleanpredicate; matching keys are left untouched (exclude non-body objects, or scope a partial rotation).onProgress— per-object callback for logging or an operator dashboard on long sweeps.progress— aReEncryptProgressStore(e.g.InMemoryReEncryptProgressStore) for crash-resume of very large sweeps.verifyKeyringCompleteness— on by default: samples some blobs and refuses to start if any references a version missing from the ring.newInfo— rotates the HKDF context instead of (or alongside) the key; see rotating the HKDF context.
The sweep is idempotent + resumable — an object already at
the active version is skipped without a write, so re-running
after an interruption is safe. By default a resumed run re-lists
and re-checks every key; pass a progress store to skip straight
past the objects already done.
Step 4 — retire the old key
Section titled “Step 4 — retire the old key”After the sweep completes (every item encrypted under v2):
const keyRing: MasterKeyRing = { active: { version: 2, key: Buffer.from(process.env.MASTER_KEY_V2!, 'base64') }, // retired dropped — v1 is gone};Drop v1 entirely. Any data still encrypted under v1 (e.g., backups that haven’t been re-encrypted) is now unreadable.
Wait a rollback window before dropping. ~7 days lets you recover from “oh no, the sweep didn’t actually cover all the backups.” After confirmed migration, drop v1.
Storage of master keys
Section titled “Storage of master keys”The keyRing doesn’t ship a key-storage backend. Common patterns:
| Source | Pattern |
|---|---|
| Env vars | process.env.MASTER_KEY_V2 — simplest, fine for tests. |
| K8s secrets | Mounted as files; read at startup. |
| HashiCorp Vault | Pull dynamically at startup; refresh periodically. |
| AWS KMS / GCP KMS / Azure Key Vault | Cloud KMS APIs. Decrypt-on-load via the KMS encryption keys. |
For production, KMS is the right answer — keys never leave the secure boundary in plaintext form.
Multi-cluster considerations
Section titled “Multi-cluster considerations”If multiple clusters share the same encrypted store (e.g., a DR replica that reads the primary’s backups), all clusters need the same keyRing. Rotate them together; don’t let one cluster fall behind on key generations.
Failure modes
Section titled “Failure modes”Where to next
Section titled “Where to next”- Object storage encryption — what uses the master keys.
- Object storage key rotation — the storage-side mechanics.
- Cluster security — in-transit complement to at-rest encryption.
- Operations overview — full security checklist.
