Documentation

Deployment & CLI

Deploy the flare-encrypt store to your own Cloudflare account: fe init / wrangler deploy, D1, the KEK, and the freshness Durable Object. KEK custody, the rotation runbook, bootstrap gating, the Logpush→R2 audit sink, fe run injection safety, break-glass offline decrypt, and the fe key signing verbs with the custody drill.

What deploys

The store is a single standalone Cloudflare Worker plus one D1 database, one master-key secret (Secrets Store, or a plain Worker Secret as a config-only fallback), and a sharded freshness Durable Object — all on your own account. There is no hosted control plane in the loop: you own the code, the database, and the master key. The Worker binds Secrets Store directly, which gives per-accessenv.KEK.get() rather than an ambient secret.

Deploy to your own account

fe init --dry-run prints the ordered deploy plan — every step is a wrangler or D1 primitive, so nothing is hidden: create the D1 database, apply migrations, generate and provision the master key, deploy, mint the first admin token via /admin/bootstrap, delete the one-shotBOOTSTRAP_TOKEN, and store the endpoint and admin token in the OS keychain. You run the steps yourself — automatic execution isn't wired yet, so nothing runs against your account implicitly.

fe init --dry-run   # print the plan: create D1 + migrate + provision KEK + deploy + bootstrap

The steps are the same primitives, run by hand:

wrangler d1 create flare-encrypt                    # note the database_id
wrangler d1 migrations apply flare-encrypt --remote  # 0001_init.sql

# KEK as a two-level per-environment JSON map, keyed by environment id:
printf '{"%s":{"1":"%s"}}' "$ENV_ID" "$(openssl rand -base64 32)" \
  | wrangler secrets-store secret put flare-encrypt-kek --store-id <store-id>

wrangler secret put BOOTSTRAP_TOKEN                  # one-shot; deleted after bootstrap
wrangler deploy

The Worker binds three things: DB (the D1 database — everything in it is non-secret metadata or AES-GCM(...) = ct || tag), KEK (fetched per request viaenv.KEK.get()), and FRESHNESS (the Durable Object on both the write and read paths). Non-secret build config lives in varsAAD_FMT,DEFAULT_ENV, and TIME_TRAVEL_WINDOW_MS — and never key material.

KEK custody

The master-key value is a two-level, per-environment JSON map — never a bare key. Outer keys are environment ids; inner keys are stringified kek_kv integers; inner values are base64 of the raw 32-byte AES-256 key:

{ "<environment_id>": { "1": "<base64-32B>", "2": "<base64-32B>" } }
  • Per-environment material. Each environment holds its own key bytes per version, sokek_kv indexes into that environment's sub-map and a KEK is resolved by(environment_id, kek_kv). Two environments both at kek_kv = 1 hold different bytes. A KEK compromise unwraps only that environment's DEKs, and a version can be purged or rotated for one environment without touching any other.
  • Generate with a CSPRNG. Each key is 32 raw bytes from openssl rand orcrypto.getRandomValues, never a passphrase. The Worker imports each needed version to anextractable:false handle and never logs the raw bytes.
  • Secrets Store or a Worker Secret. Operators without the Secrets Store beta use the identical two-level value as a plain Worker Secret; every code path is the same. The only capability lost is rotate-without-redeploy, and a Worker Secret is ambient in env — access it behind one narrow module and never log or serialize env.

Retention is coupled to erasure: a retained old KEK version is a live wrapping key. Keep a version in an environment's sub-map only while a live DEK there still references it. Once rotation reports zero DEKs on (environment_id, kek_kv = N), purge "N" — pruning it early bricks those tenants with KekVersionUnknown.

Rotation runbook

This is crypto-key rotation, distinct from connector-driven secret-value rotation. Every rotation returns 202 { jobId } and runs in a resumable Workflow — never a synchronous handler, which would exhaust the D1 subrequest and Worker CPU budget mid-pass and leave DEKs half-rewrapped.

  • KEK rotation (proactive) — O(tenants), zero data re-encryption. Add a new version to that environment's sub-map, then fe rotate kek --project app --env production. The Workflow re-wraps each of that environment's tenant_deks rows (unwrap under the old KEK, re-wrap under the new — same DEK plaintext), at most 200 DEKs per step. Pollfe rotate status until zero DEKs remain on the old kek_kv, then purge it.
  • DEK rotation — O(that tenant). fe rotate dek … --tenant <ulid>mints a new dek_ver and re-encrypts that tenant's secrets under it, at most 50 secrets per step, guarded on the value revision so a concurrent live write is never clobbered. The superseded DEK row goes retiring and is dropped once no secret references it.
  • Compromise-driven rotation. A KEK leak needs the heavier operation: generate fresh DEKs for every tenant in the compromised environment, re-encrypt all of that environment's data, and purge the compromised kek_kv from that environment's sub-map. Because key material is per-environment, recovery is scoped to that one environment.

A KEK-map entry (or an old dek_ver) is never pruned until the completion count reaches zero; tenant_deks.status (active → retiring → revoked) is driven by the Workflow, not set by hand. An aad_fmt bump uses the same machinery: new writes set the new format, a Workflow re-encrypts old rows, and a golden vector for the new format exists before it is used.

Bootstrap gating

A Worker cannot delete or rewrite its own binding at runtime, so the store never claims a secret "self-deletes." Bootstrap is gated on a D1-observable, irreversible condition instead:POST /admin/bootstrap succeeds only while zero admin tokens exist inapi_tokens; once the first admin is minted, the endpoint refuses forever. The first call additionally requires the deploy-time BOOTSTRAP_TOKEN Worker Secret as a second factor. The init plan calls the endpoint, deletes BOOTSTRAP_TOKEN via wrangler as defense-in-depth, and ends with a step to force-rotate the admin token and mint least-privilege per-tenant tokens for real use.

