Skip to content

feat: authenticate callers with short-lived signed tokens - #41

Merged
github-actions[bot] merged 2 commits into
mainfrom
feat/28-short-lived-credentials
Oct 4, 2026
Merged

github-actions[bot] merged 2 commits into
mainfrom
feat/28-short-lived-credentials

Conversation

@Yash-Chindam

Copy link
Copy Markdown
Owner

What

Closes the first line of the spec's security design (§14): "Authenticate users and services with short-lived credentials." Until now the gateway accepted only static API keys.

How

  • llm_router.credentials.TokenVerifier accepts a bearer token only when it is:
    • signed with an asymmetric algorithm by a key the issuer publishes (unsigned and shared-secret tokens are refused);
    • issued by the configured issuer for the configured audience, and not expired;
    • carrying iat, exp and sub, with a lifetime no longer than ROUTER_JWT_MAX_LIFETIME_SECONDS;
    • naming a tenant. There is no default tenant for a token.
  • Keys come inline as a JWKS document or from the issuer's endpoint. Fetched keys are cached, refetched when a token names an unknown key (rate-limited), and kept when the issuer is unreachable.
  • A refused token gets 401 with the reason and is never retried as a static key.
  • The caller is identified by issuer and subject, so a renewed token is the same caller.
  • Static keys keep working for development until ROUTER_REQUIRE_SHORT_LIVED_CREDENTIALS=true.
  • The cluster manifests and the Helm chart now require tokens: ROUTER_API_KEYS is no longer mounted, and the issuer's keys come from the secret manager. auth.issuer and auth.audience are chart values.

Tests

tests/integration/test_credentials_api.py (30 tests), using real RSA and EC keys generated in the test: a valid token; twelve kinds of token that must be refused (expired, too long-lived, wrong key, unknown key, wrong audience, wrong issuer, no tenant, no subject, HS256, alg: none, malformed, no iat); key rotation without a restart; issuer outage; the gateway end to end, including tenant entitlement from the token, static-key refusal when tokens are required, and quota shared across a renewed token.

Local: ruff and mypy clean, full suite passes, Playwright 9/9, rebuilt image scans clean with Trivy.

Limits

  • Tested with locally generated keys, not against a real identity provider.
  • The issuer URL in the manifests is a placeholder.
  • Inline keys are read at start-up, so rotating them through the secret manager needs a pod restart; the endpoint option does not.
  • No token revocation list: a token is trusted until it expires, which is why the lifetime cap exists.
  • Service-to-service credentials inside the cluster (gateway to engine, Redis, MLflow) are still network-policy-scoped rather than token-authenticated.

🤖 Generated with Claude Code

Yash-Chindam and others added 2 commits October 4, 2026 12:02
Verify bearer tokens against the identity provider's published keys,
refuse any token that outlives the configured lifetime, and take the tenant
from the token. Static keys remain for development; the cluster manifests
and the Helm chart now require tokens and mount no static key.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added documentation Improvements or additions to documentation area/api area/tests dependencies labels Oct 4, 2026
@github-actions
github-actions Bot merged commit d269127 into main Oct 4, 2026
6 checks passed
@github-actions
github-actions Bot deleted the feat/28-short-lived-credentials branch October 4, 2026 06:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/api area/tests dependencies documentation Improvements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant