Skip to main content

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.

Maturity

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

  1. Open the Control Zero dashboard and navigate to your project.
  2. In the Secrets Vault section, click Add Secret.
  3. Select the provider (OpenAI, Anthropic, Google, Cohere, or Custom).
  4. Paste your API key and give it a name (e.g., OPENAI_API_KEY).
  5. 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:

  1. The SDK consults the policy engine for Secrets:read <name>.
  2. If allowed, the SDK fetches the value over the network and returns the plaintext string to your code.
  3. If the read is denied but approval-eligible, the SDK can wait for a human approval (HITL) before returning. See Secrets approvals.
  4. 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

OperationWhen It RunsNetwork Call
initialize()Once at startupYes (policy bundle)
get_secret()Per readYes (live, policy-gated, ~10s timeout)
guard()Per actionNo (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

ProviderType StringExample Key Prefix
OpenAIopenaisk-proj-...
Anthropicanthropicsk-ant-...
GooglegoogleAIza...
Coherecohereco-...
CustomcustomAny 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

ConsiderationPolicy-Only (Default)Secrets Vault
Provider keys on your serversYesNo
Provider keys in Control ZeroNoYes (encrypted)
Environment variable managementYou handle itNot needed
Key rotationYou handle itVia dashboard
Setup complexityNoneOne-time per key
How the SDK gets the valuen/aget_secret(name) per read
Compliance (key centralization)Keys in your infraKeys in one encrypted vault

Next Steps