| Attachment | Type URL (@type) | Per-route override | Build feature |
|---|---|---|---|
HTTP filter chain (http_filters) | type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.JwtAuthentication | disabled only | None |
What it does
The JWT filter extracts a token from the request, validates its signature against a provider's JWKS keys along with the issuer, audiences and expiry, and rejects failures with 401. Which provider applies is decided by rules matched against the request path; validated claims are attached to the request for later filters (Cedar, the MCP gateway) and can be copied into headers.
Minimal configuration
From arion-runtime-jwt.yaml; the fragment is an entry in http_filters, before the router:
- name: jwt_authn
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.JwtAuthentication
providers:
idp:
issuer: https://idp.example.com
audiences: [api.example.com]
from_headers:
- name: Authorization
value_prefix: "Bearer "
remote_jwks:
http_uri:
uri: https://idp.example.com/.well-known/jwks.json
cluster: idp
timeout: 5s
cache_duration: 300s
rules:
- match: { prefix: "/" }
requires: { provider_name: idp }Fields
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
providers | map | Yes | — | Named providers; fields below. |
rules | list | No | empty | Ordered rules. Each has an optional match (prefix, path, safe_regex, path_separated_prefix, plus headers and query_parameters) and requires: {provider_name}. The first matching rule picks the provider. |
A request that resolves to no provider is rejected with
401("no provider found") — this includes requests matching no rule and rules written withoutrequires. There is noallow_missingin Arion;requires_any,requires_all,allow_missing,allow_missing_or_failedandrequirement_mapnames are unsupported and refused at startup.
Provider fields
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
issuer | string | No | — | Expected iss claim. |
audiences | list | No | empty | Accepted aud values; empty accepts any. |
remote_jwks / local_jwks | oneof | Yes | — | Key source. remote_jwks fetches http_uri (uri, cluster, timeout) and refreshes every cache_duration (default 60s), with an optional core retry_policy. local_jwks reads inline or from a file (inline_string, filename, inline_bytes, environment_variable). |
from_headers | list | No | empty | Token locations: header name plus value_prefix to strip. There is no implicit default — a provider with no token source rejects every request. |
from_params / from_cookies | list | No | empty | Query parameters or cookies to read the token from. |
forward | bool | No | false | Keep the token on the forwarded request instead of stripping it. |
claim_to_headers | list | No | empty | Copy claims to headers: {header_name, claim_name}. |
payload_in_metadata, header_in_metadata | string | No | — | Store the decoded payload or header under this dynamic-metadata key. payload_in_metadata is what makes claims visible to Cedar and MCP RBAC. |
clock_skew_seconds | uint32 | No | 0 | Tolerated clock skew for time-based claims. |
clear_route_cache | bool | No | false | Recompute the route after the filter runs. |
subjects, forward_payload_header, failed_status_in_metadata | — | No | — | Accepted at load but not enforced at runtime today. |
require_expiration, max_lifetime, jwt_cache_config and normalize_payload_in_metadata are unsupported and refused at startup.
Ordering and interactions
Run JWT after CORS (preflights carry no token) and before anything that consumes identity: RBAC on claim_to_headers output, Cedar policies over claims, per-user rate limits, and MCP tool authorization. Per route the filter supports only the generic FilterConfig override with disabled: true.