Issue & use API keys
Mint a DB-backed tenant API key over the API, scope it with roles and environments, and authenticate requests with it.
Ductor supports two authentication modes: OIDC JWTs (the primary path) and DB-backed API keys for service-to-service calls. This page owns the end-to-end task sequence: enable the auth path, mint a key, capture the secret, authenticate a request, then list and revoke the key to validate the lifecycle.
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 API-key auth first
DB-backed API keys are gated by auth.api_key_enabled. Set
DUCTOR_AUTH_API_KEY_ENABLED=true (with auth.enabled=true) so keys are
accepted. The tenant of a key always comes from the authenticated context that
created it — never from the request body.
1. Create a key
CreateApiKey requires the tenant:write scope and the admin role. It maps to
POST /api/v2/api-keys:
curl -s -X POST http://localhost:8080/api/v2/api-keys \
-H "Authorization: Bearer $ADMIN_JWT" \
-H 'Content-Type: application/json' \
-d '{
"name": "ci-deploy-key",
"roles": ["operator"],
"scopes": ["pool:write"],
"environment_mode": "production_only"
}'A key carries four things worth knowing at mint time — RBAC roles, optional per-key scopes, an optional expiry, and an environment mode:
| Field | Meaning |
|---|---|
name | Required, unique per tenant. |
roles[] | RBAC roles — validated against the catalog (viewer / operator / admin / service plus module roles). |
scopes[] | Optional per-key resource:action grants (e.g. pool:write). |
expires_at | Optional RFC-3339 timestamp; unset means the key never expires. |
environment_mode | all (default), selected, production_only, or non_production_only. |
The full field set — including environment_keys[] for selected mode — is
documented in Management · API keys.
2. Capture the secret — it's shown once
The response carries the new key record and the plaintext secret. The
secret is never stored, logged, or retrievable again — only its bcrypt hash and
prefix persist:
{
"key": {
"id": "01J...",
"name": "ci-deploy-key",
"key_prefix": "duk_9f2a",
"roles": ["operator"],
"status": "active",
"created_at": "2026-07-11T12:00:00Z",
"scopes": ["pool:write"],
"environment_mode": "production_only"
},
"secret": "duk_9f2a1b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a"
}A secret is the literal duk_ prefix followed by ~43 URL-safe characters of
CSPRNG entropy; only its first 8 characters (duk_9f2a) are stored as
key_prefix, alongside a bcrypt hash of the whole thing.
Store the secret immediately
If you lose secret, you cannot recover it — revoke the key and mint a new
one. The key_prefix (e.g. duk_9f2a) is safe to log and helps you identify a
key later without exposing it.
3. Authenticate with the key
Send the plaintext secret on requests. Ductor accepts it either as an API-key header or, where supported, as a bearer credential:
curl -s http://localhost:8080/api/pools \
-H "X-API-Key: $DUCTOR_API_KEY"The tenant, roles, and scopes are resolved from the stored key record — the key carries its own authorization context.
4. List and revoke
# list (requires tenant:read)
curl -s http://localhost:8080/api/v2/api-keys \
-H "Authorization: Bearer $ADMIN_JWT"
# revoke by id (requires tenant:write + admin)
curl -s -X POST http://localhost:8080/api/v2/api-keys/$KEY_ID/revoke \
-H "Authorization: Bearer $ADMIN_JWT"ListApiKeys returns records without secrets or hashes — each record carries
status, key_prefix, last_used_at, revoked_at, and the roles/scopes/
environment scoping. Revocation flips the key to revoked immediately.
Choosing between JWTs and API keys
| OIDC JWT | DB-backed API key | |
|---|---|---|
| Best for | Interactive users, federated identity | Service-to-service, CI, scripts |
| Config | api.oidc_issuer, api.oidc_audience | auth.api_key_enabled=true |
| Tenant source | JWT claims | Stored key record |
| Rotation | IdP-managed | Revoke + reissue |
| Header | Authorization: Bearer <jwt> | X-API-Key: <secret> |