Skip to content

Enrollment Keys

Enrollment keys are short-lived, usage-limited tokens that authorize new agents to register with the Breeze API. Each key is scoped to an organization and optionally pinned to a specific site, ensuring that agents land in the correct place in the multi-tenant hierarchy without exposing long-lived credentials.


ConceptDescription
Enrollment keyA 64-character hex string (32 random bytes) presented by the agent during the POST /api/v1/agents/enroll call.
SHA-256 + pepperThe raw key is never stored. It is hashed with a server-side pepper before being written to the database.
TTL (expiresAt)Time-to-live. After this timestamp the key is rejected. Default: 60 minutes from creation.
Max usage (maxUsage)Maximum number of successful enrollments allowed. Default: 1. Range: 1 — 100,000.
Usage count (usageCount)Incremented atomically on each successful enrollment. Once it reaches maxUsage, the key is exhausted.
Site pinning (siteId)When set, every agent that enrolls with this key is placed into the specified site. The enrollment endpoint requires a siteId to be present on the key.
Enrollment secretA separate, static secret (AGENT_ENROLLMENT_SECRET environment variable) that gates the enrollment endpoint in production. This is independent of the enrollment key itself.

  1. Authenticate with a user session that has the organizations:write permission and has completed MFA.
  2. Send a POST request to /api/v1/enrollment-keys with the desired scope.
  3. Copy the key field from the 201 response. This is the only time the raw key is returned.
  4. Embed the key in your agent installer, deployment script, or MDM payload.
Terminal window
curl -X POST https://breeze.yourdomain.com/api/v1/enrollment-keys \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"orgId": "ORG_UUID",
"siteId": "SITE_UUID",
"name": "Chicago Office Q1 Deploy",
"maxUsage": 50,
"expiresAt": "2026-04-01T00:00:00Z"
}'

Request body:

FieldTypeRequiredDefaultDescription
orgIduuidDependsInferred for org-scoped usersTarget organization. Required for partner and system scopes.
siteIduuidNonullPin enrolled agents to a specific site. The enrollment endpoint will reject keys without a siteId.
namestringYesHuman-readable label (1—255 characters).
maxUsageintegerNo1Maximum enrollments allowed (1—100,000).
expiresAtdatetimeNoNow + TTLISO 8601 expiration. Defaults to current time plus ENROLLMENT_KEY_DEFAULT_TTL_MINUTES (default 60).

Response (201):

{
"id": "a1b2c3d4-...",
"orgId": "ORG_UUID",
"siteId": "SITE_UUID",
"name": "Chicago Office Q1 Deploy",
"usageCount": 0,
"maxUsage": 50,
"expiresAt": "2026-04-01T00:00:00.000Z",
"createdBy": "USER_UUID",
"createdAt": "2026-03-02T12:00:00.000Z",
"key": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2"
}

Terminal window
# All non-expired keys for the authenticated org
curl https://breeze.yourdomain.com/api/v1/enrollment-keys?expired=false \
-H "Authorization: Bearer $TOKEN"
# Filter by org (partner/system scope)
curl "https://breeze.yourdomain.com/api/v1/enrollment-keys?orgId=ORG_UUID&page=1&limit=25" \
-H "Authorization: Bearer $TOKEN"

Query parameters:

ParameterTypeDescription
pagestringPage number (default 1).
limitstringResults per page (default 50, max 100).
orgIduuidFilter by organization.
expiredtrue or falseFilter by expiration status.

The response includes a pagination object with page, limit, and total fields.


Rotation generates new key material for an existing record while resetting usageCount to zero. Use it to extend a deployment window or reissue a compromised key without changing the key ID.

Terminal window
curl -X POST https://breeze.yourdomain.com/api/v1/enrollment-keys/KEY_UUID/rotate \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"maxUsage": 100,
"expiresAt": "2026-06-01T00:00:00Z"
}'
FieldTypeRequiredDescription
maxUsageinteger or nullNoNew limit. Pass null for unlimited. Omit to keep the current value.
expiresAtdatetimeNoNew expiration. Omit to keep the current value.

The old key value is immediately invalidated. The response includes the new key field.


Enrollment keys are hard deleted from the database. This is irreversible.

Terminal window
curl -X DELETE https://breeze.yourdomain.com/api/v1/enrollment-keys/KEY_UUID \
-H "Authorization: Bearer $TOKEN"

When an agent starts for the first time, it presents the enrollment key to the API. The full flow is:

  1. The agent sends POST /api/v1/agents/enroll with the raw enrollment key, hostname, OS type, architecture, and agent version.
  2. In production, the API first validates the static enrollment secret (from the AGENT_ENROLLMENT_SECRET env var or x-agent-enrollment-secret header).
  3. The API hashes the enrollment key with SHA-256 + pepper and looks up the matching record.
  4. The API checks that the key has not expired (expiresAt > NOW()) and has remaining usage (usageCount < maxUsage).
  5. usageCount is atomically incremented.
  6. The API verifies the key has a siteId. If not, the enrollment is rejected and the usage increment is rolled back.
  7. A device record is created (or updated if the hostname already exists in the same org + site) with a new agentId and agentTokenHash.
  8. Hardware and network inventory from the enrollment payload is stored.
  9. An mTLS certificate is issued if Cloudflare mTLS is configured for the organization.
  10. The API returns the agentId, deviceId, authToken (a brz_-prefixed bearer token), orgId, siteId, and heartbeat configuration.
// Successful enrollment response (201)
{
"agentId": "hex-agent-id",
"deviceId": "uuid",
"authToken": "brz_a1b2c3d4...",
"orgId": "uuid",
"siteId": "uuid",
"config": {
"heartbeatIntervalSeconds": 60,
"metricsCollectionIntervalSeconds": 30
},
"mtls": null
}

