Skip to content

fix(crypto): fail loud instead of silently minting an ephemeral key (#1507)#1509

Merged
os-zhuang merged 3 commits into
mainfrom
claude/upbeat-allen-qVGl6
Jun 2, 2026
Merged

fix(crypto): fail loud instead of silently minting an ephemeral key (#1507)#1509
os-zhuang merged 3 commits into
mainfrom
claude/upbeat-allen-qVGl6

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes #1507.

Problem

The default ICryptoProvider backs every secret-at-rest in the platform — encrypted settings (sys_setting.value_enc), ObjectQL secret fields, and runtime datasource credentials. Its key resolution fell back, silently, to a fresh per-process randomBytes(32) key (or auto-minted a new on-disk key on every boot) when no stable key was available.

In an ephemeral-FS container or a multi-node cluster, each restart / each node then encrypts under a different key, and every previously-written sys_secret value becomes undecryptable. The failure was invisible at encrypt and boot time and only surfaced later as "all my saved passwords / API keys / DB credentials fail to decrypt".

Changes

Implements all three proposals from the issue:

  1. Fail loud instead of going ephemeral. When NODE_ENV=production and no stable key source (env var or a pre-existing persisted file) is available, the provider now throws an actionable error at construction instead of generating a key. It never auto-mints a key in production. Development and test keep the ergonomic fallback (persisted dev key / ephemeral test key, with loud warnings).

  2. Ship a persistent env-master-key default. Added OS_SECRET_KEY as the canonical production master key (32-byte hex or base64), the documented production default. OS_DEV_CRYPTO_KEY remains the dev convenience key. KMS / Vault stay as future plug-ins behind the same ICryptoProvider seam.

  3. Reframed the provider. Renamed InMemoryCryptoProviderLocalCryptoProvider (the old name wrongly implied an ephemeral key). InMemoryCryptoProvider stays as a deprecated alias for backward compatibility, so existing importers (serve, datasource binder, tests) keep working.

Key resolution order (first match wins)

Tier Source Prod Dev Test
1 opts.key (explicit)
2 OS_SECRET_KEY
3 OS_DEV_CRYPTO_KEY (legacy OBJECTSTACK_DEV_CRYPTO_KEY)
4 persisted file ~/.objectstack/dev-crypto-key read-only read or auto-create skipped
5 ephemeral random throws last-resort (loud warn) silent

A present-but-malformed OS_SECRET_KEY is treated as an explicit operator error and throws in every mode (never silently diverges to a different key).

Boundary note

Per the issue: secrets surviving a restart is correctness, not a premium feature. LocalCryptoProvider and the env-key path stay open-source; KMS, automatic rotation, and per-tenant key isolation remain the paid layer.

Files

  • packages/services/service-settings/src/local-crypto-provider.ts — new implementation (renamed + fail-loud + OS_SECRET_KEY).
  • …/in-memory-crypto-provider.ts — now a backward-compat re-export shim.
  • …/local-crypto-provider.test.ts — coverage for every key-resolution tier (env hex/base64, prod fail-loud, prod pre-existing file, no-mint-in-prod, dev persist, test ephemeral, legacy alias, AAD, rotation, digest).
  • settings-service-plugin.ts, cli/serve.ts, objectql/engine.ts, datasource-secret-binder.ts, spec/crypto-provider.ts — wiring + docs updated to LocalCryptoProvider; serve surfaces the production-key error verbatim.
  • README.md — documents the OS_SECRET_KEY default and fail-loud behaviour.

Testing

  • @objectstack/service-settings: 93 tests pass (incl. the new suite).
  • @objectstack/service-datasource-admin: 29 pass.
  • Full @objectstack/cli build (51 packages, incl. serve.ts) and @objectstack/objectql build succeed.

Compatibility / behaviour change

Only deployments explicitly running NODE_ENV=production without a key and without a pre-existing key file will now refuse to boot (with actionable guidance) — which is exactly the silently-broken case this fixes. Dev (NODE_ENV unset/development) is unchanged apart from louder warnings. InMemoryCryptoProvider imports keep working via the alias.

https://claude.ai/code/session_01Qzri77Foepw1hnw52Vm3pP


Generated by Claude Code

…1507)

The default ICryptoProvider backs every secret-at-rest in the platform
(encrypted settings, ObjectQL secret fields, datasource credentials). Its
key resolution silently fell back to a per-process random key (or
auto-minted a new on-disk key each boot) when no stable key was available,
making every sys_secret value undecryptable after a restart in containers
or on a second node — with no error at encrypt or boot time.

- Rename InMemoryCryptoProvider -> LocalCryptoProvider (old name wrongly
  implied an ephemeral key); keep InMemoryCryptoProvider as a deprecated
  alias.
- Add OS_SECRET_KEY as the canonical production master key (32-byte
  hex/base64), the documented production default.
- Fail loud in production: when NODE_ENV=production and no stable key
  source (env var or pre-existing persisted file) exists, throw an
  actionable error at construction instead of generating a key. Never
  auto-mint a key in production. Dev/test keep the ergonomic fallback.
- serve surfaces the production-key error verbatim and refuses to wire an
  unstable provider for secret fields.
- Document the env-key default and reframe the provider in the README;
  update spec contract and downstream comments.
- Add LocalCryptoProvider test coverage for every key-resolution tier.

https://claude.ai/code/session_01Qzri77Foepw1hnw52Vm3pP
@vercel

vercel Bot commented Jun 2, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
spec Ready Ready Preview, Comment Jun 2, 2026 3:26am

Request Review

…VGl6

# Conflicts:
#	packages/cli/src/commands/serve.ts
@os-zhuang os-zhuang marked this pull request as ready for review June 2, 2026 03:20
Comment thread packages/cli/src/commands/serve.ts Fixed
The datasource-admin block is the first to touch `sharedCryptoProvider`
(declared `undefined` just above), so the `?? new LocalCryptoProvider()`
left operand was always nullish — a useless conditional. Assign directly;
the secret-field wiring below still reuses the one instance.

https://claude.ai/code/session_01Qzri77Foepw1hnw52Vm3pP
@os-zhuang os-zhuang merged commit 55866f5 into main Jun 2, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/xl tests tooling

Projects

None yet

3 participants