API Keys
Mint, list, and revoke DB-backed tenant API keys through the v2 API — roles, scopes, environment scope, expiry, storage, and rotation.
DB-backed API keys are Ductor's durable, revocable, per-tenant credentials for
service-to-service traffic. This page owns the security model: how a presented
key is verified, what is stored, how roles and scopes are evaluated, and what
threat boundaries the auth path enforces. Unlike JWTs (validated against your
IdP) these are owned by Ductor and validated against the tenant_api_key table.
Choose the right API-key page
Auth · API keys owns the verification model, storage/hash/prefix scheme, roles and scopes, and threat model. Management · API keys owns mint, list, rotate, revoke, and the operational lifecycle API. Issue & use API keys owns the end-to-end task sequence and validation path.
Enable the auth path first:
export DUCTOR_AUTH_API_KEY_ENABLED=trueThe secret is shown exactly once
CreateApiKey returns the plaintext key once, in the create response. It is
never stored, logged, or retrievable afterward — only the key's prefix and a
bcrypt hash are persisted. If you lose it, revoke and mint a new one.
The lifecycle API
Three REST endpoints (each also available over Connect-RPC). The tenant is always taken from the authenticated request context, never from the request body — you cannot mint or list keys for another tenant.
| Operation | Method & path | Required authorization |
|---|---|---|
| List keys | GET /api/v2/api-keys | tenant:read |
| Create key | POST /api/v2/api-keys | tenant:write and the admin role |
| Revoke key | POST /api/v2/api-keys/{id}/revoke | tenant:write and the admin role |
Mint a key
curl -sS -X POST https://api.example.com/api/v2/api-keys \
-H "Authorization: Bearer $ADMIN_JWT" \
-H "Content-Type: application/json" \
-d '{
"name": "CI Pipeline Key",
"roles": ["operator"],
"scopes": ["workflow:read", "workflow_run:read"],
"environment_mode": "non_production_only",
"expires_at": "2026-12-31T00:00:00Z"
}'Response — note secret, returned here and never again:
{
"key": {
"id": "b1e7…-uuid",
"name": "CI Pipeline Key",
"key_prefix": "duk_b1e7",
"roles": ["operator"],
"scopes": ["workflow:read", "workflow_run:read"],
"status": "active",
"environment_mode": "non_production_only",
"environment_keys": [],
"created_at": "2026-07-11T12:00:00Z"
},
"secret": "duk_b1e7_live_9f2c8a7b6d5e4f3a2b1c0d9e8f7a6b5c"
}Use the returned secret on subsequent calls:
curl -sS https://api.example.com/api/v2/workflow-runs \
-H "X-API-Key: duk_b1e7_live_9f2c8a7b6d5e4f3a2b1c0d9e8f7a6b5c"List keys
Returns every key for the tenant — active and revoked, newest first — so a management UI can show full history. Records never include the secret or its hash.
curl -sS https://api.example.com/api/v2/api-keys \
-H "Authorization: Bearer $ADMIN_JWT"{
"keys": [
{
"id": "b1e7…-uuid",
"name": "CI Pipeline Key",
"key_prefix": "duk_b1e7",
"roles": ["operator"],
"status": "active",
"created_at": "2026-07-11T12:00:00Z",
"last_used_at": "2026-07-11T12:03:41Z"
}
],
"total_count": 1
}Revoke a key
curl -sS -X POST https://api.example.com/api/v2/api-keys/b1e7…-uuid/revoke \
-H "Authorization: Bearer $ADMIN_JWT"Revocation takes effect immediately — the auth path only ever considers
active keys. Revoking an already-revoked key is a no-op. The action cannot be
undone.
What a key carries
Roles
The optional roles list is validated against the RBAC role catalog: the built-in
viewer, operator, admin, and service, plus any module-registered roles. An
unknown role name is a 400. At auth time the role bundle expands to its scopes.
See Authorization for what each role grants.
Scopes
The optional scopes list attaches per-key resource:action scopes. Each must be
a known scope in the catalog (an unknown scope is rejected at mint time). Per-key
scopes are unioned with the role-bundle expansion when the authorizer
evaluates a request — so you can grant a key exactly workflow:read without
handing it a whole role.
Environment scope
environment_mode constrains where a key may mutate environment-owned
artifacts:
| Mode | Meaning |
|---|---|
all (default) | No environment restriction |
selected | Only the environments listed in environment_keys |
production_only | Only production environments |
non_production_only | Everything except production |
environment_keys is only valid with selected (and must be non-empty there);
supplying keys without selected — or with any other mode — is rejected. If you
pass environment_keys but omit environment_mode, the server treats the request
as selected. Read-only calls are still governed by RBAC scopes regardless of
environment mode.
Expiry
expires_at is an optional RFC-3339 timestamp. Unset means the key never expires.
The authentication path rejects a key presented after its expiry.
Storage & validation model
- Never plaintext. Only two things are persisted: the key prefix (first 8
characters, e.g.
duk_b1e7, shown for identification) and a bcrypt hash of the full key. The scheme the mint side writes is exactly what the auth side validates against. - Prefix-narrowed lookup. On a presented key, the store fetches active candidates by prefix — at most one or two rows — and bcrypt-compares against those. This keeps validation cheap and bounds the work an attacker can force.
- Role storage. The per-key role bundle was added in migration 142; an empty role list simply disables the role gate while per-key scopes still apply.
Rotation
Keys have no in-place rotation — rotation is mint-then-revoke, which lets you cut over with zero downtime:
Mint a new key with the same roles/scopes/environment scope.
Deploy it to the consumer and confirm traffic is flowing on the new
key_prefix (watch last_used_at).
Revoke the old key. It stops authenticating immediately.
Set an expiry as a safety net
Set expires_at on machine keys so a forgotten key eventually self-retires even
if step 3 is missed.
Auditing key lifecycle
Because API keys are bearer credentials, their issuance, status changes, and
revocation are recorded to the same audit stream the authorizer uses
(tenant.apikey.created, tenant.apikey.status_changed, tenant.apikey.revoked).
The audit event carries the actor, tenant, and key ID — never the plaintext or
the key name. Configure the audit sink as described in
Authorization → Audit logging.
Browser-Approved CLI & MCP Login
OAuth device authorization for short-lived, tenant- and environment-bound CLI/MCP credentials without pasted API keys.
SAML 2.0 SSO
Ductor's receive-only SAML Service Provider — the per-tenant /saml router, replay-before-mint ACS pipeline, attribute-to-role derivation, and its oracle-defense posture.