Enrollment keys are hashed using SHA-256 with a server-side pepper before storage. The pepper is read from environment variables in this priority order:

  1. ENROLLMENT_KEY_PEPPER
  2. APP_ENCRYPTION_KEY
  3. SECRET_ENCRYPTION_KEY
  4. JWT_SECRET

In production, at least one of these must be set or the server will refuse to start.

The hash is computed as:

SHA-256( pepper + ":" + rawKey )

This means that even if the database is compromised, the raw enrollment keys cannot be recovered without knowledge of the pepper.

The enrollment endpoint uses two layers of protection:

LayerPurposeScope
Enrollment secret (AGENT_ENROLLMENT_SECRET)Static gating token validated at the start of the request. Required in production if set.Global — same for all enrollments.
Enrollment keyPer-deployment token that determines org, site, and usage limits.Per-key — each deployment batch gets its own key.

The enrollment secret can be passed as the enrollmentSecret field in the request body or via the x-agent-enrollment-secret header.

If an agent with the same hostname already exists in the same org and site, the enrollment endpoint updates the existing device record rather than creating a duplicate. This supports scenarios like OS reinstalls or agent upgrades. However, if the existing device has been decommissioned, re-enrollment is blocked with a 403 error.

Every enrollment key operation is recorded in the audit log:

ActionTrigger
enrollment_key.createNew key created.
enrollment_key.rotateKey material regenerated. Includes previous and new maxUsage, expiresAt, and usageCount.
enrollment_key.deleteKey permanently deleted.
agent.enrollAgent successfully enrolls using a key. Logged with actorType: agent.

Enrollment key management respects the Breeze multi-tenant hierarchy:

User ScopeBehavior
OrganizationCan only manage keys for their own organization. orgId is inferred automatically.
PartnerCan manage keys for any organization they have access to. Must provide orgId when managing multiple orgs. If the partner has exactly one org, orgId is inferred.
SystemCan manage keys for any organization. Must provide orgId.

All management endpoints require organizations:read for listing/viewing and organizations:write + MFA for creating, rotating, and deleting.


VariableDefaultDescription
ENROLLMENT_KEY_DEFAULT_TTL_MINUTES60Default time-to-live for new enrollment keys when expiresAt is not specified.
ENROLLMENT_KEY_PEPPERPepper used for SHA-256 hashing of enrollment keys. Falls back to APP_ENCRYPTION_KEY, SECRET_ENCRYPTION_KEY, or JWT_SECRET.
AGENT_ENROLLMENT_SECRETStatic secret that gates the enrollment endpoint in production. If empty or unset, the gate is skipped in non-production environments.

All routes are prefixed with /api/v1/enrollment-keys and require JWT authentication.

MethodPathPermissionDescription
GET/enrollment-keysorganizations:readList enrollment keys with pagination and filters.
POST/enrollment-keysorganizations:write + MFACreate a new enrollment key. Returns the raw key once.
GET/enrollment-keys/:idorganizations:readGet metadata for a single enrollment key (raw key not included).
POST/enrollment-keys/:id/rotateorganizations:write + MFARegenerate key material, reset usage count. Returns the new raw key.
DELETE/enrollment-keys/:idorganizations:write + MFAPermanently delete an enrollment key.

The agent enrollment endpoint is separate:

MethodPathAuthDescription
POST/api/v1/agents/enrollEnrollment secret + enrollment keyRegister a new agent. No JWT required.

”Invalid or expired enrollment key” (401)

Section titled “”Invalid or expired enrollment key” (401)”

The SHA-256 hash of the provided key did not match any active record, or the key has expired or reached its usage limit. Possible causes:

  • The key was rotated and the old value is being used in the installer.
  • The expiresAt timestamp has passed. The default TTL is 60 minutes.
  • usageCount has reached maxUsage. Check the key details via GET /enrollment-keys/:id.
  • The server pepper changed since the key was created. Rotate or recreate the key.

The AGENT_ENROLLMENT_SECRET environment variable is set in production but the agent did not provide it. Pass the secret via the enrollmentSecret field in the JSON body or the x-agent-enrollment-secret HTTP header.

The provided enrollment secret does not match the configured AGENT_ENROLLMENT_SECRET. The comparison uses timing-safe equality to prevent timing attacks. Verify the secret value in your deployment configuration.

”Enrollment key must be associated with a site” (400)

Section titled “”Enrollment key must be associated with a site” (400)”

The enrollment key was created without a siteId. The enrollment endpoint requires every key to specify which site agents should be placed into. Delete this key and create a new one with a siteId.

”Device has been decommissioned” (403)

Section titled “”Device has been decommissioned” (403)”

An agent with the same hostname exists in the same org and site, but its status is decommissioned. Contact an administrator to either remove the decommissioned record or assign the agent to a different site.

An organization-scoped user attempted a key management operation without a valid orgId in their session. This usually indicates a misconfigured user account.

”No enrollment key pepper configured” (server startup)

Section titled “”No enrollment key pepper configured” (server startup)”

In production, at least one of ENROLLMENT_KEY_PEPPER, APP_ENCRYPTION_KEY, SECRET_ENCRYPTION_KEY, or JWT_SECRET must be set. Without a pepper, enrollment key hashes are vulnerable to rainbow table attacks.

Key appears valid but agent fails to connect after enrollment

Section titled “Key appears valid but agent fails to connect after enrollment”
  • Verify the authToken returned by enrollment is stored correctly in the agent config file.
  • Check that secrets.yaml permissions are 0600 (owner read/write only) and agent.yaml is 0640.
  • Ensure the agent is connecting to the correct WebSocket URL (/api/v1/agents/:id/ws) with the Authorization: Bearer brz_... header.