- Security & Policies
- Upgrade from CEL policies to a Rego policy set
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.
What changes
Section titled “What changes”- The engine refuses to upgrade while any CEL policy row exists. Platform policy rows count.
- The upgrade drops the tables
policies,policy_versions,policy_templatesandrole_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-setand/api/v1/platform/policy-set. - The CLI commands for CEL policies and templates (
ironflow policy create,list,get,update,delete,test,templateandversions list) are gone. So are theironflow rolecommands that attach a policy to a role, and the--policy-idsflag ofironflow platform roles create.ironflow policynow manages the policy set. - The SDK methods for CEL policies and role-policy links are gone. The SDKs have
policySetmethods. 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).
The error that you will see
Section titled “The error that you will see”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 againThe engine does not change the database when it stops. You can go back to the old binary.
The order of steps
Section titled “The order of steps”The policy-set API exists only after the upgrade. So you prepare the Rego set first, and push it second.
- List the CEL rows. Run the SQL below on the old version.
- Write the Rego set and test it. Write the files in a local directory. Run
opa testthere. - Delete the CEL rows. Save the old rows first if you want a record. The version history is dropped by the upgrade.
- Upgrade. Start the new binary. The migration passes.
- Push the set.
ironflow policy pull ./p, copy your files into./p,ironflow policy validate ./p, thenironflow policy push ./p. The set starts inmonitormode. - Read the would-deny rows. See Roll out with monitor mode.
- 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.
List the rows
Section titled “List the rows”For PostgreSQL:
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.
Delete the rows
Section titled “Delete the rows”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.
How a CEL policy maps to Rego
Section titled “How a CEL policy maps to Rego”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 field | Rego |
|---|---|
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 expression | The same logic as lines in the rule body. |
Empty condition (always true) | No extra line. |
name | The text of the reason. |
Here is a whole policy. The CEL policy denied destructive actions in production, with no condition:
actions: secrets:manage,apikeys:manageresources: 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.
noton 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_fromandvalid_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 separatedenyrules.!xbecomesnot x.x.startsWith(y)becomesstartswith(x, y).x.endsWith(y)becomesendswith(x, y).size(x)becomescount(x).y in liststaysy in list. - Reasons. CEL gave no reason. A Rego
denymember is the reason in the audit row. Write a text that tells a person what to do.
Patterns and their Rego versions
Section titled “Patterns and their Rego versions”Each pair below was run on both engines in a test. The Rego version denies exactly the inputs that the CEL version denied.
Email domain
Section titled “Email domain”subject.user_email.endsWith("@contractor.test")package ironflow.authz
deny contains "contractors are read-only" if endswith(input.subject.user_email, "@contractor.test")One environment
Section titled “One environment”request.environment == "env_prod"package ironflow.authz
deny contains "no production" if input.request.environment == "env_prod"A role
Section titled “A role”"member" in subject.rolespackage ironflow.authz
deny contains "members denied" if "member" in input.subject.rolesAn action prefix
Section titled “An action prefix”request.action.startsWith("secrets:")package ironflow.authz
deny contains "no secrets" if startswith(input.request.action, "secrets:")A caller from another organization
Section titled “A caller from another organization”subject.org != request.org_idpackage ironflow.authz
deny contains "cross-org" if input.subject.org != input.request.org_idBefore the next upgrade
Section titled “Before the next upgrade”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.