fe login --url https://flare-encrypt.<subdomain>.workers.dev --token fe_...
fe token create --project app --env production --tenant <ulid> --cap secrets:read,secrets:write
fe token revoke <token-id>

A tenant = NULL (cross-tenant) token requires an explicit admin orsecrets:export-all capability — token scoping is the only read-time tenant-isolation control. token.create and token.revoke are audited, and a token value is returned once at mint.

The audit sink

The in-D1 hash chain is tamper-evident, but the principal it holds accountable can rewrite it wholesale, so every audit row also streams to an immutable external sink via Logpush → R2 with Object Lock in compliance mode. That mirror is the only record that survives full-account compromise, and it is reconciled against the in-D1 chain to catch a full-account attacker.

wrangler r2 bucket create flare-encrypt-audit
wrangler r2 bucket lock set flare-encrypt-audit --retention-days 3650   # Object Lock, compliance mode
# Logpush job: audit dataset → the locked R2 bucket; verify the objects are immutable.

fe run — the injector

fe run -- <cmd> is the runtime path for non-Workers consumers. It does one export round-trip (a single audited event), holds the values in process memory, and hands them to the child through execve's environment — no temp file, no generated .env, no shell-history exposure. Two OS-level protections back this up, because "never on disk" is a claim about the tool's own file I/O, not about memory or the child's environment:

  • argv rejection. fe set refuses a value passed as a positional argument (it would appear in ps aux, /proc/PID/cmdline, and shell history) and requires --stdin or a no-echo prompt.
  • No core dump. fe run sets ulimit -c 0 on the child shell before exec, so a crash of the injected process cannot dump the secret set to/cores or /var/crash. Credentials live in the OS keychain, never a dotfile.

The honest guarantee: the CLI writes no plaintext file, but OS swap and core-dumps remain the host's responsibility — disable them for high-assurance use. Env-var injection is inherently visible in the child's /proc/PID/environ, ps e, and crash dumps; that is documented, not marketed past.

Break-glass — offline decrypt

Because @fractalboxdev/flare-encrypt-sdk is a pure leaf with zero Cloudflare dependencies that derives the encryption context deterministically from row fields, you decrypt an exported D1 snapshot with just the target environment's KEK bytes and Node — no Worker, no D1 runtime, no dashboard:

wrangler d1 export flare-encrypt --output ./snapshot.sql --remote
fe export --offline --snapshot ./snapshot.sql --kek ./kek.json --tenant <ulid>

# kek.json is the flat single-environment version map for the target environment —
# that environment's sub-map from KEK custody:  { "1": "<base64-32B>", "2": "<base64-32B>" }
# (--tenant is optional; omit it to decrypt every tenant in the snapshot)
#
# Under the hood, using only public SDK primitives:
#   importKek(kekBytes, kek_kv)  → unwrapDek(KEK, wrappedDek, tenantId)  → decrypt(DEK, record, dataCtx)

This is the one CLI path that touches disk deliberately, and it is what makes "you fully own it" provable when Cloudflare is unavailable — the concrete differentiator from a hosted vault, which has no portable offline read.

The offline kek.json is a plaintext copy of the root wrapping key. Holding it next to a D1 export deliberately reconstructs the full-KEK-plus-full-D1 condition the rest of the design avoids, so it is break-glass only: escrow- or HSM-sourced, materialized on an isolated host, audited and dual-control, and destroyed after use.

Signing keys — fe key, and the custody drill

The signing-key surface has the same operator verbs. fe key create prints only the public key; fe key sign reads the message from stdin — never argv — and signs the exact bytes; fe key rotate activates a new version while prior versions stay verifiable as retiring. A sign-only CI credential is one command:

fe key create release                        # → base64 public key, for verifier distribution
fe key sign release --stdin < artifact.bin   # → base64 signature (message from stdin ONLY)
fe key rotate release                        # v+1 signs; prior versions stay verifiable
fe token create --name ci-release --cap keys:sign --key release \
  --project app --env production --tenant <ulid>   # signs that one key; can read nothing

Wrapped signing keys live in the same D1 export, so key custody survives a store outage under the same "you fully own it" guarantee. The custody drill proves it offline:

wrangler d1 export flare-encrypt --output ./snapshot.sql --remote
fe key drill --snapshot ./snapshot.sql --kek ./kek.json            # custody proof, no private material
fe key drill --snapshot ./snapshot.sql --kek ./kek.json --reveal   # break-glass proper: PKCS#8 keys

The default drill unwraps every wrapped version, signs a challenge, and verifies it against the stored public key — custody is proven without a private key ever being printed.--reveal trades the never-leaves property for survivability and is treated as key exposure: run it on an isolated host under the KEK-custody rules above, thenfe key rotate each revealed key and retire the revealed versions.

Operations quick reference

TaskCommand
First deployfe init --dry-run → run the printed steps
Add a KEK versionput {"<env_id>":{"1":…,"2":…}} to KEK
KEK rotatefe rotate kek …fe rotate status
DEK rotatefe rotate dek … --tenant <id>
Crypto-shred a tenantDELETE /v1/tenants/… (reports erasure_eta)
Break-glass readwrangler d1 export + fe export --offline
Create / rotate a signing keyfe key create <name> / fe key rotate <name>
Sign from CIfe key sign <name> --stdin under a keys:sign key-pinned token
Signing custody drillwrangler d1 export + fe key drill … (--reveal ⇒ rotate + retire)
Mint a scoped tokenfe token create … (--cap keys:sign --key <name> for sign-only)

The schema behind these commands is the Data model; the trust boundaries they assume are the Security model.