Multi-user keys
Supported modes: Hosted Hybrid Available in: Free Solo Teams SDK: 1.6.0+
Real teams share API keys. A project key gets dropped in everyone's ~/.controlzero/config.yaml; a CI key gets used by every job; a team-shared agent key powers every developer's coding assistant. The API key is a machine credential, not a per-user identity.
Without per-user identity, approvals are broken: every request is attributed to the key creator, the requestor's /my-requests page is empty, the audit log lies, and the self-approval guard mis-fires.
This page explains how identity works on Control Zero so approvals (and audit) attribute requests to the actual human.
The contract
When a top-level email is configured, the SDK sends an identity claim on every backend call:
- Header:
X-CZ-Requestor-Email: alice@acme.com - Source: the top-level
emailkey in~/.controlzero/config.yaml, written bycontrolzero install --email. That file is the only place the SDK reads the operator email from — there is noCONTROLZERO_REQUESTOR_EMAILenvironment variable and nouser_email=constructor argument.
If email is missing, nested under another key, or the file is malformed, the SDK sends no identity header and the backend records identity_provenance: unknown. Nothing errors and nothing warns. Check the file itself after install rather than assuming it took.
Backend resolves the claim against users.email + org_membership:
- Known user in this org →
requestor_user_id = users.id. Normal flow. - Email in
usersbut not in this org results inE1706 HITLIdentityNotInOrg. - Email unknown results in
E1707 HITLIdentityRequired.
What the SDK does on install
controlzero install --email alice@acme.com writes:
# ~/.controlzero/config.yaml
api_key: cz_live_...
email: alice@acme.com
email is a top-level key. The SDK does not read a nested identity.email.
If --email is omitted and stdin is a TTY, the CLI prompts. Non-TTY environments (CI, Docker entrypoints, Lambda) must either pass --email explicitly at install time or write the top-level email key into ~/.controlzero/config.yaml directly. There is no environment-variable equivalent.
What the SDK does at runtime
On every Client.guard() / Client.get_secret() / Client.request_approval() call, the SDK attaches the X-CZ-Requestor-Email header. The backend resolves it. Audit rows record both the api_key_id AND the claimed_email -- so a forensic review sees the full attribution chain.
Threat model: spoofable claim
A user with the API key can claim ANY email in the org via env var or config edit. This is intentional: the API key already grants full access to the policy bundle and the audit log; the claim adds no new powers it just attributes them.
Forensic trail: the audit log records both api_key_id AND claimed_email. If alice's key was used to claim bob@acme.com, the row says so. The dashboard shows "via shared key (3 distinct claimants this week)" as a badge in the approver UI when the same key has had more than 3 claimants in 7 days.
This is the same threat surface as git config user.email. No one verifies it; commits get blamed on whoever the config says. Same here.
Pure-local mode is exempt
If you call Client(policy={...}) with no api_key (pure local mode), no identity check fires. The 11-line Hello World still works unchanged. controlzero install --email is only required when an api_key is configured.
Existing customers (upgrading to 1.6.0)
If no top-level email is configured, nothing fails at construction time and
nothing warns. Audit and bundle traffic continue; the identity header is simply
omitted. The first approval request your code raises is where it surfaces, as a
hard E1707 error. Run controlzero install --email <your-email> to set it.
When a user leaves the org
A user removed from the org mid-flight gets E1708 HITLIdentityClaimRejected on the next poll. The pending approval request synthetically-denies; the SDK raises HITLIdentityClaimRejected. The audit row records the rejection.
CI keys without a human
CI keys are by definition not user-scoped. For approval purposes, install with controlzero install <agent> --email ci@acme.com (a real user in the org, typically the ops lead) so escalations have a human accountability anchor.
For pure-machine audit (no approvals), the CI key works fine without identity. Audit rows show claimed_email: null and identity_provenance: 'legacy'.
See also
- Approval Workflow. The parent concept
- E1706
- E1707
- E1708