Enrollment and Fleet Management
This guide is for org admins who need to roll out Control Zero across an engineering team and keep it healthy. It covers the four pieces an admin actually touches:
- Enrollment tokens — generate single-use tokens that developers exchange for a signed identity on first run.
- Fleet view — see every enrolled machine with online / stale / offline status.
- Coverage tracking — find users in the org who have not enrolled yet, send reminders, exempt contractors, escalate persistent gaps.
- Custom DLP rules — write block / mask / detect rules for your org's specific sensitive content patterns.
If you are a developer who just received an enrollment token, jump
straight to Step 2: Run controlzero enroll.
Architecture in 30 seconds
The developer machine never sends a static bearer token after
enrollment. Every request after enroll is signed with a fresh
keypair the SDK generated locally on first run. The private key
lives at ~/.controlzero/machine.key (mode 0600) and is never
transmitted.
Step 1: Generate an enrollment token
Open the dashboard and go to Settings → Enrollment (or directly:
/settings/enrollment).
You will see a 3-step setup card. Click Generate token in step 2.
A yellow banner appears with the raw token value:
cz_enroll_a3f1c2d8e7b9f4a5...
Copy it now. This is the only time you will see the raw value;
the server only stores sha256(token).
The token defaults to:
- Expiry: 24 hours
- Max uses: 1 (single-use)
- IP allowlist: none
To make a multi-use token, call the API directly:
curl -X POST https://api.controlzero.ai/api/orgs/$ORG_ID/enrollment-tokens \
-H "Authorization: Bearer $FIREBASE_ID_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"ttl_hours": 168,
"max_uses": 50,
"ip_allowlist": ["10.0.0.0/8", "192.168.1.0/24"]
}'
Multi-use tokens require an IP allowlist. The server rejects multi-use without one to limit blast radius if a token leaks.
Step 2: Run controlzero enroll on each machine
The developer pastes the token into one command. Pick the language that matches their workflow:
Python
pip install controlzero
controlzero enroll --token cz_enroll_a3f1c2d8e7b9f4a5...
Node.js
npm install -g @controlzero/sdk
controlzero enroll --token cz_enroll_a3f1c2d8e7b9f4a5...
Go
go install controlzero.ai/sdk/go/cmd/controlzero@latest
controlzero enroll --token cz_enroll_a3f1c2d8e7b9f4a5...
The command:
- Generates a fresh signing keypair locally.
- Computes a stable machine fingerprint (hostname + OS + arch).
- POSTs the token + the machine fingerprint + the public key to
/api/enroll. - Receives back a server-assigned
machine_id,org_id, the org's signing public key, and the org's tamper behavior setting. - Persists state to
~/.controlzero/enrollment.jsonand the private key to~/.controlzero/machine.key(mode 0600).
Org-level signing key provisioning. The org's cryptographic signing keypair is lazily initialized on the first enrollment request for that org. If no signing key exists yet, the backend generates one, stores the private half securely, and returns the public half to the enrolling machine. All subsequent enrollments for the same org receive the same public key. The SDK uses this key to verify the signature on every policy bundle it pulls.
Configurable tamper behavior. The enrollment response includes
the org's tamper_behavior setting, which controls how the SDK
reacts when a policy bundle fails signature verification. Admins
configure this in the dashboard under Settings → Security or
in policy YAML. See
Tamper Detection and Enforcement for
the four available modes and recovery procedures.
If you re-run controlzero enroll on the same machine with a
different token, the backend deduplicates by (org_id, fingerprint)
and returns the same machine_id. This is by design: MDM
thundering herds (same script pushed to every laptop on a schedule)
don't create duplicate fleet entries.
Verify enrollment worked
controlzero heartbeat
Output:
Heartbeat OK.
server_time : 2026-04-08T15:30:42Z
policy_version : 12
If this prints anything else (or errors), see Troubleshooting.
Step 3: Check the fleet view
Open the dashboard and go to Governance → Fleet (or directly:
/governance/fleet).
Each row shows:
| Field | Meaning |
|---|---|
| Hostname | Whatever os.hostname() returned on the machine |
| User | The email captured at enrollment time (if present) |
| OS | Platform + release string |
| Status | online (last_seen under 2 min) / stale (under 1 h) / offline |
| Last seen | Relative time of the most recent heartbeat |
| Decisions (24h) | Tool calls evaluated in the last 24 hours |
The list is paginated (100 per page, max 500 via ?limit=500).
Sort order is last_seen DESC so the freshest machines float
to the top.
Status thresholds are derived in real time from last_seen,
not stored. A machine flips from online to stale 2 minutes
after its last heartbeat with no extra writes.
Step 4: Track coverage gaps
A "coverage gap" is an org member who has not enrolled a machine.
Open Governance → Coverage (/governance/coverage).
Each gap row shows the user, their role, and three action buttons:
Notify
Sends a reminder (the actual notification channel is wired in your
org's notification settings — email, Slack, etc). Increments the
reminder_count on the user's coverage_user_state row so you
can see persistent non-compliance.
Exempt
Removes the user from the coverage list with a required reason. Common cases:
- Contractor with no workstation — they don't have a laptop to enroll.
- PM / non-technical role — they don't run AI tools.
- Maternity / leave — temporary exemption with an
exempt_untildate.
The exemption is recorded in the audit log with the reason and the
admin who set it. Pass an exempt_until ISO timestamp to make it
auto-expire; otherwise it's permanent until manually revoked.
Escalate
Flags the user as "persistent non-compliance" but keeps them on the coverage list. Use this for users who have ignored multiple reminders. The dashboard shows escalated users with a red badge so they stand out at a glance.
Custom DLP rules
DLP rules block, mask, or detect sensitive content in tool calls
and LLM prompts. Open Governance → DLP Rules (/governance/dlp-rules).
Create a rule
Click New rule. Fill in:
- Name: human-readable label, shown in audit logs.
- Pattern: an RE2 regex. PCRE features (lookaheads, backrefs) fail at compile time and the form rejects them inline.
- Category:
pii/secret/ip/financial/compliance/custom. - Action:
detect(log only) /mask(replace match with placeholder) /block(deny the call). - Scopes: which surfaces enforce this rule. Pick any combination of
sdk,gateway,browser_ext,scout.
Test before committing
The form has a built-in Test pattern affordance. Type a sample
input (e.g., customer SSN is 123-45-6789) and hit Test. The
backend compiles the pattern and reports:
compiles: true / false— if false, the error message points to the exact column.matched: true / false— whether the sample triggered.matches: [...]— up to 10 matched substrings.
Iterate until you're happy, then Create as draft.
Lifecycle: draft → live
A new rule lands in draft. Click Publish to transition it to
live. Publishing bumps the org's monotonic policy_version so
the next SDK /api/policy poll picks up the change.
For high-impact rules you can use the two-person approval flow:
- Admin A:
POST /dlp/rules/{id}/request-approval - Admin B (different user):
POST /dlp/rules/{id}/approve
Self-approval is rejected with 403 SAME_ACTOR.
Rollback
POST /dlp/rules/{id}/rollback with a target_version in the body
restores any prior version's pattern + scopes + action. The result
lands in draft (not live) so you have to explicitly re-publish.
This avoids accidentally rolling a regression straight into prod.
The version history is immutable: GET /dlp/rules/{id}/versions
returns every prior state with the edited_by admin and edit_reason.
Cooperative guard via MCP
If your developers run AI coding clients (Claude Desktop, Cursor, Cline) that already speak MCP, install the Control Zero MCP server to give those clients a way to ask before running a tool:
npm install -g @controlzero/mcp-server
Add to your client's MCP config (Claude Desktop example):
{
"mcpServers": {
"controlzero": {
"command": "controlzero-mcp",
"env": {
"CONTROLZERO_API_KEY": "cz_live_xxxxxxxx"
}
}
}
}
The client now has access to three new tools:
guard_check_tool_call— "is it OK if I runbashwith these args?" Returnsallow/warn/denyplus matching rules.guard_test_pattern— admin tooling: test a regex without leaving the chat.guard_list_active_rules— read the org's live rule list so the agent can warn the user upfront.
This is cooperative, not enforcement. The client could ignore the result. Pair the MCP guard with the SDK guard call (hard enforcement) for full coverage.
Operational defaults
| Setting | Default | Override |
|---|---|---|
| Enrollment token TTL | 24 hours | ttl_hours in create body, max 168 (7 days) |
| Enrollment token max_uses | 1 | max_uses in create body, multi-use REQUIRES ip_allowlist |
| Heartbeat interval | 5 minutes | SDK config |
| Policy poll interval | 5 minutes | SDK config (uses ETag, ~99% are 304s) |
| Online status threshold | 2 minutes | hardcoded in fleet derivation |
| Stale status threshold | 1 hour | hardcoded in fleet derivation |
| Audit batch size | 500 entries | enforced server-side, SDK batches client-side |
Troubleshooting
"enroll failed: HTTP 503: FEATURE_DISABLED"
The org's ENROLLMENT_API_ENABLED flag is off. The API returns 503
for every enrollment endpoint when disabled. Ask your admin to set
ENROLLMENT_API_ENABLED=true in the deployment env vars and restart
the backend.
"enroll failed: HTTP 401: INVALID_TOKEN"
The token is wrong, expired, revoked, or already used (single-use). Generate a fresh one from the dashboard.
"enroll failed: HTTP 401: TOKEN_EXHAUSTED"
A multi-use token hit its max_uses cap. Generate a new one with a
higher cap, or rotate to single-use tokens distributed via MDM.
"enroll failed: HTTP 403: IP_NOT_ALLOWED"
The token has an ip_allowlist and the calling machine's source IP
is not in any of the listed CIDR blocks. Either add the developer's
office subnet to the allowlist or distribute single-use tokens that
don't have IP restrictions.
Heartbeat returns 401 INVALID_SIGNATURE
The local private key was deleted, corrupted, or doesn't match the public key registered at enrollment time. The fix is to re-enroll with a fresh token:
rm -rf ~/.controlzero/
controlzero enroll --token FRESH_TOKEN_HERE
Heartbeat returns 401 CLOCK_SKEW
The machine's system clock is more than 5 minutes off from the
backend's wall clock. Run ntpdate (Linux) or check the System
Settings clock sync (macOS / Windows).
Machine appears in /governance/fleet but last_seen doesn't update
Check the SDK process is actually running and not crashed. Look at
~/.controlzero/sdk.log (Python) or journalctl -u controlzero
(Linux systemd). Restart the SDK; the next heartbeat will populate
last_seen immediately.
Production verification
The /api/health and synthetic sign-up monitor are public:
curl -sf https://api.controlzero.ai/health
# {"status":"healthy","service":"control-zero","version":"0.1.0",...}
The synthetic monitor runs every 5 minutes and exercises the full sign-up + enrollment + heartbeat path against production. If it goes red, the GitHub Actions dashboard shows the failing step within 5 minutes.