Skip to main content

Policy Versioning

Every time you update a policy’s code, Formal automatically creates a new version. This gives you a full audit trail and the ability to roll back to any previous version.

How Versioning Works

  • Each policy starts at version 1 on creation.
  • Every code update increments the version number.
  • All versions are stored permanently for compliance and auditing.
  • Version history includes the code and timestamp of each change.

Viewing Version History

  1. Navigate to Policies
  2. Select the policy
  3. Open the Version History tab
  4. Browse previous versions with their timestamps and code
We recommend storing your policy code in version control (e.g., Git) alongside your Terraform configuration. This gives you both Formal’s built-in version history and your team’s standard code review workflow.

Rolling Back to a Previous Version

You can restore any previous version of a policy:
  1. Navigate to the policy in the web console
  2. Open Version History
  3. Select the version you want to restore
  4. Click Restore to apply that version’s code
The restored version becomes a new version (the version number increments), preserving the full history. For example, restoring version 3 when the current version is 5 creates version 6 with version 3’s code.
For critical rollbacks, you can also set the policy to draft or dry-run first, restore the version, verify it works as expected, then re-activate.

Testing Policies

Formal provides several ways to test policies before they affect production traffic.

1. Syntax Validation

Formal validates your Rego code before saving. If there’s a syntax error, the policy cannot be created or updated, and you’ll receive a detailed error message. Common validation errors:
ErrorCauseFix
rego_parse_errorInvalid Rego syntaxCheck for missing brackets, commas, or keywords
rego_compile_errorUndefined variable or functionEnsure all referenced variables exist in the policy scope
Wrong package namePackage is not formal.v2Use package formal.v2 as the first line
Empty modulePolicy contains only the package declarationAdd at least one rule (session, pre_request, or post_request)

2. Dry-Run Mode

Deploy a policy in dry-run mode to see what it would do without actually enforcing it:
  1. Set the policy status to Dry-run
  2. Monitor the Logs page for policy trigger events
  3. Dry-run events show what action the policy would have taken (block, mask, filter, etc.)
  4. Once satisfied, switch to Active
Dry-run policies are logged with full context, including the action that would have been taken and the reason. Use the Logs page to filter by policy name.

3. Impact Reports (Backtesting)

Test a policy against historical log data to understand its impact before activating it. Impact reports evaluate your policy code against real past traffic.
  1. Navigate to Policies
  2. Select a policy or create a new one
  3. Click Impact Report
  4. Choose the time window to backtest against (up to 31 days)
  5. Review the results: which queries would have been affected, and what actions would have been taken
Impact reports require Policy Evaluation Input Retention to be enabled. Without retained inputs, there is no historical data to backtest against.

4. Stage-Based Rollout

You can disable policy evaluation at specific stages (session, pre-request, or post-request) for a particular resource or connector. This lets you roll out a policy one stage at a time:
  1. Deploy the policy in active mode
  2. Disable evaluation at the stages you’re not ready to test yet
  3. Verify the enabled stage works correctly
  4. Gradually enable the remaining stages
This is useful when a policy has logic across multiple stages and you want to validate each independently.

5. Local Testing with Desktop App

The Formal Desktop App lets you test policy evaluation locally. Connect to a resource through the Desktop App and run queries to see how policies evaluate in real time.
  1. Write the policy code
  2. Validate syntax (automatic on save)
  3. Backtest with an impact report against historical data
  4. Dry-run against live traffic
  5. Review logs for unexpected triggers
  6. Activate when confident

Policy Suspensions

Temporarily suspend a policy for specific users without deactivating it entirely.

Time-Based Suspension

Grant a user temporary exemption from a policy for a defined duration (default: 24 hours). This is useful for break-glass scenarios where a user needs emergency access.

One-Off Suspension

Grant a single-use exemption that expires after one matching request. This is useful for controlled exceptions — for example, allowing a single DELETE statement that would otherwise be blocked.
Policy suspensions can be integrated with approval workflows. See Workflows for examples of Slack-based suspension request flows.

Performance Considerations

