- Security & Policies
- Custom Roles & Policy Sets
Custom Roles & Policy Sets
Overview
Section titled “Overview”Ironflow decides each request in two layers:
- Custom roles. Organization-scoped roles beyond the three built-in ones. A custom role grants no actions in Layer 1; it is a name that policy-set rules can test in
input.subject.roles. - The policy set. A group of Rego files that can deny a request that a role allowed. It is Layer 2. It cannot allow a request that RBAC denied.
How it works
Section titled “How it works”API key -> roles -> RBAC (Layer 1) -> Rego policy set (Layer 2, deny only) -> allow or deny- Each API key is assigned one or more roles.
- On every request, Layer 1 RBAC runs first. If the roles of the caller do not grant the action, the request is denied and Layer 2 does not run.
- Layer 2 evaluates the policy set of the organization against the request and the caller.
- A non-empty
denyset, a false decision rule or an evaluation error denies the request.
Layer 2 can only take away what Layer 1 granted. Role scoping happens in Rego. For example, "admin" in input.subject.roles limits a rule to admins. There is no link between a role and a policy.
IRN (Ironflow Resource Names)
Section titled “IRN (Ironflow Resource Names)”Every resource is identified by an IRN with 7 colon-separated segments:
irn:ironflow:{org}:{project}:{type}:{environment}:{id}| Segment | Description |
|---|---|
irn:ironflow | Fixed prefix |
{org} | Organization ID (e.g., org_default) |
{project} | Project ID (e.g., proj_default_default) |
{type} | Resource type: function, run, event, stream, projection, secret, org, role, user, project, file; any other API path uses its URL segment as-is (e.g., apikeys, policy-set) |
{environment} | Environment ID (e.g., env_default, env_default_prod, env_default_staging) |
{id} | Resource ID or * for wildcard |
A * replaces one whole segment. It is not a prefix or a suffix: order.*
matches the literal segment order.*, not order.created.
irn:ironflow:*:*:function:env_default_prod:* # all functions in prod, any org/projectirn:ironflow:org_default:*:*:*:* # all resources in org_defaultirn:ironflow:*:*:event:*:* # all events in any org/project/envActions
Section titled “Actions”Actions follow the {resource}:{operation} format:
| Action | Description |
|---|---|
functions:register | Register a function |
functions:invoke | Invoke a function |
functions:list | List functions |
functions:read | Read function details |
runs:read | Read runs |
runs:cancel | Cancel a run |
runs:delete | Delete a terminal run — admin-only, reached through the admin wildcard; never granted to developer or viewer |
events:emit | Emit events |
outbox:manage | Requeue or discard outbox dead-letter entries (listing needs only events:subscribe) |
events:subscribe | Subscribe to events |
streams:read | Read entity streams |
streams:delete | Delete an entity stream — admin-only, reached through the admin wildcard; never granted to developer or viewer |
events:redact | Irreversibly replace a stored event, step or run payload with a placeholder — admin-only, reached through the admin wildcard; never granted to developer or viewer |
entities:append | Append to streams |
entities:read | Read entity data |
projections:read | Read projections |
projections:manage | Manage projections |
secrets:read | Read secrets |
secrets:manage | Manage secrets |
users:read | Read users |
users:manage | Manage users |
users:password | Change your own password (PATCH /api/v1/users/{id}/password). Every built-in role holds it |
apikeys:read | Read API keys |
apikeys:manage | Manage API keys |
orgs:read | Read organizations, roles, and the policy set |
orgs:manage | Manage organizations, roles, and the policy set |
policies:write | A legacy alias, kept working. Layer 1 gates policy-set writes on orgs:manage. The self-lockout preflight evaluates a candidate set for both orgs:manage and policies:write. Write new rules against orgs:manage. See Recover from a lockout |
org:export | Export the org’s data snapshot (GET /api/v1/export) — admin-only, reached through the admin wildcard; never granted to developer or viewer |
agent:tools:read | List agent tools |
agent:tools:register | Register an agent tool |
agent:tools:invoke | Invoke an agent tool |
agent:tools:unregister | Unregister an agent tool |
files:read | Read and list file buckets and objects, and create download URLs |
files:write | Put, delete, move and copy file objects, and create upload URLs |
files:manage | Create, change and delete file buckets |
mcp:use | Connect to the remote MCP endpoint (/api/v1/mcp) |
* | Wildcard (all actions) |
A second platform:* family of 13 actions (platform:tenants:*,
platform:users:*, platform:keys:*, platform:roles:*,
platform:policies:*, platform:audit:read, platform:impersonate and
platform:impersonate:read) gates the control-plane surface
reached with an ifplatform_ key. Those actions are not part of an
organization’s own authorization surface — do not name them in a tenant policy set. A fourteenth action, platform:bootstrap, runs the one-time platform bootstrap. Only an admin of the default organization holds it. Four platform:instance:* actions (read, freeze, attest, reconcile) gate the instance routes under /api/v1/platform/instance. The cluster: actions (cluster:rotate-token, cluster:log-level, cluster:health, cluster:inspect) are instance-scoped in the same way: only an admin of the default organization or a platform principal whose role grants them (built in: platform_admin) can pass them.
Example rules
Section titled “Example rules”Each rule is a deny rule in package ironflow.authz. See Author a policy set for how to test and push them.
Deny all writes to production:
package ironflow.authz
import data.ironflow.lib.irn
deny contains "no writes to production" if { irn.match_action("functions:register,functions:invoke,events:emit,entities:append", input.request.action) irn.match_resource("irn:ironflow:*:*:*:env_default_prod:*", input.request.resource)}Deny destructive actions in production:
package ironflow.authz
import data.ironflow.lib.irn
deny contains "no destructive operations in production" if { irn.match_action("secrets:manage,projections:manage,apikeys:manage", input.request.action) irn.match_resource("irn:ironflow:*:*:*:env_default_prod:*", input.request.resource)}Deny non-oncall callers in staging:
package ironflow.authz
import data.ironflow.lib.irn
deny contains "staging is for the on-call rotation" if { irn.match_resource("irn:ironflow:*:*:*:env_default_staging:*", input.request.resource) not "oncall" in input.subject.roles}The policy set
Section titled “The policy set”The policy set is documented in its own pages:
- Authorization policy sets explains the model, the input and the decision contract.
- Author a policy set walks through pull, test, dry-run and push.
- Policy set input lists every input field.
- Policy set IRN library lists the matching functions.
REST API
Section titled “REST API”| Method | Path | Description |
|---|---|---|
POST | /api/v1/roles | Create custom role |
GET | /api/v1/roles | List roles |
GET | /api/v1/roles/{id} | Get role |
PATCH | /api/v1/roles/{id} | Update role |
DELETE | /api/v1/roles/{id} | Delete role |
Create Role
Section titled “Create Role”POST /api/v1/rolesContent-Type: application/jsonAuthorization: Bearer <api-key>{ "name": "billing-team"}Response (201 Created):
{ "id": "role_a1b2c3d4", "org_id": "org_default", "name": "billing-team", "is_default": false, "created_at": "2026-03-01T10:00:00Z"}Policy set
Section titled “Policy set”| Method | Path | Description |
|---|---|---|
GET | /api/v1/policy-set | Get the active set. Version 0 means no set. The answer has enforcement_active. |
PUT | /api/v1/policy-set | Replace the set. Needs If-Match: <version>. |
GET | /api/v1/policy-set/versions | List versions. |
GET | /api/v1/policy-set/versions/{n} | Get one version with its files. |
POST | /api/v1/policy-set/versions/{n}/rollback | Store version n as a new version. Needs If-Match. |
POST | /api/v1/policy-set/validate | Validate a candidate set. |
POST | /api/v1/policy-set/dry-run | Evaluate a candidate set, or the active set, for one input. |
The same seven routes exist under /api/v1/platform/policy-set for the platform organization’s set. A platform principal can also call POST /api/v1/platform/orgs/{org}/policy-set/monitor to force a tenant’s set into monitor mode. See Policy set errors for the error codes.
Role Commands
Section titled “Role Commands”ironflow role create <name> --org <org_id>ironflow role list --org <org_id>ironflow role get <id>ironflow role delete <id>Policy set commands
Section titled “Policy set commands”ironflow policy pull <dir>ironflow policy validate <dir>ironflow policy dry-run [dir] --input request.json [--explain]ironflow policy push <dir> [--mode enforce|monitor] [--decision <rule>]ironflow policy versionsironflow policy rollback <version> --dir <dir>ironflow policy force-monitor <org> --reason "<why>" # platform credentials, break-glassSee ironflow policy for every flag.
Caching and performance
Section titled “Caching and performance”- Each node compiles the set of an organization once for each version and keeps the compiled query.
- A write increments the epoch of the organization (
policy_epoch). Every node that sees the new epoch drops its compiled query and compiles the new set on the next request. - There is no decision cache. Each request evaluates the compiled set, with a 50 ms limit.
- A role change does not increment the epoch. Each API request reads the caller’s roles from the store when it authenticates, so a role change applies to the caller’s next request.