Choose the credential by who is calling:
| Caller | Credential | Lifetime |
|---|---|---|
| An application or service with a deployed secret | Gateway key | Until it expires or is revoked |
| A workload that already has an identity (Kubernetes service account, CI job, Govern agent, client certificate) | Workload identity exchange | 60 to 3600 seconds |
| A coding tool used by a person | Gateway token | At most one hour |
| Traffic without a Govern credential, decided by your own rules | Authorizer | Per request |
Gateway keys
A gateway key is an API key that can call the gateway and nothing else: it holds only models:read. Its secret is shown once, when it is created or rotated. The console page is AI Gateway > Gateway keys.
curl -sS -X POST "$KEYSTONE_URL/api/v1/models/gateway-keys" \
-H "Authorization: Bearer $ADMIN_TOKEN" -H 'Content-Type: application/json' \
-d '{"name": "support-bot", "scope_kind": "project", "scope_id": "'$PROJECT_ID'"}'
| Setting | Meaning |
|---|---|
scope_kind, scope_id | Who the key acts for: org, team, project or user. A project key may name only that project in X-Keystone-Project. A user key acts as the user but never uses their personal provider keys. The scope never changes. |
allowed_models, allowed_routes | Narrow what the key may use. Empty allows everything its scope is assigned; a listed model still needs the assignment. Applied to the requested model, its fallbacks and every route target. |
budgets | The key's own spend or token budgets, per day or month, hard or soft, on top of its scope's. They count across rotations. |
rate_limits | The key's own request or token limits per window. |
ip_allowlist | Client networks (CIDR or addresses). Other sources get 403 key_network_forbidden. |
metadata | Chargeback metadata stamped on every request; the key's values win over X-Keystone-Metadata. |
expires_at | The key stops working then. |
Rotate and revoke
POST /api/v1/models/gateway-keys/{id}/rotate with {"overlap_seconds": 3600} issues a new secret and lets the current one keep working for the overlap (0 to 604800 seconds, default 3600). The key's id, budgets, rate limits and analytics continue. POST .../revoke revokes every credential of the key at once.
At request time a revoked or expired key answers 401, and a key whose team, project or user no longer exists answers 403 key_scope_unavailable.
Routes
| Route | Permission |
|---|---|
GET /api/v1/models/gateway-keys?kind=static (or workload) | models:view |
POST /api/v1/models/gateway-keys | models:admin |
GET /api/v1/models/gateway-keys/{id} | models:view |
PATCH /api/v1/models/gateway-keys/{id} | models:admin |
POST /api/v1/models/gateway-keys/{id}/rotate, /revoke | models:admin |
GET /api/v1/models/gateway-keys/{id}/analytics | models:view |
Every write is audited (models.gateway_key.*) without the secret.
Per-key analytics
GET /api/v1/models/gateway-keys/{id}/analytics?from=&to=&bucket=hour (or day) reports a key's requests, errors, input and output tokens and cost over time, per model and per outcome. Hour buckets cover at most 31 days, day buckets 92. Requests refused before the budget step (authentication, the key's own limits, DLP, a rate limit) are in the request log and the audit trail, not here.
Workload identity
A workload identity trust lets a workload exchange a credential it already has for a short-lived gateway key, so no static secret is deployed. The console page is AI Gateway > Workload identity.
| Kind | Accepts |
|---|---|
oidc | A JWT of one issuer (a Kubernetes service account issuer, GitHub Actions, a cloud's workload identity). iss must equal issuer and aud must name one of audiences. The signature is checked against an uploaded key set (jwks, for issuers Govern cannot reach, such as an air-gapped cluster) or one fetched from jwks_uri. Tokens that live longer than a day are refused. |
agent | A Govern agent's client credentials. Suspended, revoked or inactive agents are refused; agent_ids can limit the trust. |
mtls | A client certificate issued by ca_pem, from the TLS connection or a header a TLS-terminating proxy sets (NI_AUTH_MODELS_GATEWAY_CLIENT_CERT_HEADER; set it only when the proxy always overwrites the header). |
Rules map workloads to what they get. The first enabled rule whose every match entry matches one of the claim's values (* matches any run of characters) decides the issued key's scope_kind, scope_id, allowed_models, allowed_routes and metadata. A workload no rule matches is refused. An OIDC rule needs at least one claim condition.
| Kind | Claims rules can match |
|---|---|
oidc | The token's claims, nested by dotted path, for example kubernetes.io.namespace |
agent | agent_id, agent_name, client_id |
mtls | subject.cn, subject.o, subject.ou, san.dns, san.uri, san.email, fingerprint |
The exchange
The exchange needs no Govern credential; the identity id names the trust.
curl -sS -X POST "$KEYSTONE_URL/api/v1/models/gateway/identity/token" \
-H 'Content-Type: application/json' \
-d '{"identity_id": "'$IDENTITY_ID'",
"grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
"subject_token": "'"$(cat /var/run/secrets/tokens/gateway)"'",
"subject_token_type": "urn:ietf:params:oauth:token-type:jwt"}'
The answer holds access_token (a nia_ key), expires_in (60 to 3600 seconds, default 900), gateway_key_id, the scope and the rule that matched. The exchange also accepts an RFC 8693 form, and an agent's client_id and client_secret (or HTTP Basic).
Every refusal answers 400 invalid_grant without saying why. The reason is in the organisation's audit trail (models.gateway_identity.exchange, outcome failure), and every issued key is audited too. Issued keys are listed with kind=workload and purged a week after they expire.
| Route | Permission |
|---|---|
GET, POST /api/v1/models/gateway-identities | models:view, models:admin |
GET, PATCH, DELETE /api/v1/models/gateway-identities/{id} | models:view, models:admin (deleting deletes the keys it issued) |
Gateway tokens for coding tools
Coding tools send model requests as the person using them, with a short-lived gateway token instead of a long-lived key.
- A token starts with
nigw_, lives at most one hour (5 to 60 minutes may be asked for), and is bound to the person's session or enrolled device. - It is accepted only on the gateway's client endpoints under
/api/v1/models/gateway/v1/; anywhere else it is refused with401 gateway_token_route. - It carries the person's current permissions capped to
models:read. - Signing out of the session, retiring or reassigning the device, suspending the person or revoking the token with
POST /api/v1/auth/token/revokeends it.
| Route | Credential | Bound to |
|---|---|---|
POST /api/v1/models/gateway/tokens | A person's access token (not an API key, not a root session) | The session |
POST /api/v1/agent/gateway-token | The device token of an enrolled device assigned to a person | The device |
Both need the person to hold models:read. Each token issued, and each refused mint, is audited as models.gateway_token.issued.
People get a token from the command line:
keystone-cli token models [--tool T] [--ttl D] [--device] [--json] [--no-cache]
With KEYSTONE_TOKEN set it mints for the person; without it, or with --device, for the enrolled device. It caches the token per user until five minutes before expiry. Administrators distribute the keystone-cli binary; Govern publishes no download of it.
The console page Models > Connect a coding tool shows the configuration for each tool and mints a token for tools that take a pasted key. Per-tool steps are on the tool pages under Third-party AI coding tools.
Authorizers
Authorizers decide who may call the gateway per route, like an API gateway's authorizers. The console page is AI Gateway > Authorizers.
| Type | Admits |
|---|---|
keystone_iam | Callers Govern authenticated that hold models:read. The default when nothing is configured. |
jwt | Bearers of a JWT of jwt.issuer for one of jwt.audiences, signed by an uploaded jwt.jwks or a key set from jwt.jwks_uri; required_scopes and required_claims must match. |
custom_http | What your own https endpoint decides. Govern posts the request's identity sources and expects {"allow": true, ...}. Errors, timeouts and unreadable answers deny (503 authorizer_unavailable). |
custom_cel | A CEL expression evaluated in process over the request and its identity sources. An error or the cost limit denies. |
none | Callers without a credential. Govern refuses a none authorizer without a rate limit and a hard spend budget in its usage plan. |
Any type can also require a client certificate from a truststore (mtls.ca_pem) and HMAC-signed requests (signing, with the X-Keystone-Signature header). Traffic admitted without a Govern credential runs as the authorizer's own usage plan: its scope, models, budgets and rate limits.
PUT /api/v1/models/gateway-authorization sets the default authorizer and per-route overrides by endpoint id. On the in-server paths callers always hold a Govern credential, so the route's extra requirements (truststore, signed requests) still apply whatever the type. A route's authorizer is the only authentication on the public account edge, /api/v1/models/edge/{org}/....
| Route | Permission |
|---|---|
GET, POST /api/v1/models/gateway-authorizers | models:view, models:admin |
GET, PATCH, DELETE /api/v1/models/gateway-authorizers/{id} | models:view, models:admin |
POST /api/v1/models/gateway-authorizers/{id}/test | models:admin (the test console; nothing reaches a model) |
POST, DELETE /api/v1/models/gateway-authorizers/{id}/signing-keys | models:admin |
GET, PUT /api/v1/models/gateway-authorization | models:view, models:admin |
Every write and every test is audited (models.gateway_authorizer.*, models.gateway_authorization.put).
Provider-native passthrough
Clients pinned to a provider's own API can use POST /api/v1/models/gateway/anthropic/v1/messages and POST /api/v1/models/gateway/openai/v1/chat/completions (also /v1/completions, /v1/embeddings, /v1/responses). They run through the same pipeline, reach only that provider's connections, and answer 403 passthrough_disabled until an administrator turns them on:
PUT /api/v1/models/gateway-passthrough (models:admin)
{"enabled": true, "anthropic_betas": [...], "openai_betas": [...]}
Beta values not listed are dropped, never forwarded. Beta families that make the provider act on its own (mcp-client, code-execution, skills, files-api, web-fetch) are refused on every path.