Skip to content

API Key Management

Use the AI Gateway key API to create, inspect, rotate, and revoke workload credentials. On a mode: 4 rule with sse_mode: true or pd_disagg_mode: true, the request path actively enforces key validity, model allow-lists, per-key request rate, and tenant request rate. Plain fullproxy rules do not enter this gate.

Request path

flowchart LR
    C([Client]) --> K{"X-Api-Key<br/>valid?"}
    K -->|no| R401([401 invalid_api_key])
    K -->|yes| M{"Requested model<br/>allowed?"}
    M -->|no| R403([403 model_not_allowed])
    M -->|yes| L{"Key and tenant<br/>rate buckets allow?"}
    L -->|no| R429([429 rate limit])
    L -->|yes| B["Inference backend"]

    style B fill:#e8f5e9,stroke:#43a047
    style R401 fill:#ffcdd2,stroke:#e53935
    style R403 fill:#ffcdd2,stroke:#e53935
    style R429 fill:#fff9c4,stroke:#f9a825

API-key configuration uses an independent PostgreSQL data-plane store. It does not depend on the management user service. Management operations use a control-plane bearer identity when management authentication is configured; inference requests use X-Api-Key and never use the management bearer token.

Development-source behavior

The independent key store and authentication-plane separation described on this page are implemented in the current development source but have not completed release qualification. Confirm the flags and API schema exposed by the exact image you deploy.

Key checks require both a store and an AI-processing rule

The current data-plane key gate runs only for mode: 4 rules that also set sse_mode: true or pd_disagg_mode: true. A plain mode: 4 rule remains keyless even when --aikey-db-host is configured. If the store is unset, the gated paths also admit requests without validating X-Api-Key. Configure both prerequisites, then prove a missing-key request returns 401 on every protected VIP before exposure.

A key store does not protect the management listener

The key lifecycle routes are registered independently of --userservice. If none of the management authentication modes (--userservice, --oauth2, or --manualtoken) is enabled, the current authorizer grants management requests unrestricted access. Before exposing port 11111, enable one management authentication mode and verify an unauthenticated key-creation request returns 401.

Security rules

  • Use TLS for both control-plane and inference traffic outside an isolated lab.
  • Create one key per workload and environment, with the smallest model list and rate limits it needs.
  • The create response is the only place that returns the raw key. List and get return metadata only.
  • Never put a raw key in a URL, log, metric label, Git repository, screenshot, or support ticket.
  • Prefer protected header files or a secret-aware client over credentials in command arguments.

Prepare a management header file:

export CONTROL_API="https://gateway.example.com/netlox/v1"
install -m 600 /dev/null ./control-plane.headers
printf 'Authorization: Bearer %s\n' "$CONTROL_PLANE_TOKEN" > ./control-plane.headers

Endpoints

Method Path Purpose
POST /config/ai/apikey Create a key; raw value is returned once
GET /config/ai/apikey?tenant_id=... List key summaries, optionally by tenant
GET /config/ai/apikey/{key_id} Read one key summary
PATCH /config/ai/apikey/{key_id} Replace allowed_models and/or change enabled; raw-middleware route
DELETE /config/ai/apikey/{key_id} Permanently delete a key
POST /config/ai/tenant/ratelimit Set tenant RPS and token quotas
GET /config/ai/tenant/ratelimit/{tenant_id} Read tenant limits

Create a key

Only tenant_id is required, but a production key should be explicitly scoped:

curl --fail-with-body --silent --show-error \
  --request POST "$CONTROL_API/config/ai/apikey" \
  --header @control-plane.headers \
  --header 'Content-Type: application/json' \
  --data '{
    "tenant_id": "team-a",
    "name": "chat-service",
    "allowed_models": ["example-chat-model"],
    "rate_limit_rps": 5,
    "burst_size": 10,
    "tokens_per_min": 0,
    "enabled": true
  }' > ./new-key.json

jq '{key_id, raw_key_present: (.raw_key | type == "string")}' ./new-key.json

Expected result: 201 Created, with key_id and raw_key. Move the raw value directly into your secret manager, then securely remove the temporary file.

Field Type Meaning
tenant_id String Owning tenant; required
name String Non-secret operator label
api_key String Optional caller-supplied key for a controlled credential import; write-only
allowed_models String array Exact model identifiers this key may use
rate_limit_rps Integer Per-key requests per second; 0 disables this limit
burst_size Integer Per-key request bucket capacity
tokens_per_min Integer Persisted and returned by the key API, but not enforced by the current data path; use tenant/model TPM
expires_at RFC 3339 timestamp Optional key expiry
enabled Boolean Defaults to enabled when omitted

An empty model list records no model restriction. Prefer an explicit list when the workload should use only known models.

Per-key tokens_per_min is currently a round-trip schema field, not an enforcement control. Configure aggregate and per-model token budgets through the tenant rate-limit API and verify their 429 behavior independently.

