Authorization
After a request is authenticated, the auth-gateway authorizes the principal against the matched route's requirements and any global rules. Authorization runs only when there is a matched route's auth context; if the route has no auth-related fields, the principal proceeds with no additional checks beyond authentication.
A denial returns 403 forbidden with the reason and the principal's roles and scopes in the JSON
body.
Evaluation order01
The AuthzEvaluator runs these steps in sequence and returns deny on the first failure:
Required roles (
require_roles) — if non-empty, the principal must have at least one of the listed roles (any-of). Otherwise: deny.Required scopes (
require_scopes) — if non-empty, the principal must have all of the listed scopes (all-of). Otherwise: deny.Route Rhai rule (
authz_rule) — if set, the expression is evaluated; if it denies, the request is denied. (A rule that allows does not short-circuit — evaluation continues to global rules.)Global rules (
auth.authz.global_rules) — each rule is evaluated in order and may deny.
If all steps pass, the request is allowed.
Steps 1 and 2 are plain Rust comparisons against the principal's roles/scopes — no scripting
engine is involved, so they are cheap and always available. A principal authenticated with empty
roles (for example a standard JWT without a roles claim) will fail any non-empty require_roles.
Roles and scopes02
These come from the principal that the provider produced:
JWT / OIDC — roles from the
rolesclaim, scopes from the whitespace-splitscopeclaim.API key — roles are always
["api_consumer"]; scopes are the matched key entry'sscopes.Forward auth — roles and scopes from the auth service's
X-Auth-Role/X-Auth-Scopes.mTLS — roles from
cn_to_roles; no scopes.
Set them per route:
routes:
- path: /admin/*
upstream: backend
require_roles: ["admin", "superadmin"] # any-of
require_scopes: ["write"] # all-ofRhai rules03
A Rhai expression evaluates to a boolean: true means allow, false means deny. If evaluation errors (syntax, runtime, or a non-boolean result), the rule is treated as deny. The engine is sandboxed with limits — max expression depth 25/10, max 10,000 operations, and max string size 4096 bytes — so a runaway script is denied rather than hanging the request.
Rules run with these variables and helper functions in scope:
principal— a map withid,name,roles(array),scopes(array), andattributes(map).request— a map withmethodandpath.has_role(role)— true if the principal hasrole.has_scope(scope)— true if the principal hasscope.has_any_role(roles)— true if the principal has any role in the array.has_all_roles(roles)— true if the principal has every role in the array.
routes:
- path: /reports/*
upstream: analytics
# Allow admins, or anyone with the reports scope doing a GET.
authz_rule: 'has_role("admin") || (has_scope("reports") && request.method == "GET")'The Rhai context exposes request.method and request.path (and the principal), but not request
headers or route metadata — those are only available to OPA (below).
Global rules and the engine04
auth.authz configures the authorization engine and a list of global rules applied to every
authenticated request after the per-route checks.
| Key | Type | Default | Description |
engine | rhai | opa | both | rhai | Default engine used to evaluate global rules. |
global_rules | list of rules | [] | Rules applied to all authenticated requests. |
opa | object | none | OPA connection settings (see OPA). |
A global rule
| Key | Type | Default | Description |
name | string | — | Rule name, surfaced in deny reasons. Required. |
description | string | none | Free-text description. |
engine | rhai | opa | both | inherits engine | Override the engine for this rule. |
rule | string | — | A Rhai expression, or (for OPA) the policy is evaluated remotely. Required. |
action | allow | deny | allow | How the rule's boolean outcome is interpreted (see below). |
How action combines with a rule's boolean outcome:
action | Rule evaluates to allow (true) | Rule evaluates to deny (false) |
allow | continue (request stays allowed) | deny — "Not allowed by rule" |
deny | continue | deny — "Denied by rule" |
In other words: with action: allow, the rule expresses a condition that must hold (a false
result denies); with action: deny, the rule expresses a forbidden condition (a true/deny result
denies). Any rule can short-circuit to a denial; rules never override an earlier denial.
Each global rule's effective engine is its own engine if set, else the top-level
auth.authz.engine. When a rule's engine is opa or both but no opa block is configured, the
rule silently falls back to evaluating its rule string as Rhai.
OPA05
When engine is opa (or both) and an opa block is configured, global rules are decided by an
external Open Policy Agent server. The gateway POSTs an input
document to the configured endpoint and reads a boolean result: true allows, anything else (or
a missing result) denies.
The input.input document sent to OPA contains:
principal—id,name,roles,scopes,attributes.request—method,path, and the requestheaders.route—upstream,path, and routemetadata.
opa key | Type | Default | Description |
endpoint | string | — | OPA data API URL that returns { "result": <bool> }. Required. |
timeout | duration | 100ms | Per-request timeout to OPA. |
cache_ttl | duration | 300s | How long an OPA decision is cached (keyed by principal id, method, path, and a hash of headers). |
fail_open | boolean | false | When OPA is unreachable or errors: true allows the request, false denies it. |
fail_open: true means an OPA outage causes requests to be allowed rather than blocked. Use it
only when availability outweighs policy enforcement, and prefer false for sensitive routes.
auth:
default_provider: primary-jwt
global_enforce: true
authz:
engine: both
global_rules:
# Rhai: block a path outright (deny when the expression is true)
- name: block-internal
engine: rhai
rule: 'request.path == "/internal"'
action: deny
# OPA: defer the decision to a policy server
- name: opa-policy
engine: opa
rule: "data.octopus.allow"
action: allow
opa:
endpoint: http://opa:8181/v1/data/octopus/allow
timeout: 100ms
cache_ttl: 30s
fail_open: falseFor Rhai rules the rule string is the expression itself. For OPA the decision comes from the
opa.endpoint response; the rule's rule string is not evaluated locally when OPA handles it.
See also06
Authentication — producing the principal that gets authorized.