Skip to main content

Deploy and roll back policy

Deploy custom Rego with POST /admin/api/policy-bundle/deploy. It saves your rules, waits for the policy engine to load them, and undoes the save if the engine refuses them. This page shows a deploy, a refused deploy, a rollback, and the single-rule endpoints.

The examples use the reports_read_only.rego rule from Write a policy, and these two values.

export POLICYARC_URL="https://your-environment.example"
export POLICYARC_ADMIN_KEY="paste the Admin API key"

Deploy a rule​

jq -n --rawfile rego reports_read_only.rego \
'{mode: "merge", rules: [{path: "pbac/controls/reports_read_only.rego", content: $rego}]}' |
curl -sS -X POST "$POLICYARC_URL/admin/api/policy-bundle/deploy" \
-H "X-Admin-API-Key: $POLICYARC_ADMIN_KEY" \
-H 'Content-Type: application/json' -d @-

A deploy the engine loaded returns HTTP 200.

{
"mode": "merge",
"rulesUpserted": 1,
"rulesDeleted": 0,
"dataUpserted": 0,
"dataDeleted": 0,
"previousEtag": "\"f4365ca3...\"",
"newEtag": "\"a8bc24de...\"",
"rulesProtected": 0,
"dataProtected": 0,
"revision": "3cc40555...",
"activation": "confirmed"
}

"activation": "confirmed" means the policy engine reported this deploy's revision as the one it is running. Our deploy returned in under two seconds.

activation is unverified when the server runs with PBAC_BUNDLE_CONFIRM_ACTIVATION=false. The server then returns without asking the policy engine, so it does not refuse or roll back a bundle the engine cannot load.

The body takes rules, a list of path and content, and data, a list of dataKey and payload. One request can carry several rules and data rows, and they are saved together.

Use merge, not replace​

mode is merge or replace. Use merge unless you mean to replace everything.

replace deletes every rule and data row that is not in the request. It keeps only the product's bundled rules and its five bundled data rows, such as oauth_config and pbac.operator.segments. Every other row goes, including rows you wrote, the denylist and trusted issuers rows, and rows the server writes for itself, such as pbac.controls.shapes, pbac.resources.registry and pbac.controls.resource_read_only. A replace that leaves out the denylist row removes it, which lifts every denial it held. We sent a replace with one rule and no data, and it deleted 8 data rows.

merge saves what you send and leaves everything else alone.

Read a refused deploy​

When the policy engine cannot load the bundle, the deploy returns HTTP 422 and nothing changes. We sent a rule with a missing closing brace.

{
"error": "bundle_not_activated",
"error_description": "OPA did not activate the deployed bundle (expected revision 0011761776e2..., active 3cc40555f5b8...) — the bundle was rejected; the deploy has been rolled back",
"expectedRevision": "0011761776e2...",
"activeRevision": "3cc40555f5b8...",
"rolledBack": true
}
  • rolledBack: true means your rules and data are what they were before the request. The rule that was live before kept working.
  • error is opa_unreachable instead when the authorization server could not reach the policy engine at all.
  • The refusal took about 10 seconds, because the server waits that long for the engine before it gives up.

The response does not say what is wrong with the Rego. Run opa check on your file before you deploy, and you see the parse error and its line at once.

opa check reports_read_only.rego

Known issue with data rows in a batch​

A batch deploy can return this 422 when nothing is wrong. It happens when a data row's keys, at any depth, are not in the order the database stores them. The server works out the expected revision from the row as you sent it, and the policy engine loads the row as the database returns it, so the two revisions differ. The database orders keys shortest first, and keys of the same length in byte order, so Zz comes before aa.

We deployed a rule with the row {"start_hour": 9, "end_hour": 17}. The policy engine loaded the bundle, and the deploy still waited 10 seconds, returned 422 and rolled back. The same row sent as {"end_hour": 17, "start_hour": 9} deployed in about one second.

Until this is fixed, send keys in the order a GET of the row returns them, or write data rows with their own PUT request and deploy only rules in the batch.

curl -fsS -X PUT "$POLICYARC_URL/admin/api/policy-data/by-key?dataKey=custom.reports_hours" \
-H "X-Admin-API-Key: $POLICYARC_ADMIN_KEY" \
-H 'Content-Type: application/json' \
-d '{"dataKey": "custom.reports_hours", "payload": {"start_hour": 9, "end_hour": 17}}'

A data write is not confirmed the way a deploy is. The policy engine loads it a few seconds later.

Roll back​

The server keeps no history of rule content, so keep your Rego in version control.

To undo a rule, delete it. Find its id by its path, then delete it.

id=$(curl -fsS "$POLICYARC_URL/admin/api/policy-rules" -H "X-Admin-API-Key: $POLICYARC_ADMIN_KEY" |
jq -r '.[] | select(.path == "pbac/controls/reports_read_only.rego") | .id')
curl -sS -o /dev/null -w '%{http_code}\n' -X DELETE "$POLICYARC_URL/admin/api/policy-rules/$id" \
-H "X-Admin-API-Key: $POLICYARC_ADMIN_KEY"

The delete returns 204. It is not confirmed like a deploy. In our run a write token was still refused on the first try and issued about a second later.

To return a rule to an earlier version, deploy that version's file again with merge.

To stop one client at once while you investigate, add it to the denylist control. See Control data.

See what was deployed​

GET /admin/api/bundle-audit lists the bundles the server built, newest first. GET /admin/api/bundle-audit/latest returns the newest one.

curl -fsS "$POLICYARC_URL/admin/api/bundle-audit?size=3" -H "X-Admin-API-Key: $POLICYARC_ADMIN_KEY" |
jq '.[] | {createdAt, triggerSource, ruleCount, etag, previousEtag}'

Each row has etag, previousEtag, ruleCount, rulePaths, dataKeys, bundleSizeBytes, triggerSource, triggeredBy and createdAt. A row records a bundle the server built, not one the engine accepted. Our refused deploy left a row with the broken rule counted, and the rollback left another.

GET /admin/api/bundle-audit/at?timestamp= returns the bundle that was current at an ISO 8601 time. It records rule paths and data keys, not their content.

Single-rule endpoints​

PUT /admin/api/policy-rules/by-path?path=<path> saves one rule and waits for the policy engine like a deploy. We saved a rule with a missing closing brace. The server returned the same 422 bundle_not_activated body with "rolledBack": true after about 10 seconds, and the stored rule was still the version before. A valid rule returned 200 in under two seconds.

Each single-rule save is checked against the bundle as it stands, without the rules you have not saved yet. Deploy rules that depend on each other together with policy-bundle/deploy.

The body of by-path needs version, kind, path and content. A request without one of them returns HTTP 400. We left out version and got this.

{"fieldErrors": {"version": "Version is required"}, "error": "Validation failed"}

POST /admin/api/policy-rules and PUT /admin/api/policy-rules/{id} are confirmed the same way. They take the same body as by-path. Only rules whose version is current reach the policy engine. We saved a rule with "version": "v2". The server returned 201 and the rule never took effect. The batch deploy sets current for you.

Next steps​