ZSC — Zeq Secure Context
ZSC is the framework's secret store. Where most apps drop credentials
into .env and call it a day, ZSC treats every secret read as a
state-machine transition: encrypted at rest with AES-256-GCM,
gated by ZID permissions, recorded as a hash-linked audit entangled state
row with a verifiable proof digest, and rotated automatically on the
1.287 Hz HulyaPulse cadence.
Concrete picture: STRIPE_SECRET_KEY stops being a file someone might
cat and starts being a row in zsc_secrets that only the owner ZID
can decrypt, only after the read produces a secret_read row in
audit_log. Lose the master key → no plaintext. Leak the master →
every prior read is still on the entangled state, forensics intact.
Why this replaces .env
A loose .env might once have held boot config, but in this framework
the deployment origin is auto-detected and the only env values that
stay outside the vault are the two host-level bootstrap pointers. .env
is certainly not fine for
the kind of secrets that, if leaked, let an attacker drain a wallet
or impersonate the foundation. The framework already runs everything
through audit_log with prev_hash linkage; ZSC just extends that
contract to the secret-read step.
Problem with .env | What ZSC does instead |
|---|---|
| Plaintext on disk | AES-256-GCM at rest, key from PBKDF2-200k over a 32-byte master |
Anyone with shell access can cat it | Decrypt requires the in-process master; the bytes never hit disk again |
| No record of who read what, when | Every read writes a secret_read row with proof_digest = SHA-256(name | zid | transitionId | purpose) |
| Bound to file lifetime | Bound to a ZID + permission list; revocable per caller |
| Stale forever | Rotation daemon re-encrypts under a fresh IV every expires_zeqond |
.env is a deprecated legacy bootstrap in this framework — the
target is that every secret lives in the vault. ZeqContext.read()
still falls through to process.env when a key isn't in zsc_secrets,
but that fall-through is a transitional shim, not a feature to lean on:
it's the non-fatal migration ramp that lets you move entries into the
vault one at a time without touching the call site. The only values
that legitimately stay outside the vault are the two host-level
bootstrap pointers — ZEQ_FIELD_KEY (the master that decrypts the
vault) and DATABASE_URL (where the vault lives) — and those belong in
the operator's shell rc / OS keychain / Docker secret, never in a
repo-tracked .env.
The lifecycle of one secret read
A call like ZeqContext.read("STRIPE_SECRET_KEY", { zid: callerZid })
runs through this path:
- Rate-limit check (in-memory map, keyed by
(name, zid)). If the caller has hit 5 denials in the last 60 Zeqonds (≈ 46.6 s), we short-circuit withreason="rate_limited"before the DB hit — prevents timing oracles. - Vault read via
vaultGetWithMeta(name)— selectsvalue_enc,value_iv,bound_zid,permissionsfromzsc_secrets. - Permission gate — caller's ZID must be
bound_zid, inpermissions[], or equal to the system-bypass constant"ZEQ-SYS". Failure →secret_deniedaudit row + denial counter bump. - Decrypt — AES-256-GCM, key derived once per process via
zeqField.ts::deriveKey(). Auth-tag failure →nullreturned, audit row markeddecrypt_failed. - Audit row —
secret_readwritten toaudit_logwithproof_digest = SHA-256(name | zid | transitionId | purpose).transitionIdis the entangled state's stable row identifier — using it (notzeqond_number) keeps the proof verifiable even when chain-write contention bumps the zeqond. - Counter bump —
last_read_zeqond+read_countupdated fire-and-forget on the row (we don't block the read for forensics).
Every step that can fail returns null and falls through to
process.env. The vault is a non-fatal optimisation layer — DB
down, decrypt failed, permission denied, all degrade to "fall through
to env" so production never crashes because the vault is unhappy.
The four transition types
audit_log.transition_type carries a secret_* family ZSC writes:
| Type | When | What it proves |
|---|---|---|
secret_set | vaultSet() upsert | A secret arrived in the vault under this name, owner ZID, and permission list. Forensic origin-of-value. |
secret_read | Successful ZeqContext.read() | This ZID accessed this secret at this Zeqond. The entangled state is the forensic record. |
secret_rotated | rotationDaemon re-encrypted | The IV changed at this Zeqond. Old ciphertext is dead, new one is live. |
secret_denied | Permission gate rejected, or rate-limiter tripped | Someone tried to read this secret and the gate said no. Audit-visible attack signal. |
All four rows carry proof_digest, all four hash-link backward via
prev_hash. Run pulse > context audit STRIPE_SECRET_KEY and you get
the full life-cycle for that name in chronological order.
Permissions — ZID-gated reads
zsc_secrets.bound_zid is the owning ZID; zsc_secrets.permissions
is the list of additional ZIDs allowed to read. The gate works:
isPermitted(callerZid, boundZid, permissions) =
callerZid === "ZEQ-SYS" || // system bypass
callerZid === boundZid || // owner
permissions.includes(callerZid) // explicit grant
ZEQ-SYS is the system ZID — boot-time reads (the scheduler, the
rotation daemon, the audit-log writer itself) use it to break the
chicken-and-egg of "you need ZSC to start, but starting writes to the
chain which needs ZSC." Operators see the bypass in the audit row's
actor_zid field, so it's not invisible — just exempt from the gate.
Grant + revoke are admin operations:
POST /api/zsc/grant { name, zid } → adds zid to permissions[]
POST /api/zsc/revoke { name, zid } → removes zid from permissions[]
Both write secret_set (with a purpose_tag of granted /
revoked) so the permission history is itself audit-chained.
Rate limiter — the 5-in-60-Z window
Two reasons we don't just bounce denied reads with a 403:
- Timing oracle protection. A denial that takes 8 ms (DB hit
- permission gate) and a permission grant that takes 12 ms (DB hit + decrypt + audit write) leak the existence of a secret to anyone with a stopwatch. The rate-limiter trips before the DB hit so denied reads have flat-rate latency.
- Cheap attack surface. Without throttling, a leaked API key could enumerate every secret name via permission failures alone.
The window is DENIAL_WINDOW_ZEQONDS = 60 (≈ 46.6 s) with
DENIAL_THRESHOLD = 5. Hit 5 denials in 60 Z and the 6th attempt
returns reason="rate_limited" until the window clears. A successful
grant in the middle of an active window does not bypass the limiter —
the denied state is sticky until the window expires.
Rotation — the daemon that re-encrypts every secret
Every 100 Zeqonds (≈ 77.7 s) the rotation daemon ticks. It scans
zsc_secrets WHERE expires_zeqond < currentZeqondNumber(),
re-encrypts up to 64 rows per batch under a fresh IV, and bumps
expires_zeqond by ROTATION_PERIOD_ZEQONDS (default 86,400 ≈ 18.6 h).
Each rotation writes a secret_rotated audit row carrying:
proof_digest = SHA-256(name | "ZEQ-SYS" | transitionId | "auto_rotated")
The proof is verifiable against the entangled state after the fact — you can re-derive it from the row's stable fields and confirm bit-for-bit. That's the framework's standard "every transition produces a forensically reproducible proof" contract.
The daemon is self-throttled — if a previous tick is still in
flight at the next 100-Z mark, the new tick is skipped. No queue
build-up, no thundering herd. Operators tune the batch size and
period via ZSC_ROTATION_BATCH and ZSC_ROTATION_PERIOD_ZEQONDS.
Vault-first, env-fallback — the non-fatal contract
ZeqContext.read(name, { zid }) returns one of three things:
- Vault hit + permitted → plaintext from the encrypted row.
Audit row:
secret_read. - Vault hit + denied →
null. Audit row:secret_denied. Caller's code path: same asprocess.env.Xbeing unset. - Vault miss → fall through to
process.env[name]. No audit row. Caller's code path: identical to today's.envsemantics.
This is the migration ramp, not a destination. Existing code that
reads process.env.STRIPE_SECRET_KEY keeps working during the
cut-over — the day someone calls POST /api/zsc/set with that name,
the code path silently flips to vault-served. No call-site changes, no
deploys. The end state is that nothing but the two bootstrap pointers
is served from env at all.
The encryption substrate — reused, not reinvented
ZSC doesn't ship a new cipher. It calls zeqEncrypt / zeqDecrypt
from shared/api-core/src/lib/zeqField.ts — the same AES-256-GCM
primitive that already protects waitlist emails, contact form PII,
and BYOK credentials. One key derivation, one salt material
(HULYAS.ZeqField.f=1.287Hz.τ=0.777s.α=1.29e-3), one rotation
contract via ZEQ_FIELD_KEY_PREV.
The master key sits at the root: a 32-byte secret (64-hex
ZEQ_FIELD_KEY) loaded once at boot from the host-level bootstrap
pointer — the operator's shell rc, OS keychain, systemd
EnvironmentFile, or Docker secret — never from a repo-tracked file.
That bootstrap layer is documented separately in
Operate → ZSC Bootstrap.
What this gives you in practice
Three concrete wins:
- An attacker who pops a shell on an api-core node can read every
secret today via
cat .env. With ZSC live, they can decrypt nothing without also dumping the running process's memory (the master key never hits disk) — and every read they perform is an audit row. - A leaked Stripe key is no longer a permanent secret. When the
rotation daemon ticks, the IV rotates; if you also rotate the
upstream Stripe credential and
vaultSetit under the same name, the leaked plaintext is dead even though the audit entangled state is intact. - You can prove who read what. A compliance request asks
"did anyone ever read the prod API key for $tenant during this
incident window?" — that's a single SQL query against
audit_logfiltered bytransition_type = 'secret_read'andproof_digest.
Where to look next
- ZSC Audit Trail — the four transition types, how
proof_digestis computed, how to verify the entangled state - BYOK — the sister doc; BYOK uses the same
zeqFieldcipher for LLM credentials but is account-scoped where ZSC is system-scoped - Operate → ZSC Bootstrap — KMS adapter setup, master-key rotation, recovery procedures
- Build → Context CLI — the 8
pulse > context …commands operators use day-to-day /portal/secrets/— the admin UI (paired with the CLI)- API reference:
/api/zsc/list,/info,/set,/rotate,/grant,/revoke,/audit,/delete,/probe-permission