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

Authorization policy sets (Rego)

Ironflow makes each authorization decision in two layers. Layer 1 is role-based access control (RBAC). Layer 2 is the policy set of the organization. The policy set is a group of Rego files. It can deny a request that RBAC allowed. It cannot allow a request that RBAC denied.

Use a policy set for rules that roles cannot express. For example: “developers can invoke functions, except from a contractor email address, except in a change freeze, except on production secrets.”

Request Layer 1: RBAC roles grant actions the only layer that allows allow Layer 2: Rego policy set deny set is not empty decision rule is false or undefined error, timeout or panic any one of these is a deny pass Handler 403: no grant 403 in enforce mode monitor mode: allow, write a would_deny row

Layer 1 decides first. If RBAC denies the request, Layer 2 does not run. If RBAC allows it, Layer 2 evaluates the organization’s set. The rule “Layer 2 can only deny” is part of the design. A Rego rule such as allow := true adds no access.

A policy set belongs to one organization. Each organization has at most one set. It has these parts:

  • Modules. A map from file name to Rego source. The files use Rego v1 syntax. They can import each other and can have _test.rego files.
  • Decision rule. An optional rule path, for example data.envoy.authz.allow. Use it for policies that were written as Envoy ext_authz policies.
  • Mode. monitor or enforce. A new set starts in monitor.
  • Version. A number that grows with each write. Ironflow keeps the newest 100 versions.

An organization with no set has no Layer 2 opinion. The platform organization has its own set. See The platform set.

The engine embeds the OPA Go library (Rego v1). It runs no OPA server, no OPA bundles and no OPA decision logs. The binary stays a single process, and the engine builds without cgo. The IRN helper library is a Rego package that Ironflow adds to every set, so stock opa test can run on your files. See Policy set IRN library.

The scope of the OPA claim is exact: embedded OPA engine, Rego v1, Envoy ext_authz input. Ironflow makes no other claim about OPA.

Every evaluation gets one input document. The document has two views.

The native view is always present:

  • input.request has action, resource, environment and org_id.
  • input.subject has id, user_email, roles, groups, org, project, env, api_key_id, is_platform and webhook_source_id.

The Envoy view is present when the check is for an HTTP request. It copies the input of the OPA-Envoy plugin:

  • input.attributes.request.http has method, path, host, scheme and headers.
  • input.parsed_path is the path split into segments.
  • input.parsed_query maps each query key to a list of values.

The path is the path that the router sees, with percent-decoding applied as the router applies it. 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. The request body is not read. Read the full field list in Policy set input.

A check made inside a handler has no HTTP request. For example, the impersonation check works this way. These checks carry the native view only.

Two entry points decide the outcome.

Entry pointTypeEffect
data.ironflow.authz.denyset of stringsA set that is not empty denies. Each member is a reason.
The decision rule of the set, for example data.envoy.authz.allowboolean, or an object with a boolean allowedfalse denies with the reason <rule path> is false. An undefined rule also denies, as the OPA-Envoy plugin does.
Neither appliesnot applicableNo opinion. Layer 1 decides.

Note these points:

  • The decision rule applies only when the request has the Envoy view. Handler-side checks evaluate deny only. Without this rule, an Envoy policy that reads input.attributes would deny every handler-side check.
  • Ironflow evaluates both entry points when both apply. Either one can deny. Neither one can allow what Layer 1 denied.
  • Ironflow ignores the other fields of an object result (headers, body, http_status). A deny always returns Ironflow’s own 403.
  • A result that is not a boolean, or an object without a boolean allowed, denies. The string "true" denies.

The 403 body says insufficient permissions. It does not carry the reasons. The reasons go to the audit row and to dry-run. See Debug a deny.

A set in monitor mode runs on every request, exactly as in enforce mode. A deny does not block the request. Instead, Ironflow writes an authz.decision.would_deny audit row and adds one to the counter ironflow_policy_would_deny_total. The rows are sampled: at most one row for each combination of organization, action and first reason, every 10 seconds on each node. The counter counts every would-deny decision. The capture settings for allowed decisions do not apply to these rows. This is true for a deny, an evaluation error, a timeout and a compile failure.