The normal path omits api_key, lets the Gateway generate the credential, and receives it once as raw_key. The development contract also accepts a caller-supplied key between 16 and 512 printable, non-space ASCII characters. That path stores only its SHA-256 hash and never echoes the supplied value.

Imported-key response boundary

The current handler returns an empty raw_key value for an imported key, while the development Swagger description says the field is omitted and its response schema still marks raw_key as required. Do not automate against empty-versus-absent behavior until the release contract resolves this mismatch. Generated-key responses are unaffected.

List and inspect without exposing the secret

curl --fail-with-body --silent --show-error \
  --get "$CONTROL_API/config/ai/apikey" \
  --header @control-plane.headers \
  --data-urlencode 'tenant_id=team-a' \
  | jq 'map({key_id, name, allowed_models, enabled, expires_at})'

curl --fail-with-body --silent --show-error \
  --header @control-plane.headers \
  "$CONTROL_API/config/ai/apikey/$KEY_ID" \
  | jq '{key_id, tenant_id, name, allowed_models, enabled, expires_at}'

Verify that neither response contains raw_key nor key_hash. Avoid listing all tenants unless your role and operational need require it.

Disable, rotate, and delete

Disable is reversible and is useful for a controlled cutover:

curl --fail-with-body --silent --show-error \
  --request PATCH \
  --header @control-plane.headers \
  --header 'Content-Type: application/json' \
  --data '{"enabled":false}' \
  "$CONTROL_API/config/ai/apikey/$OLD_KEY_ID"

Expected result: 204 No Content. The route is described in the companion swagger-extras.yml contract because it is dispatched before the generated OpenAPI handler chain.

  1. Create a replacement with the same or narrower permissions.
  2. Store it in the workload's secret manager.
  3. Roll the workload to the replacement.
  4. Verify authorized requests succeed and old-key traffic has stopped.
  5. Permanently delete the old key:
curl --fail-with-body --silent --show-error \
  --request DELETE \
  --header @control-plane.headers \
  "$CONTROL_API/config/ai/apikey/$OLD_KEY_ID"

Expected result: 204 No Content; subsequent get returns not found. On the current development data path, prove that inference use fails on the same mode: 4 rule with sse_mode: true or pd_disagg_mode: true; a plain fullproxy rule does not enter the key gate. A delete is permanent—create a new key if access is needed again.

Disable, allow-list changes, and delete evict the local authentication and key-summary caches before the operation returns. The development HA path also sends a best-effort invalidation to peers. An unreachable or older peer may continue using a cached entry until its five-minute cache TTL expires, so do not describe peer invalidation as an instantaneous cluster-wide revocation guarantee.

Verify data-plane enforcement

Write the inference header without printing its value:

install -m 600 /dev/null ./inference.headers
printf 'X-Api-Key: %s\n' "$INFERENCE_API_KEY" > ./inference.headers
Test Expected response
Valid key and allowed model Backend response
No key or unknown key 401 invalid_api_key
Disabled, expired, or revoked key 401
Valid key, disallowed model 403 model_not_allowed
Burst over key or tenant request bucket 429

Run destructive or throttling probes only with a dedicated non-production tenant. Never print the test key in the result.

Tenant limits

Tenant RPS, aggregate TPM, per-model TPM, and token burst capacity use the tenant rate-limit API. These limits are shared by the tenant's keys and are explained step by step in AI Traffic Governance.

Troubleshooting

Symptom Likely cause Action
Create returns 400 Missing tenant or invalid body Validate JSON and provide a non-empty tenant_id
Management call returns 401 Management credential missing or expired Refresh through the approved identity workflow
Management call returns 403 Authenticated viewer or unknown role attempted a mutation Use an explicitly authorized administrator; do not widen the viewer role
Key or quota call returns 503 ai_key_store_unconfigured --aikey-db-host is unset Stop exposure, configure the independent key store, and re-run missing-key probes
Key or quota call returns 503 ai_key_store_unavailable A configured store did not initialize or is unreachable Restore the store and verify the reconnect; do not bypass the key check
Inference call returns 401 Key missing, unknown, disabled, expired, or revoked Inspect summary by key_id; do not log the raw key
Inference call returns 403 Effective model is not allowed Compare the exact model with allowed_models
Inference call returns 429 Key/tenant RPS or token quota Inspect the error reason, Retry-After, and metrics
Key cannot be recovered Raw value was not stored Revoke the record and rotate to a new key
CRUD fails despite valid management auth Independent PostgreSQL key store unavailable Restore the data-plane store securely; management-user health does not prove key-store health

Cleanup

rm -f ./new-key.json ./control-plane.headers ./inference.headers
unset CONTROL_PLANE_TOKEN INFERENCE_API_KEY KEY_ID OLD_KEY_ID