Skip to content

Manage policy versions and roll back

Every policy save creates a new row in policy_versions containing a full snapshot of the policy at that moment (decision S7 — full-fidelity v1, no diff-only optimization). Rollback is a fresh save with the snapshot’s fields, producing version+1 rather than rewriting an existing row. This means the version timeline is monotonic and every audit row’s policy_version always resolves.

Two common cases:

  1. You shipped a wrong policy. The latest version denies more than you meant. The right move is roll back to the last known-good version, then edit forward from there once you understand the fix.
  2. A planned change needs reversion. A policy you put live for a maintenance window expired wrong (e.g., valid_until typo) and is still firing. Rolling back to the pre-change version restores the intended state.
Terminal window
ironflow policy versions list <policy_id>

Output:

VERSION NAME EFFECT CHANGE CREATED
1 deny-prod-invoke-non-oncall deny initial — oncall-only prod invoke 2026-04-12T10:14:02Z
2 deny-prod-invoke-non-oncall deny widen to staging 2026-04-19T14:30:11Z
3 deny-prod-invoke-non-oncall deny narrow back to prod 2026-04-22T09:11:48Z
4 deny-prod-invoke-non-oncall deny allow incident-responder role too 2026-05-07T08:00:00Z

Audit rows from before 2026-05-07 cite policy_version: 3; rows from after cite version 4.

Before rolling back, confirm the target version is the one you want by listing all versions and checking the row with the matching version number:

Terminal window
ironflow policy versions list <policy_id>

This is also useful for replaying old audit decisions — see debug-deny.md.

Terminal window
ironflow policy rollback <policy_id> 3

What this does:

  1. Reads version 3’s snapshot from policy_versions.
  2. Runs the three save preflights (saver, per-admin, role-only) against the snapshot.
  3. Saves the snapshot as a new version (append-only history).
  4. Bumps the tenant epoch in NATS KV; in-memory caches across the cluster invalidate on next lookup.

The previous versions stay in history. If you need to re-roll-back, ironflow policy rollback <policy_id> 5 is just another rollback — there’s nothing special about “the last rollback.”

Same as a normal save:

Terminal window
# Confirm the rollback row is now latest
ironflow policy get <policy_id>
  • Rollback target was created before a schema migration. A version 1 saved before valid_from/valid_until were added will rollback as a snapshot with those fields null (the column defaults). This is the intended behavior — a policy that didn’t have a window before still doesn’t. If you want to add a window post-rollback, save a forward edit (version 6) on top.
  • Audit-row replay across rollback. An audit row from version 4 cites policy_version: 4. After rolling back to version 3 (saved as version 5), that audit row still resolves to version 4 in policy_versions. The version is part of the audit, not part of the live policy.
  • Bulk rollback (multiple policies). No native multi-policy rollback in v1. Script the loop:
    Terminal window
    for id in $(ironflow policy list --json | jq -r '.[].id'); do
    ironflow policy rollback "$id" "$(ironflow policy versions list "$id" --json | jq -r '.[-2].version_num // empty')"
    done
    …but think first. A bulk rollback is rarely the right move during an incident — usually one policy is the cause.
  • Not undo for audit rows. Rolling back a policy does not delete or modify any policy_decisions row. Audit history is immutable.
  • Not a chain repair tool. If the audit chain shows corruption, that’s a tampered chain and a security incident — rollback won’t fix it. Open an incident; do not attempt to overwrite audit rows.
  • Not an export. If you want to ship a known-good version to another tenant, use the template bundle path — see the dashboard “Templates” workflow (UI-8).
  • Debugging a deny that fired against an old version: debug-deny.md
  • Break-glass when rollback itself is denied: emergency-bypass.md
  • Architectural rationale for full-fidelity snapshots: ADR 0016, decision S7