AuthZEN
This middleware is currently in early access, available starting v3.21.0-ea.3.
The AuthZEN middleware sends each request to an external Policy Decision Point (PDP) over the AuthZEN protocol, and enforces its decision. See Understanding AuthZEN Authorization for the concept behind this middleware.
Configuration Example
- Single decision (evaluation)
- Multiple decisions (evaluations)
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: authzen-refund
namespace: apps
spec:
plugin:
authzen:
pdpEndpoint: "http://openfga:8080/.well-known/authzen-configuration/01KYSHBDW2QW6R5SE9CRJRGGKY"
mapping:
evaluation:
subject:
type: "user"
id: "$token.sub"
action:
name: "refund"
resource:
type: "order"
id: "$request.headers[?'X-Order-Id']" # optional CEL form
context:
amount: "$request.headers[?'X-Amount']"
onDenyResponse:
status: 403
body: '{"error":"forbidden"}'
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: authzen-move-file
namespace: apps
spec:
plugin:
authzen:
pdpEndpoint: "http://openfga:8080/.well-known/authzen-configuration/01KYSHBDW2QW6R5SE9CRJRGGKY"
mapping:
evaluations:
subject:
type: "user"
id: "$token.sub"
evaluations:
- action:
name: "read"
resource:
type: "folder"
id: "$request.query[?'from']" # optional CEL form
- action:
name: "write"
resource:
type: "folder"
id: "$request.query[?'to']"
onDenyResponse:
status: 403
body: '{"error":"forbidden"}'
Both examples build the AuthZEN request from token and request fields.
See Mapping the AuthZEN request for the full field-by-field breakdown, including the $ CEL convention.
That mapping builds on the subject/action/resource/context request shape explained in the concept page.
Configuration Options
| Field | Description | Default | Required |
|---|---|---|---|
pdpEndpoint | URL of the PDP's AuthZEN configuration endpoint. | "" | Yes |
clientConfig | HTTP client settings used to reach the PDP (timeout, retries, TLS). More information in Configuring the HTTP client. | No | |
mapping | The COAZ mapping used to build the AuthZEN request from the incoming HTTP request. Exactly one of mapping.evaluation or mapping.evaluations must be set. More information in Mapping the AuthZEN request. | Yes | |
onDenyResponse.status | HTTP status code returned when the PDP denies the request. Must be between 100 and 599, or the middleware fails to start. | 403 | No |
onDenyResponse.body | Go template for the response body. Can reference .Decision (single evaluation) or .Decisions (evaluations), each holding the context returned by the PDP. | "" | No |
onDenyResponse.headers | Extra headers to set on the deny response. | No | |
reason | Go template rendered against the decision and written to the log line, not to the response. Same template data as onDenyResponse.body. | "" | No |
maxRequestBodySize | Maximum request body size, in bytes, Traefik Hub buffers when mapping reads request.body. Requests over the limit are rejected with a 413. Has no effect if mapping never reads request.body. | 1048576 | No |
Mapping the AuthZEN request
Applies to both gateways: the mapping.evaluation and mapping.evaluations shapes below, and the $ CEL convention.
A mapping field is either a literal value, or, when prefixed with $, a CEL expression evaluated against the request.
To use a literal value that itself starts with $, escape it as $$; for example, "$$notcel" is sent to the PDP as the literal string $notcel.
A CEL expression errors when it selects a field or key that doesn't exist, and that error becomes a 500, not a deny.
Use CEL's optional forms to avoid this: $request.headers[?'X-Order-Id'] for a map key, or $token.?claim and $request.body.?field for field selection.
The configuration examples use this form for exactly that reason.
This is the same form the AuthZEN walkthrough uses for $request.body.?order_id.
API Gateway only: available variables are token (the caller's verified token claims) and request (method, host, path, headers, query, body).
request.headers keys are canonicalized the way Go's HTTP stack canonicalizes header names, so request.headers['X-User'] matches regardless of how the client sent it, but request.headers['x-user'] never matches.
request.query keys are matched exactly as sent on the wire, with no canonicalization.
The AuthZEN MCP middleware evaluates the same kind of expression against token and params instead.
Its policies[].match expression is a separate, non-CEL language that also sees mcp; see Configuring policies.
mapping.evaluation builds a single AuthZEN decision request:
| Field | Description | Required |
|---|---|---|
subject.type | The subject's type, for example "user". | Yes |
subject.id | The subject's identifier, for example $token.sub. | Yes |
action.name | The action being performed, for example an HTTP method or a fixed name like "refund". | Yes |
resource.type | The resource's type, for example "order". | Yes |
resource.id | The resource's identifier. | Yes |
context | Any other detail the PDP's policy needs. | No |
mapping.evaluations builds several decisions in one call. subject, action, and resource set at this level apply to every entry in evaluations unless a specific entry overrides them:
| Field | Description | Required |
|---|---|---|
subject | Shared subject for every entry, unless overridden per entry. Same shape as mapping.evaluation.subject. | Yes, in mapping.evaluations or on every entry |
action | Shared action for every entry, unless overridden per entry. Same shape as mapping.evaluation.action. | No, in mapping.evaluations; yes on any entry that doesn't inherit it |
resource | Shared resource for every entry, unless overridden per entry. Same shape as mapping.evaluation.resource. | No, in mapping.evaluations; yes on any entry that doesn't inherit it |
context | Shared context for every entry, unless overridden per entry. | No |
evaluations | List of { action, resource } entries, each using the same shape as mapping.evaluation.action / mapping.evaluation.resource, and each able to omit action/resource to inherit the shared value set in mapping.evaluations. At least one entry is required. | Yes |
options.evaluations_semantic | How the PDP evaluates multiple evaluations entries: execute_all (default) and deny_on_first_deny both require every entry to be allowed, and only differ in whether the PDP keeps scoring entries after a deny; permit_on_first_permit allows the request if any single entry is allowed. | No |
All entries in evaluations must be allowed by the PDP for the request to proceed; Traefik Hub denies the request if the PDP denies, or only partially answers, any entry.
This is the default behavior (execute_all).
mapping.evaluations.options.evaluations_semantic also accepts deny_on_first_deny and permit_on_first_permit.
deny_on_first_deny lets the PDP stop evaluating as soon as it hits a deny, instead of scoring every entry, without changing which requests get denied.
permit_on_first_permit does change the outcome: the request proceeds if any single entry is allowed.
Configuring the deny response
Applies to both gateways: this response shape, reused by the MCP middleware's defaultOnDenyResponse and per-policy onDenyResponse.
The MCP middleware always sets Content-Type: application/json on its JSON-RPC envelope after your configured headers, discarding a Content-Type set there.
This middleware lets a configured Content-Type win.
onDenyResponse.body is a Go template, with Sprig's functions available (toJson, upper, default, and the rest of Sprig's set).
When mapping.evaluation is used, the template data is .Decision, the context object the PDP returned for that single decision.
When mapping.evaluations is used, the template data is .Decisions, a list of context objects, one per entry in evaluations.
Reference a field like .Decision.reason only if your PDP actually returns it in its response context.
OpenFGA's evaluation response carries no context at all, so {{ .Decision.reason }} against an OpenFGA PDP renders <no value>.
.Decisions renders as Go's slice-of-maps form by default, [map[] map[]], which isn't valid JSON. Pipe it through toJson instead:
onDenyResponse:
status: 403
body: '{"error":"forbidden","decisions":{{ .Decisions | toJson }}}'
If onDenyResponse is left unset, Traefik Hub returns an empty 403 Forbidden response.
Configuring the HTTP client
Applies to both gateways: this section, and the clientConfig field.
clientConfig.timeoutSeconds (default 5) and clientConfig.maxRetries (default 3) control how long Traefik Hub waits for the PDP, and how many times it retries an unsuccessful call.
An unreachable or misbehaving PDP returns a 500, not the configured deny response.
Lowering these values returns that 500 sooner when the PDP is down, at the cost of giving a slow-but-healthy PDP less time to respond.
At the defaults, a PDP that never answers can hold a request for about 27 seconds.
At timeoutSeconds: 1 and maxRetries: 0, that drops to about 1 second.
The COAZ framework requires TLS between the PEP and the PDP.
Every example on this page uses a plain http://pdpEndpoint for readability.
In production, use https://.
If the PDP needs a bearer token in clientConfig.headers, a plain-HTTP endpoint sends it in cleartext.
Set clientConfig.tls (ca, cert, key, or insecureSkipVerify) to configure TLS to the PDP.
If your PDP is Keycloak, its AuthZEN evaluation endpoint requires a client-credentials bearer token from a client with authorization services enabled.
Its discovery document is open, but a request without a token in clientConfig.headers returns a 500 instead of a decision.
Traefik Hub doesn't refresh this token itself.
Reference it as a Kubernetes Secret (urn:k8s:secret:[name]:[valueKey]) in clientConfig.headers, and rotate it by updating that Secret.
Traefik Hub picks up the new value without you reapplying the Middleware resource.
Defines the configuration used to connect the API Gateway to a Third Party Software such as an Identity Provider.
clientConfig.tls
Storing secret values in Kubernetes secrets
When configuring the tls.ca, tls.cert, tls.key, it is possible to reference Kubernetes secrets defined in the same namespace as the Middleware.
The reference to a Kubernetes secret takes the form of a URN:
urn:k8s:secret:[name]:[valueKey]
This reference also works on any other string value in a plugin's configuration, not only clientConfig.tls.
For example, a bearer token configured in clientConfig.headers can reference a Secret the same way.
Rotate it by updating that Secret instead of reapplying the Middleware resource.
- Middleware configuration
- Kubernetes TLS secret
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: authzen-refund
spec:
plugin:
authzen:
clientConfig:
tls:
ca: "urn:k8s:secret:tls:ca"
cert: "urn:k8s:secret:tls:cert"
key: "urn:k8s:secret:tls:key"
insecureSkipVerify: true
apiVersion: v1
kind: Secret
metadata:
name: tls
stringData:
ca: |-
-----BEGIN CERTIFICATE-----
MIIB9TCCAWACAQAwgbgxGTAXBgNVBAoMEFF1b1ZhZGlzIExpbWl0ZWQxHDAaBgNV
BAsME0RvY3VtZW50IERlcGFydG1lbnQxOTA3BgNVBAMMMFdoeSBhcmUgeW91IGRl
Y29kaW5nIG1lPyAgVGhpcyBpcyBvbmx5IGEgdGVzdCEhITERMA8GA1UEBwwISGFt
aWx0b24xETAPBgNVBAgMCFBlbWJyb2tlMQswCQYDVQQGEwJCTTEPMA0GCSqGSIb3
DQEJARYAMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCJ9WRanG/fUvcfKiGl
EL4aRLjGt537mZ28UU9/3eiJeJznNSOuNLnF+hmabAu7H0LT4K7EdqfF+XUZW/2j
RKRYcvOUDGF9A7OjW7UfKk1In3+6QDCi7X34RE161jqoaJjrm/T18TOKcgkkhRzE
apQnIDm0Ea/HVzX/PiSOGuertwIDAQABMAsGCSqGSIb3DQEBBQOBgQBzMJdAV4QP
Awel8LzGx5uMOshezF/KfP67wJ93UW+N7zXY6AwPgoLj4Kjw+WtU684JL8Dtr9FX
ozakE+8p06BpxegR4BR3FMHf6p+0jQxUEAkAyb/mVgm66TyghDGC6/YkiKoZptXQ
98TwDIK/39WEB/V607As+KoYazQG8drorw==
-----END CERTIFICATE-----
cert: |-
-----BEGIN CERTIFICATE-----
MIIB9TCCAWACAQAwgbgxGTAXBgNVBAoMEFF1b1ZhZGlzIExpbWl0ZWQxHDAaBgNV
BAsME0RvY3VtZW50IERlcGFydG1lbnQxOTA3BgNVBAMMMFdoeSBhcmUgeW91IGRl
Y29kaW5nIG1lPyAgVGhpcyBpcyBvbmx5IGEgdGVzdCEhITERMA8GA1UEBwwISGFt
aWx0b24xETAPBgNVBAgMCFBlbWJyb2tlMQswCQYDVQQGEwJCTTEPMA0GCSqGSIb3
DQEJARYAMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCJ9WRanG/fUvcfKiGl
EL4aRLjGt537mZ28UU9/3eiJeJznNSOuNLnF+hmabAu7H0LT4K7EdqfF+XUZW/2j
RKRYcvOUDGF9A7OjW7UfKk1In3+6QDCi7X34RE161jqoaJjrm/T18TOKcgkkhRzE
apQnIDm0Ea/HVzX/PiSOGuertwIDAQABMAsGCSqGSIb3DQEBBQOBgQBzMJdAV4QP
Awel8LzGx5uMOshezF/KfP67wJ93UW+N7zXY6AwPgoLj4Kjw+WtU684JL8Dtr9FX
ozakE+8p06BpxegR4BR3FMHf6p+0jQxUEAkAyb/mVgm66TyghDGC6/YkiKoZptXQ
98TwDIK/39WEB/V607As+KoYazQG8drorw==
-----END CERTIFICATE-----
key: |-
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIC8CsJ/B115S+JtR1/l3ZQwKA3XdXt9zLqusF1VXc/KloAoGCCqGSM49
AwEHoUQDQgAEpwUmRIZHFt8CdDHYm1ikScCScd2q6QVYXxJu+G3fQZ78ScGtN7fu
KXMnQqVjXVRAr8qUY8yipVKuMCepnPXScQ==
-----END EC PRIVATE KEY-----
