The keys
Master key (ORCH_MASTER_KEY)
The most critical secret in the whole system. A Fernet key
(base64-encoded 32 bytes) used to encrypt engine API keys at rest.
Without it, no engine API key can be decrypted; products lose access
to every engine they admit.
- Format:
Fernet.generate_key().decode()(43 chars, URL-safe). - Where it lives:
ORCH_MASTER_KEYenv var on the orchestrator. Should be injected at runtime from your secret manager. - Where it doesn’t: Postgres, logs, the orchestrator’s filesystem.
- Rotation: Heavy operation. Requires re-encrypting every
engine’s
engine_key_enc. There’s no built-in rotation script today; doing it requires a custom migration.
ORCH_MASTER_KEY ever leaks, every plaintext engine API key the
orchestrator could decrypt is now exposed. Treat it like a database
master password — store in a secret manager, rotate on schedule,
audit access.
Admin key (ORCH_ADMIN_KEY)
Used by POST /products/register and
PUT /products/{product_id}/policy. Without it, you can’t onboard
new products.
- Format: any opaque string (we use
secrets.token_urlsafe(48)). - Where it lives:
ORCH_ADMIN_KEYenv var on the orchestrator. - How it’s checked: plain string comparison against the
X-Admin-Keyheader. - Rotation: trivial. Generate a new value, swap the env var,
restart the orchestrator. Tooling that uses the old value gets
INVALID_KEY; update them.
Platform API key (per product)
Generated by the orchestrator at/products/register. The product
uses it on every request as X-Platform-Key.
- Format:
pk-sept-<URL-safe random>(42+ chars). - Where it lives:
products.api_key_hash(SHA-256). The plaintext is returned once at registration and never stored. - Rotation: today, no built-in rotation endpoint. To rotate,
generate a new key (re-register the product) and update the
consumer. Future versions will add
POST /products/{id}/rotate-key.
Engine API key (per engine)
Generated by the orchestrator at provision time. The product uses it to authenticate to the specific engine viaX-Engine-Key.
- Format:
sk-sept-<URL-safe random>(42+ chars). - Where it lives:
engines.engine_key_hash— SHA-256, stored in Postgres.engines.engine_key_enc— Fernet-encrypted plaintext, stored in Postgres. The orchestrator decrypts when admitting the same user later.- The engine’s
ENGINE_KEY_HASHenv var — the engine validates incomingX-Engine-Keyagainst this.
- Rotation:
POST /engines/{user_id}/rotate-key. Generates a fresh key, updates both forms in Postgres, swaps the engine’s hash, returns the new plaintext.
What’s never stored
- Plaintext platform API keys.
- Plaintext engine API keys (the encrypted form is stored, but
plaintext only exists in flight: at provision/admit response, and
at the engine’s
ENGINE_KEY_HASHvalidation step). ORCH_MASTER_KEYitself — read from the env at startup.
The auth flow end-to-end
What the orchestrator does NOT enforce
- Per-user authentication beyond the platform key. The
orchestrator trusts that the product authenticated the user
upstream. From the orchestrator’s perspective,
user_idis just an identifier. - Encryption of in-flight network traffic. TLS belongs at the reverse proxy upstream of the orchestrator. The orchestrator itself serves HTTP.
- DDoS protection. Rate limiting at the policy layer is per-product, not per-IP. Layer 7 protection should be upstream.
- MCP credential security. The engine handles per-user OAuth and
Fernet-encrypts MCP credentials at the engine layer using a
separate
AD_ENCRYPTION_KEY. The orchestrator doesn’t see them.
Trust boundaries
The boundaries:- Public ↔ TLS terminator. TLS handshake. Standard.
- Product ↔ orchestrator. Internal network. Plaintext platform key on the wire (HTTP), but only inside the trusted network. Run on a VPC or internal subnet.
- Orchestrator ↔ Postgres. Internal. Use Postgres TLS if you cross VPCs.
- Orchestrator ↔ engine container. Internal Docker network. Plaintext engine key on the wire. Never expose engine ports beyond the host.
Rotation playbook
Rotate ORCH_MASTER_KEY
This requires downtime and is heavy. Don’t rotate unless you must.
- Schedule maintenance window.
- Take a Postgres snapshot.
- Stop the orchestrator.
- Run a one-off script that:
- Reads every
engines.engine_key_encrow. - Decrypts with old
ORCH_MASTER_KEY. - Re-encrypts with new key.
- Writes back.
- Reads every
- Update
ORCH_MASTER_KEYenv on the orchestrator. - Start the orchestrator.
- Verify by admitting a known user; confirm engine key still works.
ENGINE_KEY_HASH won’t match — every existing engine is effectively
locked. Provision new ones; the old ones are dead weight.
Rotate ORCH_ADMIN_KEY
- Generate new value.
- Update env on the orchestrator.
- Restart.
- Update tooling that calls
/products/registeror/policy.
Rotate a platform API key
Today, register the product again with a new slug or update the plaintext secret out-of-band. Future: a dedicated rotation endpoint.Rotate an engine API key
What goes wrong
| Symptom | Likely cause |
|---|---|
INVALID_PLATFORM_KEY for known products | Postgres restored from a backup older than the registration. Re-register. |
| All engine keys reject after restart | ORCH_MASTER_KEY changed without re-encrypt. |
| Product gets a fresh key on every admit | Ignoring the cached key returned by previous admit. Cache and reuse. |
Engine returns INVALID_KEY despite using the orchestrator’s key | The engine’s ENGINE_KEY_HASH env var is stale (engine restarted before the orchestrator pushed the new hash). Re-rotate. |
See also
- Architecture — implementation.
- API reference: auth — the
X-Platform-Keyflow. - Engine contract — the engine side of the same flow.

