Skip to main content

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:

  1. Enrollment tokens — generate single-use tokens that developers exchange for a signed identity on first run.
  2. Fleet view — see every enrolled machine with online / stale / offline status.
  3. Coverage tracking — find users in the org who have not enrolled yet, send reminders, exempt contractors, escalate persistent gaps.
  4. 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:

  1. Generates a fresh signing keypair locally.
  2. Computes a stable machine fingerprint (hostname + OS + arch).
  3. POSTs the token + the machine fingerprint + the public key to /api/enroll.
  4. Receives back a server-assigned machine_id, org_id, the org's signing public key, and the org's tamper behavior setting.
  5. Persists state to ~/.controlzero/enrollment.json and 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:

FieldMeaning
HostnameWhatever os.hostname() returned on the machine
UserThe email captured at enrollment time (if present)
OSPlatform + release string
Statusonline (last_seen under 2 min) / stale (under 1 h) / offline
Last seenRelative 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_until date.

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:

  1. Admin A: POST /dlp/rules/{id}/request-approval
  2. 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 run bash with these args?" Returns allow / warn / deny plus 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

SettingDefaultOverride
Enrollment token TTL24 hoursttl_hours in create body, max 168 (7 days)
Enrollment token max_uses1max_uses in create body, multi-use REQUIRES ip_allowlist
Heartbeat interval5 minutesSDK config
Policy poll interval5 minutesSDK config (uses ETag, ~99% are 304s)
Online status threshold2 minuteshardcoded in fleet derivation
Stale status threshold1 hourhardcoded in fleet derivation
Audit batch size500 entriesenforced 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.