How Policy Evaluation Works

  • Policies are evaluated in parallel across a worker pool sized to the number of CPU cores on the Connector.
  • Each policy is evaluated independently with its own context, so policies cannot interfere with each other.
  • Results are merged after all policies complete, following the conflict resolution rules.

Writing Performant Policies

Avoid deeply nested iterations or complex comprehensions over large datasets. Rego is optimized for set-based operations — prefer in checks over loops where possible.
# Preferred: set membership check
session := {"action": "allow"} if {
  "admin" in input.user.groups
}

# Avoid: iterating and comparing
session := {"action": "allow"} if {
  some group in input.user.groups
  group == "admin"
}
Use included_connectors or included_resources to limit which traffic a policy evaluates against. Unscoped policies run on every request across all resources.
# Only evaluate for production databases
included_resources := ["prod-postgres", "prod-mysql"]
Choose the most efficient stage for your use case:
  • Session: Cheapest — evaluated once per connection
  • Pre-request: Evaluated per query, but before data is fetched
  • Post-request: Most expensive — evaluated per query after data returns, with access to row/column data
If you can enforce a rule at the session stage, prefer that over pre-request or post-request.
Data from Policy Data Loaders is fetched and loaded into the Connector’s OPA context. Large datasets increase memory usage. Keep external data sets focused and small.

Scaling the Connector

If policy evaluation latency becomes a concern:
  • Scale vertically: More CPU cores = more parallel policy evaluation workers.
  • Scale horizontally: Add more Connector instances with clustering enabled.
  • Monitor metrics: Use the Connector’s health metrics to track evaluation latency.

Troubleshooting

Policy Not Triggering

Ensure the policy is in active (or dry-run) status. Policies in draft status are never evaluated.
Verify your rule uses the correct stage name:
  • session — connection-time rules
  • pre_request — before query execution
  • post_request — after data returns
A post_request rule won’t fire on connection, and a session rule won’t have access to query data.
If the policy uses included_resources, excluded_resources, included_connectors, or excluded_connectors, verify that the target resource or connector is in scope.
Use dry-run mode or impact reports to verify what input data the policy receives. Common issues:
  • input.user.groups may be empty if the user isn’t assigned to any groups
  • input.resource.environment may not be set if the resource has no environment tag
  • input.query.statement_type values are uppercase (e.g., "SELECT", "DELETE")
Another policy may be overriding yours. Remember: block wins over allow, and higher privacy masking wins. Check the Logs page for which policies were evaluated on a given request.

Policy Blocking Unexpectedly

If you have a default session := {"action": "block", ...} policy, it applies to all connections unless an explicit allow rule matches. Verify the allow conditions are correct.
When multiple policies apply, block always wins over allow. If any active policy returns block, the request is blocked regardless of other policies returning allow.
  1. Set the problematic policy to dry-run
  2. Check the logs to see when it triggers
  3. Review the input data to understand why the conditions matched
  4. Adjust the policy logic and repeat
If a policy is blocking critical operations, use a policy suspension to temporarily exempt specific users while you investigate.

Masking Not Working as Expected

Masking policies that use col.data_label require data discovery to have classified the columns. If labels aren’t assigned, the masking condition won’t match.
When matching by column path, use the full path format: database.schema.table.column (e.g., main.public.users.email).
If multiple masking policies target the same column, the one with the highest privacy level wins. See Masking Types for the privacy level hierarchy.
If masking results in type errors in your application, set "typesafe": true with an appropriate "typesafe_fallback" ("fallback_to_null" or "fallback_to_default") to maintain column data types.

Common Rego Mistakes

MistakeSymptomFix
Wrong package namePolicy never evaluatesUse package formal.v2
Missing import future.keywords.ifRego parse errorAdd the import statement
Using = instead of ==Assignment instead of comparisonUse == for equality checks inside rule bodies
input.query in session stageUndefined variable errorQuery data is only available in pre-request and post-request stages
Case-sensitive comparisonsPolicy doesn’t matchSQL statement types are uppercase ("SELECT", not "select")
not without parentheses on complex expressionsUnexpected evaluationUse not (expr) for compound negations