Skip to main content

ZSC — the Zeq Secure Context

ZSC is a core feature: the framework's entire secret surface. There are no .env files anywhere in Zeq. Every secret — session keys, Stripe keys, the node's Ed25519 identity, database URLs for sub-systems — lives encrypted at rest in the zsc_secrets table and is decrypted into process.env only in memory, only at boot.

The cipher

Each secret row stores value_enc + value_iv, sealed with:

  • AES-256-GCM, 12-byte IV, 16-byte auth tag appended to the ciphertext, hex-encoded.
  • Key = PBKDF2-SHA256(node field-key bytes, HULYAS salt, 200,000 iterations, 32-byte output).
  • HULYAS salt = HULYAS.ZeqField.f=1.287Hz.τ=0.777s.α=1.29e-3 — the kernel constants, bound into the key derivation itself.

The algorithm is defined once in shared/api-core/src/lib/zeqField.ts and mirrored byte-for-byte by the launcher and the bootstrap tool, so a value sealed by any of them opens with any of the others.

The bootstrap problem, and the vault-adjacent answer

The vault is opened by exactly two bootstrap pointers that, by definition, cannot live inside the thing they unlock:

  • ZEQ_FIELD_KEY — 64 hex; the root the row key is derived from.
  • DATABASE_URL — where the vault rows are read from.

At boot, zeq-dev-launch.mjs takes those two, connects to Postgres, decrypts every zsc_secrets row, populates process.env, and spawns the server. A shell-set value always wins over a vault row — the operator's incident-response override path.

Because those two pointers can't be in the vault, they live vault-adjacent: a sealed store (infra/zsc-bootstrap.mjsinfra/per-domain/.zsc-bootstrap), ciphertext-only on the host, encrypted with the same zeqField cipher under one master pointer, ZEQ_BOOTSTRAP_KEY. The master pointer is itself a zsc_secrets row — so the vault protects the keys to its own front door — and is held by the operator. One pointer opens the store; the store yields each domain's two pointers in memory; those open each domain's vault. No secret is ever written to disk in plaintext, and nothing sensitive is printed.

Every secret read is audited

Eliminating the .env file isn't only about encryption at rest — it's about accountability. A plaintext .env is read silently and invisibly by anything in the process; no one can say later which code read which secret, or why. ZSC closes that. Code doesn't reach for an ambient process.env.X; it asks the context:

ZeqContext.read(name, { purpose, zid })

Every read emits one row to the audit chaintransition_type = 'secret_read' — carrying a proof_digest bound to the requester's ZeqID, the Zeqond, and the stated purpose. So a secret access is no longer a silent side effect; it's an attributable, provable event you can inspect in the explorer after the fact: this identity read this secret at this tick for this reason. A .env file can't tell you any of that, which is exactly why the framework doesn't use one and recommends you don't either.

As a service, ZSC is per-machine (multi-tenant) by construction: a machine's secrets are sealed under an HKDF subkey derived from that machine, so cross-machine access isn't merely forbidden by a check — it's cryptographically impossible, because machine A's subkey will not validate machine B's AES-GCM tag.

Recreating a node

export ZEQ_BOOTSTRAP_KEY=<the master pointer, from your ZSC vault>
node infra/zsc-bootstrap.mjs up <domain> --force-recreate # decrypt in-memory → compose up
node infra/zsc-bootstrap.mjs seal # after rotating a key: reseal from the running fleet

Why it matters

Secrets bound to the kernel constants, decrypted only in memory, with the bootstrap keys themselves sealed under a single vault-held master — that is what lets a Zeq node boot from cold with no plaintext secret ever touching the filesystem. ZSC is the security spine the rest of the framework stands on.