About
This extension implements OpenFGA authorization checks as an inline Envoy HTTP filter. It calls the OpenFGA Check API via Envoy's async HTTP callout mechanism to determine whether a request should be allowed or denied, without requiring a separate authorization sidecar.
Works with Envoy, Envoy Gateway, and Envoy AI Gateway (AIGatewayRoute, HTTPRoute, MCPRoute). The extension extracts user, relation, and object from request headers, URL path segments, query parameters, or static values, then delegates the authorization decision to OpenFGA.
Key Features
- Async HTTP callout: Uses Envoy's built-in
HttpCalloutto call the OpenFGA Check API through a configured cluster, integrating with Envoy's connection pooling and circuit breaking. - Flexible input mapping: Extract
user,relation, andobjectfrom request headers, URL path segments, query parameters, or static values — no ExtProc needed for REST APIs. - AI Gateway ready: Works in the same Envoy filter chain as the AI Gateway External Processor. Use it to control access to AI models, MCP tools, or any HTTP resource.
- Dry-run mode: Log authorization decisions without enforcing them.
- Fail-open mode: Allow requests through when OpenFGA or the filter cannot enforce a deny (see Metrics).
OpenFGA best practices
Follow OpenFGA’s production guidance: enable TLS and API authentication on the OpenFGA server, disable the playground in production, and tune metrics and tracing on the server. Configure the Envoy cluster to OpenFGA with TLS and any required upstream headers (for example API tokens) so callouts are protected in transit and authenticated like any other client.
In production, set authorization_model_id to a fixed model ID instead of omitting it
(omitted uses the store’s latest model, which can change unexpectedly when models are updated).
OpenFGA’s query consistency modes apply when
Check query caching is enabled on the server. The default omits consistency from the Check
request (equivalent to minimizing latency when caching matters). Use the optional consistency
config field to send HIGHER_CONSISTENCY when you need to skip the cache after tuple writes;
avoid setting it on every request because it increases latency and load.
user, relation, and object must be valid for your authorization model (typically
OpenFGA strings such as type:id). This extension builds them from static values or request data
via prefix and extracted values—misconfiguration can produce strings OpenFGA will reject or
evaluate incorrectly.
fail_open and dry_run trade security for availability or observability: only enable
them deliberately.
This filter supports contextual tuples and context (ABAC conditions) in the Check request.
Use contextual_tuples to pass request-scoped relationships (e.g., organization membership from
a JWT claim) without storing them in OpenFGA; see Contextual tuples.
Use context to pass runtime values for condition evaluation
(e.g., current time, IP address) in ABAC-style policies.
Configuration
The extension requires an Envoy cluster pointing to the OpenFGA server. The user, relation,
and object fields for the Check API are built from static values, request headers, URL path
segments, or query parameters (see valueSource below).
{
"cluster": "openfga",
"openfga_host": "openfga.default.svc:8080",
"store_id": "01ABCDEF",
"user": {"header": "x-user-id", "prefix": "user:"},
"relation": {"value": "can_use"},
"object": {"header": "x-ai-model", "prefix": "model:"}
}Multi-Rule Configuration
For routes that handle multiple resource types (e.g., AI models, MCP tools, and generic HTTP
resources), use the rules array instead of the legacy single-rule config. Each rule can match
on request headers and define its own relation and object.
rules: Array of rules evaluated in order. The first matching rule is used. A rule with nomatchor emptymatch.headersis a catch-all and must be last.match.headers: Object mapping header names to match values. Use"*"to require the header to be present with any non-empty value; use an exact string for a value match.- Top-level
user: Serves as the default user source for all rules. Individual rules can override with their ownuserfield.
Example: AI model rule (header x-ai-eg-model), MCP tool rule (header x-mcp-tool), and a
catch-all for generic resources.
{
"cluster": "openfga",
"openfga_host": "openfga.default.svc:8080",
"store_id": "01ABCDEF",
"user": {"header": "x-user-id", "prefix": "user:"},
"rules": [
{"match": {"headers": {"x-ai-eg-model": "*"}}, "relation": {"value": "can_use"}, "object": {"header": "x-ai-eg-model", "prefix": "model:"}},
{"match": {"headers": {"x-mcp-tool": "*"}}, "relation": {"value": "can_invoke"}, "object": {"header": "x-mcp-tool", "prefix": "tool:"}},
{"relation": {"value": "can_access"}, "object": {"header": "x-resource-id", "prefix": "resource:"}}
]
}Filter Chain Ordering
When deploying with Envoy AI Gateway, filter chain order determines which headers are available when the openfga filter runs.
Recommended order:
- jwe-decrypt (optional) — decrypts a JWE-wrapped token so JWT authn can read it
- JWT Authentication (Envoy built-in) — validates the token and optionally injects
claims as request headers via
SecurityPolicy.jwt.claimsToHeaders - openfga — reads user identity from a header, checks authorization against OpenFGA
- AI Gateway ExtProc — parses the request body to route to the correct AI model
Why openfga before the AI Gateway ExtProc?
OpenFGA is a header-only filter: it runs on OnRequestHeaders before the request body is
read. The AI Gateway ExtProc processes the request body and may set headers like
x-ai-eg-model based on the JSON payload. When openfga runs first, it reads
x-ai-eg-model as set by the client — which is the intended control point for
access control (the client declares what it wants, and the check runs before any
processing). If the model name must come from the body-parsed value injected by ExtProc,
place openfga after the AI Gateway ExtProc in the filter chain instead.
Identity Integration
Envoy Gateway JWT claims injection (recommended)
The simplest way to pass user identity to openfga is via Envoy Gateway's SecurityPolicy
with claimsToHeaders. Envoy validates the JWT and injects any claim (e.g. sub) into a
request header before the filter chain runs — no extra plugins needed. The header name is
fully configurable; examples here use x-user-id for consistency with the rest of the
Composer extension ecosystem.
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: SecurityPolicy
metadata:
name: jwt-policy
spec:
targetRef:
group: gateway.networking.k8s.io
kind: HTTPRoute
name: my-route
jwt:
providers:
- name: my-provider
issuer: https://auth.example.com
remoteJWKS:
uri: https://auth.example.com/.well-known/jwks.json
claimsToHeaders:
- claim: sub
header: x-user-idThen configure openfga to read that header:
{"header": "x-user-id", "prefix": "user:"}jwe-decrypt + JWT authn (for encrypted tokens)
For deployments where clients send JWE-encrypted JWTs, chain the jwe-decrypt plugin
before openfga in the same Composer filter. The jwe-decrypt plugin decrypts the
Authorization header in-place; Envoy's JWT authn filter then validates the inner JWT
and claimsToHeaders makes the sub claim available as x-user-id for openfga to read.
Metrics
The extension exposes:
openfga_requests_total(counter, tag:decision): Authorization outcomesopenfga_check_duration_ms(histogram): Latency of OpenFGA Check API calls in milliseconds
| Decision | Description |
|---|---|
allowed |
The Check API allowed the request. |
denied |
The request was denied: OpenFGA returned not allowed, or no rule matched the request, or user / relation / object resolved empty (when fail_open is false). |
failopen |
The request was allowed because fail_open is true after a callout/API/parse failure, empty check inputs, or no matching rule. |
dryrun_allow |
OpenFGA would have denied the request but it was allowed because dry_run is enabled. |
error |
The request was denied after a callout failure, non-OK Check response, or unparseable response body (HTTP 502 to the client); not used when fail_open is true (those count as failopen). |
Usage Examples
AI Model Access Control
Control which users can access specific AI models (for example on an AIGatewayRoute).
The user identity comes from a header set by upstream authentication; the model name
comes from a header you choose (here x-ai-model, often set by the client or an
upstream filter). For Envoy AI Gateway’s body-derived model header (x-ai-eg-model),
use the multi-rule example below or place openfga after the AI Gateway ExtProc.
boe run --extension openfga \
--config '{
"cluster": "openfga",
"openfga_host": "localhost:8080",
"store_id": "YOUR_STORE_ID",
"user": {"header": "x-user-id", "prefix": "user:"},
"relation": {"value": "can_use"},
"object": {"header": "x-ai-model", "prefix": "model:"}
}' \
--cluster-insecure localhost:8080
# Allowed (if user:alice has can_use relation with model:gpt-4 in OpenFGA)
curl -H "x-user-id: alice" -H "x-ai-model: gpt-4" http://localhost:10000/v1/chat/completions
# Denied
curl -H "x-user-id: bob" -H "x-ai-model: gpt-4" http://localhost:10000/v1/chat/completions MCP Tool Access Control
Control which users can invoke specific MCP tools through an MCPRoute.
boe run --extension openfga \
--config '{
"cluster": "openfga",
"openfga_host": "localhost:8080",
"store_id": "YOUR_STORE_ID",
"user": {"header": "x-user-id", "prefix": "user:"},
"relation": {"value": "can_invoke"},
"object": {"header": "x-mcp-tool", "prefix": "tool:"}
}' \
--cluster-insecure localhost:8080 Multi-Route Authorization
Handle AI model access and MCP tool access on the same Gateway with a single filter instance. Rules are evaluated in order; the first match wins. A catch-all rule at the end covers generic HTTP resources.
boe run --extension openfga \
--config '{
"cluster": "openfga",
"openfga_host": "localhost:8080",
"store_id": "YOUR_STORE_ID",
"user": {"header": "x-user-id", "prefix": "user:"},
"rules": [
{
"match": {"headers": {"x-ai-eg-model": "*"}},
"relation": {"value": "can_use"},
"object": {"header": "x-ai-eg-model", "prefix": "model:"}
},
{
"match": {"headers": {"x-mcp-tool": "*"}},
"relation": {"value": "can_invoke"},
"object": {"header": "x-mcp-tool", "prefix": "tool:"}
},
{
"relation": {"value": "can_access"},
"object": {"header": "x-resource-id", "prefix": "resource:"}
}
]
}' \
--cluster-insecure localhost:8080
# AI model request (matches rule 1)
curl -H "x-user-id: alice" -H "x-ai-eg-model: gpt-4" http://localhost:10000/v1/chat/completions
# MCP tool request (matches rule 2)
curl -H "x-user-id: alice" -H "x-mcp-tool: github__issue_read" http://localhost:10000/mcp
# Generic HTTP request (matches catch-all rule 3)
curl -H "x-user-id: alice" -H "x-resource-id: planning" http://localhost:10000/api/docs Dry-Run Mode
Log authorization decisions without enforcing them.
boe run --extension openfga \
--config '{
"cluster": "openfga",
"openfga_host": "localhost:8080",
"store_id": "YOUR_STORE_ID",
"user": {"header": "x-user-id", "prefix": "user:"},
"relation": {"value": "can_access"},
"object": {"header": "x-resource", "prefix": "document:"},
"dry_run": true
}' \
--cluster-insecure localhost:8080 Fail-Open Mode
Allow requests when OpenFGA or the filter cannot complete a normal deny/allow path (unreachable API, missing tuple inputs, no matching rule, etc.); see Metrics for details.
boe run --extension openfga \
--config '{
"cluster": "openfga",
"openfga_host": "localhost:8080",
"store_id": "YOUR_STORE_ID",
"user": {"header": "x-user-id", "prefix": "user:"},
"relation": {"value": "can_access"},
"object": {"header": "x-resource", "prefix": "document:"},
"fail_open": true
}' \
--cluster-insecure localhost:8080 REST Resource Authorization via URL Path
Authorize access to REST resources identified by URL path segment — no custom
headers or ExtProc needed. For an API at /api/documents/{docId}, use
path_segment: 2 to extract the document ID from the third path segment (0-indexed).
Negative values count from the end: use path_segment: -1 to always get the last
segment, regardless of path depth.
boe run --extension openfga \
--config '{
"cluster": "openfga",
"openfga_host": "localhost:8080",
"store_id": "YOUR_STORE_ID",
"user": {"header": "x-user-id", "prefix": "user:"},
"relation": {"value": "can_access"},
"object": {"path_segment": 2, "prefix": "document:"}
}' \
--cluster-insecure localhost:8080
# GET /api/documents/planning -> checks user:alice can_access document:planning
curl -H "x-user-id: alice" http://localhost:10000/api/documents/planning
# GET /api/documents/budget -> checks user:alice can_access document:budget
curl -H "x-user-id: alice" http://localhost:10000/api/documents/budget
# Using -1 to always capture the last segment (works for /docs/budget or /api/v2/docs/budget)
# object: {"path_segment": -1, "prefix": "document:"} Contextual Tuples (Organization Membership)
Pass organization membership from a JWT claim as a contextual tuple. The tuple exists only for the duration of this Check request — no need to store it in OpenFGA. This is useful when the organization comes from the user's session (e.g., a JWT claim injected as a header by Envoy's JWT authentication filter).
boe run --extension openfga \
--config '{
"cluster": "openfga",
"openfga_host": "localhost:8080",
"store_id": "YOUR_STORE_ID",
"user": {"header": "x-user-id", "prefix": "user:"},
"relation": {"value": "can_use"},
"object": {"header": "x-ai-model", "prefix": "model:"},
"contextual_tuples": [
{
"user": {"header": "x-user-id", "prefix": "user:"},
"relation": {"value": "member"},
"object": {"header": "x-org-id", "prefix": "organization:"}
}
]
}' \
--cluster-insecure localhost:8080
# Checks user:alice can_use model:gpt-4 with contextual tuple
# user:alice member organization:acme
curl -H "x-user-id: alice" -H "x-org-id: acme" -H "x-ai-model: gpt-4" \
http://localhost:10000/v1/chat/completions API Token Authentication
Authenticate callouts to an OpenFGA server that requires an API token.
Use callout_headers to add an Authorization header to every Check request.
OpenFGA supports two authentication modes (see OpenFGA authentication setup):
- Preshared key — pass the configured key directly as
Bearer <key>. - OIDC (client credentials) — obtain a short-lived access token from your
IdP's token endpoint using the
client_credentialsgrant, then pass it asBearer <token>. OpenFGA will validate the token against the configured issuer and audience.
The filter does not refresh OIDC tokens itself; set up token rotation in the
identity provider or the surrounding platform and update callout_headers
accordingly.
boe run --extension openfga \
--config '{
"cluster": "openfga",
"openfga_host": "openfga.example.com",
"store_id": "YOUR_STORE_ID",
"user": {"header": "x-user-id", "prefix": "user:"},
"relation": {"value": "can_access"},
"object": {"header": "x-resource", "prefix": "document:"},
"callout_headers": {
"authorization": "Bearer YOUR_OPENFGA_API_TOKEN"
}
}' \
--cluster-insecure openfga.example.com:8080