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

Upgrade from CEL policies to a Rego policy set

Rego policy sets replace CEL policies. This is a hard cut. Ironflow does not translate CEL to Rego, because a translation error would silently allow a request that you meant to block. You rewrite each policy by hand, with this guide.

  • The engine refuses to upgrade while any CEL policy row exists. Platform policy rows count.
  • The upgrade drops the tables policies, policy_versions, policy_templates and role_policies. The version history of your CEL policies goes with them.
  • The REST routes for CEL policies, policy templates and role-policy links are gone. So are the platform CEL policy routes. The only policy routes now are /api/v1/policy-set and /api/v1/platform/policy-set.
  • The CLI commands for CEL policies and templates (ironflow policy create, list, get, update, delete, test, template and versions list) are gone. So are the ironflow role commands that attach a policy to a role, and the --policy-ids flag of ironflow platform roles create. ironflow policy now manages the policy set.
  • The SDK methods for CEL policies and role-policy links are gone. The SDKs have policySet methods. See Policy set errors for the error codes.
  • Role-policy assignment is gone. Enforcement never read it: every deny policy of the organization applied, whatever the assignment. To limit a rule to a role, test the role in Rego: "admin" in input.subject.roles.
  • Layer 2 enforcement is still opt-in (spec.auth.policyEnforcement).

If CEL rows exist, the new binary stops at migration time. The message looks like this:

store: CEL policy rows exist: cannot upgrade: 3 CEL policy row(s) exist in table "policies". Rego policy sets replace CEL policies.
List them: SELECT id, org_id, name, condition FROM policies ORDER BY org_id, name;
Rewrite each as Rego (see docs/how-to-guides/policies/upgrade-from-cel.md), delete the rows, then start the engine again

The engine does not change the database when it stops. You can go back to the old binary.

The policy-set API exists only after the upgrade. So you prepare the Rego set first, and push it second.

  1. List the CEL rows. Run the SQL below on the old version.
  2. Write the Rego set and test it. Write the files in a local directory. Run opa test there.
  3. Delete the CEL rows. Save the old rows first if you want a record. The version history is dropped by the upgrade.
  4. Upgrade. Start the new binary. The migration passes.
  5. Push the set. ironflow policy pull ./p, copy your files into ./p, ironflow policy validate ./p, then ironflow policy push ./p. The set starts in monitor mode.
  6. Read the would-deny rows. See Roll out with monitor mode.
  7. Switch to enforce.

Between steps 3 and 5, no Layer 2 deny applies. If this gap is not acceptable, upgrade a staging cluster first, and plan the production window to be short. If you have no CEL rows, step 3 has nothing to do, and the upgrade runs with no change to your steps.

For PostgreSQL:

Terminal window
psql "$IRONFLOW_DATABASE_URL" -c "SELECT id, org_id, name, effect, actions, resources, condition FROM policies ORDER BY org_id, name;"

For SQLite, run the same SQL with sqlite3 on the database file of the engine.

The column org_id shows the owner. Rows with org_platform belong to the platform organization. They become the platform policy set.

After you have written the Rego set:

DELETE FROM policies;

For PostgreSQL, the rows of role_policies and policy_versions go with the policies (cascade). On SQLite the upgrade drops the tables itself.

A CEL policy had four parts that decide a match: actions, resources, a condition, and the effect deny. In Rego, one deny rule holds all of them. The IRN library has the matchers for the first two:

CEL policy fieldRego
actions: "secrets:manage,apikeys:manage"irn.match_action("secrets:manage,apikeys:manage", input.request.action)
resources: "irn:ironflow:*:*:*:env_default_prod:*"irn.match_resource("irn:ironflow:*:*:*:env_default_prod:*", input.request.resource)
condition: an expressionThe same logic as lines in the rule body.
Empty condition (always true)No extra line.
nameThe text of the reason.

Here is a whole policy. The CEL policy denied destructive actions in production, with no condition:

actions: secrets:manage,apikeys:manage
resources: irn:ironflow:*:*:*:env_default_prod:*
condition: (empty)

The Rego rule:

package ironflow.authz
import data.ironflow.lib.irn
deny contains "no destructive operations in production" if {
irn.match_action("secrets:manage,apikeys:manage", input.request.action)
irn.match_resource("irn:ironflow:*:*:*:env_default_prod:*", input.request.resource)
}

Keep these differences in mind:

  • Missing data. In CEL, a missing key was an error, and the error denied the request. In Rego, a missing key makes the expression undefined. A rule body with an undefined expression does not fire. not on an undefined expression is true. Write the rule so the case you want to deny is explicit. The fields of the native view are always present, with empty strings or empty lists as defaults.
  • Time windows. CEL policies stored valid_from and valid_until, but the engine never read them. Rego can express a window. See the change-freeze example in Debug a deny.
  • Operators. && becomes separate lines in one rule body. || becomes separate deny rules. !x becomes not x. x.startsWith(y) becomes startswith(x, y). x.endsWith(y) becomes endswith(x, y). size(x) becomes count(x). y in list stays y in list.
  • Reasons. CEL gave no reason. A Rego deny member is the reason in the audit row. Write a text that tells a person what to do.

Each pair below was run on both engines in a test. The Rego version denies exactly the inputs that the CEL version denied.

subject.user_email.endsWith("@contractor.test")
package ironflow.authz
deny contains "contractors are read-only" if endswith(input.subject.user_email, "@contractor.test")
request.environment == "env_prod"
package ironflow.authz
deny contains "no production" if input.request.environment == "env_prod"
"member" in subject.roles
package ironflow.authz
deny contains "members denied" if "member" in input.subject.roles
request.action.startsWith("secrets:")
package ironflow.authz
deny contains "no secrets" if startswith(input.request.action, "secrets:")
subject.org != request.org_id
package ironflow.authz
deny contains "cross-org" if input.subject.org != input.request.org_id

A stored set that uses a banned builtin, or a sprintf with a computed format, becomes a compile error after an upgrade that bans more. Then the engine denies the requests of that organization in enforce mode. Before you upgrade, run ironflow policy validate on the files of each organization. See Author a policy set.