- Security & Policies
- Run an Envoy ext_authz policy
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.
Step 1. Put the files in the set
Section titled “Step 1. Put the files in the set”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.
Step 2. Set the decision rule
Section titled “Step 2. Set the decision rule”Name the rule that gives the answer. Pass it on push:
ironflow policy push ./p --decision data.envoy.authz.allowIronflow 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 result | Decision |
|---|---|
true | No deny from this entry point. |
false | Deny. The reason is <rule path> is false. |
| Undefined | Deny. 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.
The Envoy view
Section titled “The Envoy view”These fields are present for an HTTP request:
| Field | Value |
|---|---|
input.attributes.request.http.method | The HTTP method. |
input.attributes.request.http.path | The 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.host | The Host of the request. |
input.attributes.request.http.scheme | http or https. |
input.attributes.request.http.headers | A 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_path | The path split into segments. |
input.parsed_query | A 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.
The Authorization header limit
Section titled “The Authorization header limit”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.
The adapter
Section titled “The adapter”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.
Test the decision rule
Section titled “Test the decision rule”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": {} } } }}ironflow policy dry-run ./p --decision data.envoy.authz.allow --input request.jsonFor 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.