OAuth Environment Governance
This document is the canonical statement of the laws governing OAuth credential custody and environment binding for Zen Mesh runtimes. It exists to prevent recurring drift between where a credential lives, where a runtime runs, and which identity a runtime presents to an external identity provider.
1. Canonical Bitwarden identities
External OAuth credentials are custodied in Bitwarden under owner-controlled identities. The canonical identities are:
- Bitwarden folder:
zen - Sandbox OAuth item:
zen-mesh-oauth-sandbox
Item identity is exact: folder and item names must match exactly. Vague similarity ("similarly named credential") never satisfies a lookup; an ambiguous or duplicate item must be rejected, not resolved by guesswork.
Secret values are never recorded in documentation, reports, Git, handoffs, logs, screenshots, fixtures, shell history, ConfigMaps, or executor prose. Only identities, references, field names, types, and non-sensitive revision metadata may be written down.
2. Credential custody: sealed at the owner keyroot, not cluster storage
The Bitwarden API credential used for OAuth bootstrap is a custody-sealed bootstrap artifact at the owner keyroot: an age-encrypted file sealed to the Zen Lock custody master identity. The custody chain is:
owner keyroot custody-sealed bootstrap
-> operator/custody authority (Zen Lock custody master identity)
-> provider use (in-memory, on demand)
The running staging cluster hosts the Zen Lock runtime estate; it does not store the Bitwarden bootstrap material. Never describe or design the custody chain as cluster-held: decryptability follows the custody master identity, and the authoritative artifact lives at the operator keyroot, not inside any cluster.
Laws that follow:
- OAuth bootstrap consumes the existing custody-held capability through its existing lawful retrieval path (custody-master decrypt, in-memory use).
- No Bitwarden-sync prerequisite exists for OAuth work. Building a new Bitwarden sync path, re-requesting credentials from the owner, or asking the owner to copy OAuth credentials elsewhere are all out of law.
- The custody path may only be changed when a verified product defect in the existing path is discovered — never for convenience of a consuming task.
If any custody path is found to persist credentials as plaintext in an unsafe location, work must stop before propagating the path, and the defect must be classified precisely.
3. Bitwarden credential classes are not interchangeable
Two different Bitwarden credential classes exist, with materially different capabilities:
| Class | Can do | Cannot do |
|---|---|---|
Password Manager API key (scope=api) | Authenticate to the Bitwarden API (client_credentials) | Non-interactively decrypt Password Manager vault folder/item names or contents — those remain end-to-end encrypted under the master-password-derived vault key |
| Secrets Manager machine credential | Non-interactive, scoped access to Secrets Manager projects/secrets | Nothing about Password Manager vault items; it is a separate product |
Exact-match lookup of a Password Manager folder/item by name with a Password Manager API key is therefore cryptographically impossible non-interactively — this is a structural property, not a bug to engineer around. A Secrets Manager machine account is the appropriate future model for non-interactive provider integration; adopting one later is an architecture decision, not a prerequisite.
4. Sandbox OAuth bootstrap path (current)
The sandbox Google OAuth credential follows a bounded bootstrap chain that does not depend on non-interactive Bitwarden vault access:
operator-local sandbox OAuth input (owner-provided, restrictive permissions)
-> Zen Lock oauthbundles seal (canonical bundle: google-oauth-sandbox,
bound to oauth_environment=sandbox and the sandbox recipient)
-> sandbox-specific Zen Lock projection (sandbox Google fields only)
-> runtime on the staging substrate
-> Google sandbox OAuth proof
Provenance law: Bitwarden Password Manager remains the owner's durable human vault copy, but the sealed bundle cannot currently be reconciled mechanically against the Bitwarden item — doing so requires an interactive unlock ceremony. Do not claim byte-equality or verified provenance between the sealed bundle and the Password Manager item; record provenance honestly as owner-provided input plus custody-sealed state.
5. Runtime substrate and OAuth environment are separate concepts
A runtime substrate (the cluster or machine a workload runs on) and an OAuth environment (which credential identity the workload presents) are independent. A proof running on the staging substrate may legitimately qualify the sandbox OAuth credential.
Every OAuth-bearing runtime configuration must state all three explicitly:
| Concept | Example |
|---|---|
runtime_substrate | staging |
oauth_environment | sandbox |
oauth_project | zen-mesh-oauth-sandbox |
An environment must never be inferred from the substrate it happens to run on, and substrates must never be renamed to match a credential's environment.
6. Separate Google OAuth clients per environment
Each OAuth environment (sandbox, staging, prod) uses its own Google OAuth project and client — its own client ID and client secret. A shared hostname during qualification does not merge environment identities: even when two environments share public hosts, their credentials remain strictly separate.
The redirect URI is owned by the runtime and derived mechanically from the actual code and route configuration — never assumed.
7. Temporary shared public hostname (owner-decided compatibility state)
Owner decision (current): for now, prod, staging, and sandbox OAuth
environments operate on the shared public hosts app.zen-mesh.io and
api.zen-mesh.io. This is an explicit temporary compatibility arrangement,
not permanent architecture.
During the compatibility period:
- Each environment keeps its own separate OAuth client, secret, bundle, recipient, projection, and environment identity. Sharing a hostname shares nothing else.
- Hostname alone is NOT environment authority. Which credential an environment uses is bound by bundle identity, recipient, and projection — never by which host the callback arrives on.
- The sandbox bundle keeps its canonical future origin (
api.sandbox.zen-mesh.io) recorded, plus the narrowly-allowed temporary shared-host callback (https://api.zen-mesh.io/auth/callback/google) under explicit compatibility configuration — exact path, no wildcard origins, no arbitrary callback hosts. - The exception is temporary and removable: it must be represented as compatibility/debt state wherever encoded, and its removal must be mechanically testable. It must not be treated as the target topology by tests, configuration, or documentation.
8. Hostname migration (governed, single change)
The intended end-state topology:
| Environment | App origin | API origin |
|---|---|---|
| prod | app.zen-mesh.io | api.zen-mesh.io |
| staging/uat | app.uat.zen-mesh.io | api.uat.zen-mesh.io |
| sandbox | app.sandbox.zen-mesh.io | api.sandbox.zen-mesh.io |
The migration to per-environment hostnames — including every Google OAuth client's registered redirect URIs — is governed as one coordinated change. Piecemeal host-by-host migration that leaves OAuth clients registered against a mix of old and new origins is forbidden.
9. No environment secret fallback
A runtime configured for an environment must use that environment's credential — or fail closed:
- sandbox credential missing → fail; never fall back to staging or prod.
- staging credential missing → fail; never fall back to sandbox or prod.
Silent cross-environment consumption is one of the highest-severity defects in this domain. Fallback is forbidden even when it would "just work".
10. Secret-reference discipline
Downstream consumers (including the Maestro orchestrator) receive capabilities and references, not secret values:
- handoffs carry identity, reference, retrieval method, and non-sensitive revision metadata only;
- observability may represent runtime-secret availability as a boolean, never as a value;
- if tooling must materialize a secret, it stays memory/ephemeral-scoped with restrictive permissions and is removed immediately after use — never echoed, never logged, never persisted to shared temporary storage.
11. Credential exposure requires rotation
When an external API credential is exposed to an executor session or transcript, it is rotation-required from that moment — regardless of where the value did or did not persist. Recording it as a pending owner action is mandatory; trusting "it was not written anywhere" is not.
Current owner action from this law:
ROTATE_BITWARDEN_PASSWORD_MANAGER_API_KEY(credential hygiene).
This rotation is hygiene only. It does not change the Password Manager cryptographic limitation described in §3, and sandbox OAuth work does not block on it.
12. Status anchors
- Exact-identity Bitwarden lookups only; ambiguity is a rejection, not a prompt to choose.
- Custody is at the owner keyroot under the Zen Lock custody master identity — never described or designed as cluster-held material.
- A Password Manager API key and a Secrets Manager machine credential are different classes; neither substitutes for the other.
- During the shared-host compatibility period, hostname alone is not environment authority: identity binds by client, bundle, recipient, and projection; a shared hostname never erases environment identity, and a staging substrate never renames a sandbox credential.
- A new OAuth environment follows the same qualification path as sandbox — governed bootstrap, independent audit, and live login proof — before production is attempted; production credentials are created only after the next-narrower environment has fully qualified.