Use monitor mode to run an imported set against real traffic and read what it would deny. Then switch to enforce with an ordinary write. See Roll out with monitor mode.

The self-lockout preflight runs on every write in both modes. A monitor set that would lock your admins out is rejected at write time.

Layer 2 denies when it cannot decide.

  • An evaluation error, a timeout (50 ms) or a panic denies. The audit reason is policy evaluation error (<class>).
  • A builtin error denies. For example, startswith(123, "a") is an error. Without this rule, OPA treats the error as “undefined” and a broken deny rule would allow the request.
  • A stored set that no longer compiles denies requests of that organization in enforce mode. In monitor mode it writes a would_deny row. Other organizations are not affected. Every node compiles every stored set at startup and logs each failure. After an OPA upgrade, run ironflow policy validate for each set. See Upgrade from CEL.
  • A node that cannot read the store keeps serving the last set it compiled (fail-stale). A node that never loaded the set cannot know its mode, so it denies in both modes.
  • A deny reason that is not a string becomes non-string deny reason, and the request is still denied.

In monitor mode these cases, except the cold-node case, become would_deny rows. They do not block the request.

The platform organization has its own set. It uses the same editor, the same CLI with --platform, and the routes under /api/v1/platform/policy-set. It needs platform credentials. The platform set decides requests from platform principals. This includes instance-scoped actions such as platform:* and cluster:*, also during impersonation.

A platform principal that acts for a tenant organization is subject to that organization’s set only in handler-side checks, and only to its deny rule. The platform operator break-glass, ironflow policy force-monitor, is evaluated against the platform set. See Recover from a lockout.

The tenant routes are under /api/v1/policy-set and need admin RBAC. The platform routes are under /api/v1/platform and need platform credentials. The CLI addresses the platform set with --platform.

OperationRESTCLIGoNode and browserPython
Get the active setGET /policy-setpolicy pullGetPolicySetpolicySet.getpolicy_set_list
Replace the setPUT /policy-setpolicy pushPutPolicySetpolicySet.putpolicy_set_update
Validate a candidatePOST /policy-set/validatepolicy validateValidatePolicySetpolicySet.validatepolicy_set_validate
Dry-run one inputPOST /policy-set/dry-runpolicy dry-runDryRunPolicySetpolicySet.dryRunpolicy_set_dry_run
List versionsGET /policy-set/versionspolicy versionsListPolicySetVersionspolicySet.versionspolicy_set_list_versions
Get one versionGET /policy-set/versions/{n}GetPolicySetVersionpolicySet.versionpolicy_set_get_versions
Roll backPOST /policy-set/versions/{n}/rollbackpolicy rollbackRollbackPolicySetpolicySet.rollbackpolicy_set_versions_rollback
Break-glass to monitor (platform)POST /platform/orgs/{org}/policy-set/monitorpolicy force-monitorplatform_policy_set_create

A write is a compare-and-swap. PUT and rollback need If-Match: <version>, the version you read (0 for a new set). If another admin wrote first, the answer is 412. A 503 policy_set_not_propagated is not retryable: the write committed as the version in the message. Send the same request again with If-Match set to that version, never the old one. See Policy set errors.

The Go, Node and browser SDKs have no platform methods. The Python SDK names its platform methods differently from the tenant ones. Full signatures and examples: REST API, Platform API, CLI, Go SDK, Browser SDK (the Node client has the same methods), Python SDK.

  • Size. A set has at most 500 files and 2 MiB of source. Compile time is limited to 2 seconds. Evaluation time is limited to 50 ms.
  • Banned builtins. http.send, net.lookup_ip_addr, opa.runtime and a few builtins that can allocate without bound are not available. See Author a policy set.
  • Unmapped routes. A route with no mapped action is not checked by RBAC or by the policy set.
  • Dev mode. serve --dev skips authorization. Policy sets do nothing there.
  • No data documents. A set cannot read external data. The namespace data.ironflow.* is reserved for Ironflow. All reserved names are listed in Reserved names.
  • No decision cache. Each request evaluates the compiled set. Ironflow compiles a set once per version on each node.