- Security & Policies
- Author a policy set
Author a policy set
This guide takes you from an empty set to a pushed set. The flow has five steps: pull, edit, opa test, dry-run, push. At the end you know the rules for file names, packages, builtins and size, and you know how to roll back.
For the model, read Authorization policy sets first.
Before you start
Section titled “Before you start”- An admin role. Policy writes need the
orgs:manageaction. - The
ironflowbinary on your PATH and an API key inIRONFLOW_API_KEY. For a server that is nothttp://localhost:9123, setIRONFLOW_SERVER_URLor pass--server. - The
opaCLI, v1 or later, for local tests. spec.auth.policyEnforcement: trueon the server. Without it, the set is stored but no request meets it. In dev mode (ironflow serve --dev,spec.auth.devMode) the server skips all authorization, so a set has no effect there either.
Step 1. Pull the set
Section titled “Step 1. Pull the set”ironflow policy pull ./policiespull writes the files of the active set into ./policies. For an organization with no set, the directory holds only the helper files. pull also writes these files:
.ironflow-policy.jsonholds the version, the decision rule and the mode.pushreads it..ironflow/lib/irn.regois the IRN library. It makesopa testwork on your machine..ironflow/input.schema.jsonis the schema ofinput. See Test policies locally.
If ./policies holds .rego files that the server does not have, pull stops and lists them. Run it again with --prune to delete exactly those files.
Step 2. Write a rule
Section titled “Step 2. Write a rule”A new policy uses the native view of input. Create ./policies/authz.rego:
package ironflow.authz
deny contains "members cannot delete runs" if { "member" in input.subject.roles input.request.action == "runs:delete"}The rule deny is a set of strings. When the set is not empty, the request is denied. Each string is a reason. Reasons go to the audit row and to dry-run, so write text that helps the person who reads it.
To match a resource, use the IRN library:
package ironflow.authz
import data.ironflow.lib.irn
deny contains "members cannot touch production functions" if { "member" in input.subject.roles irn.match("irn:ironflow:*:*:function:env_prod:*", input.request.resource)}A set can have many files. Files import each other with normal Rego imports. This example puts a helper in lib/common.rego:
package acme.lib
is_service_account if endswith(input.subject.user_email, "@svc.acme.test")and uses it in authz.rego:
package ironflow.authz
import data.acme.libimport data.ironflow.lib.irn
deny contains "service accounts cannot touch production secrets" if { lib.is_service_account irn.match("irn:ironflow:*:*:secret:env_prod:*", input.request.resource)}subject.user_email is an empty string for a raw API key without a bound user. Test the field before you use it.
Step 3. Test with opa test
Section titled “Step 3. Test with opa test”Write tests next to the rules. A test file ends in _test.rego:
package ironflow.authz_test
import data.ironflow.authz
test_member_cannot_delete_runs if { count(authz.deny) == 1 with input as { "request": {"action": "runs:delete"}, "subject": {"roles": ["member"]}, }}
test_admin_can_delete_runs if { count(authz.deny) == 0 with input as { "request": {"action": "runs:delete"}, "subject": {"roles": ["admin"]}, }}Run them:
opa test ./policiesIronflow stores _test.rego files with the set, so pull returns the repository that you pushed. The engine never evaluates test rules.
Step 4. Dry-run against the server
Section titled “Step 4. Dry-run against the server”A dry-run sends one input to the server and returns the decision. It stores nothing. Write the input to request.json:
{ "request": { "action": "runs:delete", "resource": "irn:ironflow:org_acme:proj_default:run:env_default:run_1", "environment": "env_default", "org_id": "org_acme" }, "subject": { "id": "user_1", "roles": ["member"], "user_email": "ann@acme.test" }}ironflow policy dry-run ./policies --input request.jsonOutput:
decision: deny (set mode: monitor)denied by: deny - members cannot delete runsWithout a directory, dry-run evaluates the active set on the server. Add --explain to print the trace notes. See Debug a deny.
Validate alone, with no input, checks that the set compiles and passes every write rule:
ironflow policy validate ./policiesStep 5. Push
Section titled “Step 5. Push”ironflow policy push ./policiespush replaces the whole set with every .rego file under the directory. It skips .ironflow/ and .git/. It sends the version from .ironflow-policy.json in an If-Match header. If another admin wrote first, the server answers 412 and the CLI tells you to pull again.
The first push of a new set is in monitor mode. To choose the mode, pass --mode enforce or --mode monitor. If you leave the flag out, the mode stays as it is. To set a decision rule, pass --decision data.envoy.authz.allow. See Roll out with monitor mode.
Before it saves, the server runs the self-lockout preflight. The preflight evaluates the candidate set for every admin who must keep the right to read and write the policy set. If the set would deny one of them, the write fails with policy_set_self_lockout. See Recover from a lockout.
If the answer is a 503 with policy_set_not_propagated, the set is saved as version N but not active on every node. Run the same push again. The CLI has already recorded N, so the repeat is a no-op that finishes propagation.
Rules for a valid set
Section titled “Rules for a valid set”The server checks these rules on every write, and in validate. One failure rejects the whole set with HTTP 400 and one error per file.
File names
Section titled “File names”A name is a relative path with / separators. Each segment uses only letters, digits, _, . and -. The name ends in .rego. Names with .. or . segments, a leading /, empty segments or control characters are rejected. A file in a .ironflow or .git directory is rejected too, in any letter case, because ironflow policy push does not read those directories. Two names that differ only in case are rejected. Error code: policy_set_bad_file_name.
Rego v1 only
Section titled “Rego v1 only”A file in Rego v0 syntax fails with policy_set_v0_syntax. Convert it, and keep the logic, with:
opa fmt --v0-v1 -w ./policiesReserved packages
Section titled “Reserved packages”You cannot declare a package under data.ironflow. Two names are open to you: ironflow.authz for the entry point, and ironflow.authz_test for tests. Ironflow injects its IRN library as ironflow.lib.irn. Any other package under ironflow fails with policy_set_reserved_namespace. Packages such as envoy.authz or acme.lib are free. All reserved names are listed in Reserved names.
Types of the entry points
Section titled “Types of the entry points”deny must be a partial set rule: deny contains <reason> if { ... }. The decision rule must be a complete rule, such as allow := ..., that returns a boolean or an object. It cannot be a set or a function, and it must live outside data.ironflow. Error codes: policy_set_entrypoint_type and policy_set_bad_decision.
Rules in package ironflow.authz are also type-checked against the input schema. A wrong field type, for example input.request.action + 1, fails with policy_set_schema_error. Other packages are not type-checked, because they are your own policies for your own input shape.
Banned builtins
Section titled “Banned builtins”These builtins do input or output, read engine state, or can allocate memory without bound in one call that evaluation cannot interrupt. A set that uses one fails with policy_set_builtin_forbidden:
http.sendnet.lookup_ip_addropa.runtimejson.match_schema,json.verify_schemabits.lshregex.replace,regex.split,regex.find_n,regex.find_all_string_submatch_n,regex.template_matchindexof_n
Pure builtins stay. This includes time.* for time windows and net.cidr_* for address math. The list of allowed builtins is internal/auth/policyset/testdata/allowed-builtins.golden in the repository.
sprintf has two extra rules. The format must be a string literal. The format may have at most 32 verbs, no width or precision above 9999, and no * width or precision. Otherwise the write fails with policy_set_sprintf_format. replace and split on large strings can still allocate much memory. Keep the strings that they read small.
Size and time limits
Section titled “Size and time limits”| Limit | Value | Error |
|---|---|---|
| Files per set | 500 | policy_set_too_large |
| Source per set | 2 MiB | policy_set_too_large |
| Compile time | 2 seconds | policy_set_compile_timeout |
| Evaluation time per request | 50 ms | The request is denied in enforce mode (monitor mode records would_deny). |
| Audit reason | 1 KiB | The reasons are cut with a …(+N more) suffix. |
Write rate limits
Section titled “Write rate limits”The limits apply to each organization on each node. A cluster of N nodes allows N times the rate.
- Writes (
pushand rollback): one write every 6 seconds, with a burst of 5. Above the limit the answer is 429 withRetry-Afterin whole seconds. - Checks (
validateanddry-run): one request every second, with a burst of 10. - All compiles in flight on one node: at most 4. Above that, the answer is 429 with
policy_set_busy.
Versions and rollback
Section titled “Versions and rollback”Every write that changes the set creates a version. A write that equals the active set (same files, decision and mode) creates no version and returns the current one. Ironflow keeps the newest 100 versions for each organization.
List the versions, newest first:
ironflow policy versionsVERSION MODE CREATED BY DECISION4 enforce 2026-10-12T08:00:00.000000Z user_ann3 monitor 2026-10-11T09:11:48.000000Z user_ann2 monitor 2026-10-10T14:30:11.000000Z user_bob data.envoy.authz.allow1 monitor 2026-10-09T10:14:02.000000Z user_bobRoll back with the number of the version you want:
ironflow policy rollback 3 --dir ./policiesA rollback is a new write. It stores the files and the decision rule of version 3 as version 5. History never changes. These rules apply:
- The mode stays. A rollback restores files and the decision rule. It keeps the current mode. Rolling back does not switch enforcement on or off.
- Pull afterward. The CLI does not change your local directory on a rollback. Run
ironflow policy pull ./policiesto get the restored files and the new version number. - Same safeguards as a push. A rollback needs the current version in
If-Match(from--diror--if-version), runs the self-lockout preflight and is rate-limited like a push.
The dashboard shows a per-file diff between two versions under Version history.
The dashboard editor
Section titled “The dashboard editor”The page Policies (/policies) edits the same set. It has a file tree, a code editor with Rego highlighting, a Decision rule field and a Mode switch. The Validate button checks the draft with the server. A dry-run pane evaluates an input against the draft and can show the explain trace. Save writes the draft. The page asks before you leave with unsaved changes. Switching from monitor to enforce opens a dialog with the count of would-deny rows from the last 24 hours.
The dashboard never bypasses the self-lockout preflight. The platform set has the same editor at /platform/policies. On the CLI, platform operators address the platform set with the --platform flag (it needs platform credentials) on pull, push, validate, dry-run, versions and rollback.
Next steps
Section titled “Next steps”- Start without risk: Roll out with monitor mode
- Use a Rego policy that you already have: Port an existing policy
- Limit one API key to some functions: Limit an agent key to specific functions
- Every error code: Policy set errors