AI Key Store Operations¶
The AI Gateway stores data-plane API keys and tenant quotas in a dedicated
PostgreSQL schema. This store is independent of the management user service:
its availability controls key/quota CRUD and evaluation on fullproxy services
that declare Gateway-owned authentication, not operator login. api_key_auth
is independent of SSE and P/D. A rule that omits it preserves a backend-owned
X-Api-Key; disabled strips that header; required validates it.
Development-source behavior
The independent PostgreSQL store and peer invalidation described here are implemented in the current development source but have not completed release qualification. Confirm the options, schema, and failure behavior against the exact image you deploy.
Architecture and boundary¶
flowchart LR
APP([Inference workload]) -->|X-Api-Key| VIP[Inference VIP]
VIP --> CACHE["In-memory key cache<br/>five-minute TTL"]
CACHE -->|miss| DPDB["PostgreSQL<br/>aigw schema"]
OP([Administrator]) -->|Bearer token| API["Management API<br/>:11111"]
API -->|key and quota CRUD| DPDB
API -->|login and users| MGMT["Management user store<br/>separate options and runtime"]
DPDB -. revoke / patch / delete .-> PEER["Best-effort peer<br/>cache invalidation"]
style DPDB fill:#e8f5e9,stroke:#43a047
style MGMT fill:#e1f5fe,stroke:#0288d1
style CACHE fill:#fff9c4,stroke:#f9a825
The Gateway qualifies every data-plane table with schema aigw and uses its
own PostgreSQL role and connection pool. The runtime creates these tables after
verifying that the schema already exists and the role has USAGE and CREATE:
aigw.api_keys;aigw.tenant_rate_limits;aigw.tenant_model_rate_limits;aigw.user_rate_limitsandaigw.user_model_rate_limits;aigw.rate_limit_defaults.
Only a SHA-256 hash of each API key is stored. The opaque key_id is generated
independently from the secret, so the identifier does not disclose key
material.
Plane separation is not management authentication
A configured key store does not secure port 11111. If no user, OAuth, or manual-token
management mode is enabled, the current authorizer permits key and quota CRUD without a
credential. Configure and verify management authentication separately.
Provision the schema and role¶
The repository provides scripts/aigw-db-bootstrap.sql. Run it as the database
owner; the Gateway's restricted role must not be able to create its own role or
schema.
Supply passwords through the environment or secret injection, not literal SQL or command-line flags:
export AIGW_DB_PASSWORD="$DATA_PLANE_STORE_PASSWORD"
export AIGW_MGMT_DB_PASSWORD="$RESERVED_MANAGEMENT_STORE_PASSWORD"
psql -v ON_ERROR_STOP=1 \
--username "$POSTGRES_OWNER" \
--dbname "$POSTGRES_DATABASE" \
--file scripts/aigw-db-bootstrap.sql
unset AIGW_DB_PASSWORD AIGW_MGMT_DB_PASSWORD
The script is idempotent and rotates the two login-role passwords when re-run.
It creates both aigw and aigw_mgmt schemas and removes direct cross-schema
grants.
Current management-store boundary
The current Gateway user-service runtime still uses the separate --database* management
store. Although the bootstrap script reserves aigw_mgmt, do not claim or assume that operator
users and tokens have migrated to PostgreSQL until the deployed runtime options and code show
that wiring.
Verify ownership and least privilege with deployment-specific role names. The
data-plane role must be able to create in aigw, must not be able to use
aigw_mgmt, and should not be able to create objects in public. Table access
inside another schema must also be tested; schema USAGE alone does not grant
table SELECT. PostgreSQL versions that grant CREATE on public through the
PUBLIC pseudo-role require a database-level hardening decision: revoking a
grant from aigwuser alone does not remove an inherited PUBLIC grant.
Configure the Gateway¶
| Option | Required | Default | Purpose |
|---|---|---|---|
--aikey-db-host |
Yes | none | Enables construction of the AI-key service and names the PostgreSQL host |
--aikey-db-port |
No | 5432 |
PostgreSQL port |
--aikey-db-user |
Yes | none | Restricted role that owns or can use aigw |
--aikey-db-name |
Yes | none | Database containing the aigw schema |
--aikey-db-password-file |
Recommended | none | File containing the store password |
AIGW_DB_PASSWORD |
Alternative | none | Password source only when no password file is named |
--aikey-db-ssl |
Production | off | Require verified TLS to PostgreSQL |
--aikey-db-ssl-ca-cert-file |
With TLS | none | CA that issued the PostgreSQL server certificate |
--aikey-db-ssl-client-cert-file |
With TLS | none | Client certificate |
--aikey-db-ssl-client-key-file |
With TLS | none | Client private key |
Mount the password as a restricted secret file:
--aikey-db-host=postgres.example.internal
--aikey-db-port=5432
--aikey-db-user=aigwuser
--aikey-db-name=gateway
--aikey-db-password-file=/run/secrets/aigw-db-password
--aikey-db-ssl
--aikey-db-ssl-ca-cert-file=/run/secrets/postgres-ca.crt
--aikey-db-ssl-client-cert-file=/run/secrets/postgres-client.crt
--aikey-db-ssl-client-key-file=/run/secrets/postgres-client.key
When a password file is named, it takes precedence over the environment. An
unreadable or empty file is an error; the Gateway does not silently fall back
to AIGW_DB_PASSWORD. This makes a broken secret mount visible instead of
turning it into an ambiguous database-authentication failure.
With --aikey-db-ssl, the current client uses verified TLS with hostname
verification, a client certificate, TLS 1.2 or newer, and no plaintext
fallback. The server certificate SAN must match --aikey-db-host.
Upgrade from the former shared store¶
The development source no longer reads API keys or tenant quotas from the management user-service database and no longer creates those tables there. There is no automatic data migration.
Do not upgrade with only --userservice
An upgraded Gateway with --userservice but no --aikey-db-host has management login but no
AI-key store. Key/quota CRUD returns 503; a service declaring api_key_auth: required
also returns 503 policy_store_unavailable before backend dispatch. A service that omitted
the declaration remains keyless by contract. Treat either unintended state as an
exposure-blocking configuration error.
Choose one migration strategy:
- Reissue keys (recommended): provision PostgreSQL, create new scoped keys, distribute them through the secret manager, switch workloads, and delete the old records after verification.
- Controlled row migration: move
api_keys,tenant_rate_limits, andtenant_model_rate_limitsinto schemaaigw. Preservekey_hashif existing raw credentials must continue to work, normalize timestamps to explicit UTC, convert enabled values to PostgreSQL booleans, replace nullable numeric values with intentional values, and resolve duplicate hashes before import because the new store enforces uniqueness.
Take an encrypted backup, rehearse the transformation outside production, and compare row counts and tenant/model summaries without printing hashes or raw keys. Legacy public identifiers may have different security properties from new independently generated key IDs; reissue when that distinction matters.
Startup and outage behavior¶
| State | Key/quota management API | Inference key validation |
|---|---|---|
--aikey-db-host unset; api_key_auth: required |
503 ai_key_store_unconfigured |
503 policy_store_unavailable; backend receipt delta 0 |
Store configured and healthy; required fullproxy rule |
CRUD succeeds | Cache hit or PostgreSQL lookup; unknown key 401, disallowed model 403 |
Store healthy; api_key_auth omitted |
CRUD succeeds | Gateway does not validate or strip backend-owned X-Api-Key |
Store healthy; api_key_auth: disabled |
CRUD succeeds | No validation; Gateway strips X-Api-Key |
| Store configured but unavailable before an answer is cached | 503 ai_key_store_unavailable |
503 policy_store_unavailable; do not misclassify as an invalid key |
| Store becomes unreachable after a confirmed answer was cached | Store-backed CRUD fails; an unclassified driver error may surface as a generic service error | Cached/last-known-good entries remain usable under their cache contract; unknown evaluation fails closed |
| Store reconnects | CRUD resumes after the reconnect path attaches the pool | New misses can validate again |
Declaration determines the no-store posture
No store does not silently downgrade a required rule: it fails closed with 503. An omitted
or disabled rule intentionally has no API-key validation dependency. Verify the exact
declaration by read-back instead of inferring it from store health, SSE, or P/D flags.
Per-key tokens_per_min is actively consumed by the implementation and its
unit/integration tests. The frozen primary Swagger text is stale and calls it
stored-only metadata, while the companion PATCH description says it is
enforced. Do not present this implemented behavior as a released support
guarantee until the primary contract is corrected and qualified. The complete
ladder is documented in AI Quotas and QoS.
The management handlers expose unconfigured and unavailable as separate 503
error codes because they require different operator actions. The inference
path deliberately returns the same 401 invalid_api_key for unknown,
disabled, expired, malformed, or store-miss failures so it does not reveal key
existence.
Cache and revocation behavior¶
Key and quota entries have a five-minute in-memory TTL. Cache hits avoid a database call. A disable, allow-list change, or delete performs these actions:
- Resolve the stored key hash by
key_id. - Apply the database mutation.
- Evict local cache entries by both key hash and key ID.
- Send a bounded, concurrent invalidation request to configured peers.
Peer invalidation is best-effort. The local mutation does not fail merely because a peer is unreachable. A peer that does not receive the notice may continue honoring its cached copy until TTL expiry. Protect the peer synchronization network using deployment-supported controls and do not use the best-effort fan-out as proof of instantaneous cluster-wide revocation.
Production validation gates¶
Run these checks in a non-production tenant before exposure:
- With management authentication enabled, omit the bearer credential from an
invalid-body key creation. Authentication must run before body validation,
returning
401:
# docs-example: expect-schema-error
curl --silent --output /dev/null --write-out '%{http_code}\n' \
--request POST https://gateway.example.com/netlox/v1/config/ai/apikey \
--header 'Content-Type: application/json' \
--data '{}'
- Authenticate as an administrator and list the test tenant. The response
must be
200and must not containraw_keyorkey_hash. - Create a narrowly scoped test key and move the returned secret immediately into a secret manager.
- Create or select a
mode: 4test rule withapi_key_auth: requiredand read the declaration back exactly. - On that rule, send an inference request without or with an unknown
X-Api-Key; require401 invalid_api_keyand confirm the unique backend receipt counter does not change. - Disable the test key with
PATCH; require subsequent use on the same gated rule to return401. - In a multi-node lab, warm the key on that gated rule through every peer before disabling it, then verify every peer denies it. Record any TTL-bounded convergence separately.
- Delete the test key and remove all temporary header and response files.
- In a separate outage probe, make the policy unevaluable; require
503 policy_store_unavailableand another backend receipt delta of0.
Troubleshooting¶
| Symptom | Check | Corrective action |
|---|---|---|
ai_key_store_unconfigured |
Is --aikey-db-host present in the running process configuration? |
Add the complete store configuration and restart safely |
ai_key_store_unavailable |
Password source, network reachability, schema preflight, and TLS files | Repair the dependency; wait for reconnect; do not bypass enforcement |
| Preflight says schema does not exist | Bootstrap ran against a different database or only as an init hook on an existing volume | Run the shipped bootstrap explicitly against the selected database |
| Preflight says schema is inaccessible | Gateway role lacks USAGE or CREATE on aigw |
Correct grants as the database owner; do not grant broad public access |
| TLS hostname verification fails | Certificate SAN does not cover --aikey-db-host |
Reissue the certificate or use its verified DNS name |
| A revoked key works on one peer | Peer invalidation did not arrive or the peer is older | Isolate that peer and wait at least the cache TTL; validate peer compatibility |
Key list shows enabled: false |
Key is soft-disabled, not deleted | Re-enable deliberately with PATCH or permanently delete after review |