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. Coming soon: the fleet dashboard route is not yet reachable. Enrollment tokens, coverage tracking and custom DLP rules (items 1, 3 and 4) are unaffected and work today.
- 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 "Cookie: cz_session=<your dashboard session cookie>" \
-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
Configure the Control Zero registry once: add @controlzero:registry=https://npm.controlzero.ai to your .npmrc (or run npm config set @controlzero:registry https://npm.controlzero.ai). It applies to npm install and npx for the whole @controlzero scope.
npm install -g @controlzero/sdk
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.