Skip to content
Ironflow is pre-1.0: APIs can change between releases. Pin a version. What is ready →

Run an Envoy ext_authz policy

Ironflow gives a policy the same input shape as the OPA-Envoy plugin. A policy that you wrote for Envoy or Istio ext_authz runs with no edits, if it does not read the Authorization header. This guide shows how to set it up and what to do when it does read the header.

The scope of the claim is exact: embedded OPA engine, Rego v1, Envoy ext_authz input. Ironflow does not run the OPA-Envoy plugin and does not speak the ext_authz protocol.

Take the files of your policy, with their packages. This example is a small policy in package envoy.authz:

package envoy.authz
default allow := false
allow if input.attributes.request.http.method == "GET"

Copy the files into a pulled directory, for example ./p/envoy/policy.rego.

Name the rule that gives the answer. Pass it on push:

Terminal window
ironflow policy push ./p --decision data.envoy.authz.allow

Ironflow stores the path in the set and in .ironflow-policy.json. Later pushes keep it. The path must be outside data.ironflow. The rule must be a complete rule that returns a boolean, or an object with a boolean allowed. A rule that is missing, or that is a set or a function, fails the write with policy_set_bad_decision or policy_set_entrypoint_type.

How the answer counts:

Rule resultDecision
trueNo deny from this entry point.
falseDeny. The reason is <rule path> is false.
UndefinedDeny. The reason is <rule path> is undefined. This is how the OPA-Envoy plugin behaves.
{"allowed": true, ...}No deny. Ironflow ignores headers, body and http_status.
{"allowed": false, ...}, an object without allowed, or allowed: "true"Deny. The reply is still Ironflow’s own 403.

A true result does not grant anything. RBAC must already have allowed the request. See Authorization policy sets.

The decision rule runs only for requests that carry the Envoy view. These are the HTTP and ConnectRPC requests that pass the gate in front of the route. A check that a handler makes by itself has no HTTP request. It evaluates deny only.

These fields are present for an HTTP request:

FieldValue
input.attributes.request.http.methodThe HTTP method.
input.attributes.request.http.pathThe path with the query string. The path is the one the router acts on: it is normalized, and percent-decoding is applied as the router applies it.
input.attributes.request.http.hostThe Host of the request.
input.attributes.request.http.schemehttp or https.
input.attributes.request.http.headersA map with lower-case keys. Repeated headers are joined with a comma. It also has the pseudo-headers :method, :path and :authority, as Envoy has.
input.parsed_pathThe path split into segments.
input.parsed_queryA map from query key to a list of values.

The native view (input.request and input.subject) is present too. Policies for Envoy can use input.subject.roles, because Ironflow has already authenticated the caller.

These fields are absent: input.attributes.request.http.body, input.parsed_body, input.attributes.source and input.attributes.destination. Ironflow does not read the request body, because that would break file uploads and streaming.

Ironflow never copies these headers into input: authorization, proxy-authorization, cookie, set-cookie, ironflow-lease-token, x-api-key and sec-websocket-protocol. It also removes the token query parameter from path and parsed_query. A policy can copy any input value into a deny reason, and reasons reach the audit log. Credentials must never be in input.

A policy that decodes the bearer token itself cannot work. This policy reads input.attributes.request.http.headers.authorization:

package envoy.authz
import rego.v1
default allow := false
allow if {
is_token_valid
action_allowed
}
is_token_valid if {
token.valid
now := time.now_ns() / 1000000000
token.payload.nbf <= now
now < token.payload.exp
}
action_allowed if {
input.attributes.request.http.method == "GET"
token.payload.role == "guest"
glob.match("/people*", ["/"], input.attributes.request.http.path)
}
token := {"valid": valid, "payload": payload} if {
[_, encoded] := split(input.attributes.request.http.headers.authorization, " ")
[valid, _, payload] := io.jwt.decode_verify(encoded, {"secret": "secret"})
}

The header is missing from input, so token is undefined and allow stays false. Every request is denied.

Ironflow has already checked the credential. Use the result in input.subject instead of decoding the token. Replace the token rules with rules on the subject. This version keeps the method and path checks, and uses the role of the caller:

package envoy.authz
default allow := false
allow if {
input.attributes.request.http.method == "GET"
"viewer" in input.subject.roles
glob.match("/api/v1/users*", ["/"], input.attributes.request.http.path)
}

This file replaces the original file in the set. Your other files stay. Run opa test and ironflow policy dry-run as before.

A dry-run evaluates the decision rule only when input has an attributes key. Put the Envoy view in your input file:

{
"request": { "action": "users:read", "resource": "", "environment": "env_default", "org_id": "org_acme" },
"subject": { "roles": ["viewer"] },
"attributes": { "request": { "http": { "method": "POST", "path": "/api/v1/users", "host": "h", "scheme": "https", "headers": {} } } }
}
Terminal window
ironflow policy dry-run ./p --decision data.envoy.authz.allow --input request.json

For a directory dry-run, the CLI reads the decision rule from .ironflow-policy.json. Before the first push, that file does not have one, so pass --decision. The result for this input is a deny with the reason data.envoy.authz.allow is false.