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.mjs → infra/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 chain — transition_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.