Secrets Management
Status: BETA
Control Zero offers two ways to handle LLM provider keys (OpenAI, Anthropic, Google, Cohere, and others). You choose the approach that fits your security requirements.
The secrets vault and on-demand retrieval are in Beta. The API is stable and usable, but the feature is newer than core policy enforcement and is still in its soak period. Approvals on secret reads are also Beta and off by default -- see Secrets approvals.
Two Modes
Mode 1: Policy-Only (Default)
You manage your own provider keys. Control Zero only handles policy enforcement. Your keys never touch our servers.
from controlzero import Client
import openai
import os
# You manage the OpenAI key yourself (env var, vault, etc.)
openai_client = openai.OpenAI(api_key=os.environ["OPENAI_API_KEY"])
# Control Zero only enforces policies
cz = Client(api_key="cz_live_...")
# Check policy before calling OpenAI
cz.guard("llm", method="generate", args={"model": "gpt-5.4"})
response = openai_client.chat.completions.create(model="gpt-5.4", messages=[])
This is the default. No additional configuration is needed.
Mode 2: Secrets Vault (Optional)
Store your provider keys in Control Zero's encrypted vault. Your code reads a value on demand by name, when it needs it. There are no plaintext keys in your codebase or environment variables.
from controlzero import Client
cz = Client(api_key="cz_live_...")
# Read a stored key by its dashboard name, on demand.
# This is a policy-gated call (and can require human approval).
openai_key = cz.get_secret("OPENAI_API_KEY")
Retrieval is an explicit, per-call read. There is no automatic injection into your provider clients, no background substitution, and no session-end wipe -- the SDK simply returns the plaintext value to your code when you ask for it, after the policy gate allows the read. You decide how to use it (for example, passing it to your OpenAI client).
How the Vault Works
Storing a Secret
- Open the Control Zero dashboard and navigate to your project.
- In the Secrets Vault section, click Add Secret.
- Select the provider (OpenAI, Anthropic, Google, Cohere, or Custom).
- Paste your API key and give it a name (e.g.,
OPENAI_API_KEY). - Click Store Secret.
The plaintext value is shown exactly once. After that, only a masked prefix is displayed. The key is encrypted before it reaches our database.
Retrieving a Secret in the SDK
Each get_secret(name) call is a live, policy-gated read against the Control
Zero API: GET /api/secrets/{name}. The argument is the secret's name as
shown in the dashboard (case-sensitive) -- not a provider type. There is no
secrets_enabled flag and no secret bundle fetched at startup; you read each
value at the moment you need it.
The flow for every read:
- The SDK consults the policy engine for
Secrets:read <name>. - If allowed, the SDK fetches the value over the network and returns the plaintext string to your code.
- If the read is denied but approval-eligible, the SDK can wait for a human approval (HITL) before returning. See Secrets approvals.
- If denied with no approval, the SDK raises an error and returns nothing.
The network fetch has a hard timeout (about 10 seconds) so a slow or unreachable backend fails fast rather than hanging your call.
Python
from controlzero import Client
cz = Client(api_key="cz_live_...")
# Read by the secret's name (as shown in the dashboard).
openai_key = cz.get_secret("OPENAI_API_KEY")
Node.js
import { Client } from '@controlzero/sdk';
const client = new Client({
apiKey: 'cz_live_...',
policyFile: './controlzero.yaml',
});
// Read by the secret's name (as shown in the dashboard).
const openaiKey = await client.getSecret('OPENAI_API_KEY');
Security Model
Encryption
- At rest: Secrets are encrypted before storage. The encryption key is derived from your project API key. Control Zero servers never store plaintext secret values.
- In transit: All communication between SDKs and the Control Zero API uses TLS 1.3.
- In memory: When you call
get_secret(name), the SDK returns the decrypted plaintext value to your process. That value lives in your application's memory for as long as your code holds it -- the SDK does not write it to disk, log it, or include it in error messages, but it does hand the plaintext back to your code by design.
Key Derivation
The encryption key used for secrets is derived from your project API key, not stored separately. This means:
- Only holders of the project API key can decrypt the secrets.
- Rotating your project API key automatically invalidates the derived key.
- The derivation uses a cryptographic key derivation function with a fixed context string, ensuring the derived key is cryptographically independent from the API key itself.
Access Control
- Secrets are scoped to a single project. They cannot be accessed across projects.
- Storing and deleting secrets requires dashboard access or an organization-level API token.
- Every SDK read is gated by the policy engine, and the secrets backend independently re-checks the policy on each read -- so a caller that bypasses its own SDK gate still fails the backend check.
What We Cannot See
- Plaintext secret values are never stored on our servers.
- Our database contains only the encrypted ciphertext.
- Our staff cannot decrypt your secrets without your project API key.
Value Redaction in Logs and Audit
The secret value is returned to your code, but it is kept out of the governance plane. The value never appears in:
- Audit logs: only the secret name and a hash fingerprint are recorded.
- Telemetry: events for
Secrets:*actions drop any value-shaped field. - Errors and stack traces: explicit redaction in every exception path.
As defense in depth, the SDK runs a leak guard (a credential/DLP-style scanner) over outbound audit payloads before they are shipped. If a value-shaped string ever reaches an audit payload, the SDK fails closed rather than transmit it.
Performance Impact
| Operation | When It Runs | Network Call |
|---|---|---|
initialize() | Once at startup | Yes (policy bundle) |
get_secret() | Per read | Yes (live, policy-gated, ~10s timeout) |
guard() | Per action | No (local evaluation) |
guard() is evaluated locally with no network round-trip. get_secret(name)
is a live read: each call makes a fresh, policy-gated request to fetch the
current value. Cache the returned value in your own code if you call it on a
hot path.
Supported Providers
| Provider | Type String | Example Key Prefix |
|---|---|---|
| OpenAI | openai | sk-proj-... |
| Anthropic | anthropic | sk-ant-... |
google | AIza... | |
| Cohere | cohere | co-... |
| Custom | custom | Any format |
The provider is metadata you choose when you store a secret in the dashboard. You still retrieve the value by its name, not by provider type.
Choosing Between Modes
| Consideration | Policy-Only (Default) | Secrets Vault |
|---|---|---|
| Provider keys on your servers | Yes | No |
| Provider keys in Control Zero | No | Yes (encrypted) |
| Environment variable management | You handle it | Not needed |
| Key rotation | You handle it | Via dashboard |
| Setup complexity | None | One-time per key |
| How the SDK gets the value | n/a | get_secret(name) per read |
| Compliance (key centralization) | Keys in your infra | Keys in one encrypted vault |
Next Steps
- Follow the Quick Start guide, which includes an optional secrets step.
- Read Secrets approvals for human-in-the-loop gating on secret reads.
- See the Python SDK or Node.js SDK for full API reference.
- Review the API Reference for programmatic secret management.