Guides

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:

FieldMeaning
nameRequired, 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_atOptional RFC-3339 timestamp; unset means the key never expires.
environment_modeall (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 JWTDB-backed API key
Best forInteractive users, federated identityService-to-service, CI, scripts
Configapi.oidc_issuer, api.oidc_audienceauth.api_key_enabled=true
Tenant sourceJWT claimsStored key record
RotationIdP-managedRevoke + reissue
HeaderAuthorization: Bearer <jwt>X-API-Key: <secret>

Next steps