Write custom policies with GitOps
Add your own Open Policy Agent (OPA) Rego code to the policy that Permit.io generates, for rules that the Policy Editor can't express. This guide is for developers who have connected a Git repository to Permit and want to extend the generated policy. The worked example is a deny rule: the tmp-admin role grants access only inside a set time window, even when other policies allow the action.
Prerequisites
- A Git repository connected to a Permit project. See Connect a GitHub repository to Permit.
- An Edge PDP for the environment you change. The Cloud PDP doesn't run custom Rego. See Cloud PDP capabilities.
- A role with the key
tmp-adminin the environment, if you follow the example as written. - Basic knowledge of Rego. See the OPA policy language documentation.
Why you need custom Rego for a deny rule
The policies that Permit generates for role-based, attribute-based, and relationship-based access control (RBAC, ABAC, and ReBAC) are all allow rules. A permission check returns allow: true when at least one of the policies allows the action.
The Policy Editor has no rule of the form "deny access when this condition matches, regardless of other policies". To add one, you write the condition in Rego and change how the generated root policy combines the results.
How the policy repository is organized
Permit generates Rego code for OPA. Each environment has a branch in your repository, named permit/generated/<env_id>, and each branch has this layout:
| Path | Package | Contents |
|---|---|---|
root.rego | permit.root | The entry point. Combines the generated policies and the custom package into the final allow decision. |
permit/, rbac.rego, abac.rego, and other top-level files | permit.policies, permit.rbac, permit.abac, and others | Code that Permit generates from your dashboard configuration. |
custom/root.rego | permit.custom | Your custom Rego. Permit creates the file with an empty rule set. |
Permit regenerates the files outside the custom folder when you change the policy. Put your code in the custom folder so a policy change doesn't overwrite it.
The PDP can also run AWS Cedar as its engine. See Policy engines. This guide covers Rego only.
Start from the custom package file
Check out the environment branch you want to change and open custom/root.rego. The file starts with the package declaration:
package permit.custom
The generated root.rego imports the permit.custom package, so the PDP evaluates your rules together with the generated policy.
The file then sets the default value of the custom allow rule:
default allow := false
With default allow := false, the custom allow rule is false unless one of your allow rules matches.
By default, the generated root.rego allows access when the generated policies allow it or when the custom allow rule is true. A deny rule needs an "and" instead. You change the combination in Enforce the deny rule.
Write custom Rego in the syntax the PDP accepts
The PDP runs OPA with the --v0-compatible flag, which the PDP sets through OPAL_INLINE_OPA_CONFIG='{"v0_compatible": true}' in the PDP Dockerfile. Write your Rego in the pre-1.0 form that OPA's v0 mode parses:
| Rule | Form the PDP accepts | Form the PDP rejects |
|---|---|---|
| Rule body | allow { ... }, with no keyword before the body | allow if { ... } |
The in operator | import future.keywords.in, then "rbac" in policies.__allow_sources | "rbac" in policies.__allow_sources with no import |
A module written in OPA 1.0 syntax fails to parse in v0 mode, with the error rego_parse_error: 'if' keyword is required before rule body. To check a file before you push it, install the OPA command-line tool and run opa check --v0-compatible custom/root.rego.
Write the deny rule
The custom allow rule for the example is true in three cases. Each case is its own rule, and Rego combines rules with the same name with "or", so the custom allow rule is true when any of the three matches:
- The decision doesn't come from RBAC.
- RBAC allowed the request, but the
tmp-adminrole wasn't one of the allowing roles. - The
tmp-adminrole allowed the request, and the current time is inside the window.
Add all three rules to custom/root.rego, in any order:
-
Allow decisions that don't come from RBAC. Import the generated
permit.policiespackage after the package declaration:import data.permit.policiesThe
policies.__allow_sourcesset lists which generated policies allowed the request, such as"rbac"or"abac". Add a rule afterdefault allow := falsethat istruewhen RBAC isn't one of them:allow {not "rbac" in policies.__allow_sources}The
inoperator needsimport future.keywords.inin OPA's v0 mode. Add that import above thepermitimports. -
Allow RBAC decisions that don't use the
tmp-adminrole. Import the generatedpermit.rbacpackage:import data.permit.rbacThe
rbac.allowing_rolesset lists the roles that allowed the request. Add a secondallowrule that istruewhen RBAC allowed the request andtmp-adminisn't one of the allowing roles:allow {"rbac" in policies.__allow_sourcesnot "tmp-admin" in rbac.allowing_roles} -
Allow the
tmp-adminrole only inside the time window. Add two rules that hold the start and end of the window in nanoseconds, and a thirdallowrule that compares the current time with them:# Replace both timestamps with the start and end of your window.window_start_ns := time.parse_rfc3339_ns("2026-01-01T00:00:00Z")window_end_ns := time.parse_rfc3339_ns("2027-01-01T00:00:00Z")allow {"tmp-admin" in rbac.allowing_rolestime.now_ns() >= window_start_nstime.now_ns() <= window_end_ns}The two timestamps in the example are placeholders. Replace them with the RFC 3339 start and end of your own window before you push the file. Once
window_end_nshas passed, every request whose only allowing role istmp-adminis denied, so a stale window silently removes that role's access. Instead of fixed strings, you can compare against user or resource attributes from theinputdocument, so the window comes from your data instead of the policy file.
The complete custom/root.rego file:
package permit.custom
import future.keywords.in
import data.permit.policies
import data.permit.rbac
default allow := false
# Replace both timestamps with the start and end of your window.
window_start_ns := time.parse_rfc3339_ns("2026-01-01T00:00:00Z")
window_end_ns := time.parse_rfc3339_ns("2027-01-01T00:00:00Z")
# The decision does not come from RBAC.
allow {
not "rbac" in policies.__allow_sources
}
# RBAC allowed the request, but not through the tmp-admin role.
allow {
"rbac" in policies.__allow_sources
not "tmp-admin" in rbac.allowing_roles
}
# The tmp-admin role allowed the request, and the current time is inside the window.
allow {
"tmp-admin" in rbac.allowing_roles
time.now_ns() >= window_start_ns
time.now_ns() <= window_end_ns
}
Enforce the deny rule
The generated root.rego in the root of the branch has two allow rules. In Rego, two rules with the same name combine with "or", so access is allowed when either rule is true:
allow {
policies.allow
}
allow {
custom.allow
}
Replace both rules with one rule that requires both conditions:
allow {
policies.allow
custom.allow
}
After the change, access is allowed only when the generated policies allow the request and the custom allow rule is true. The comments that Permit generates in root.rego describe the same change.
After the change, the custom allow rule must be true for every request you want to allow. If custom/root.rego has only default allow := false and no matching allow rule, the PDP denies every permission check in the environment.
Re-apply the root.rego change after a policy change
root.rego sits outside the custom folder, so Permit regenerates it when you change the policy in the dashboard, and the regenerated file can overwrite the combined rule. Combining the allow rules is the one edit outside custom that Test the connection with a custom policy sanctions, and Permit's generated comments in root.rego name it. After every policy change in the dashboard, check the environment branch and re-apply the combined rule if it is gone. If the combined rule disappears, the generated policies and the custom rule go back to combining with "or", and requests that only the custom rule denies are allowed again.
Test the custom policy
- Run
opa check --v0-compatible custom/root.rego root.rego. The command prints nothing when both files parse. - Commit your changes to
custom/root.regoandroot.rego, and push the environment branchpermit/generated/<env_id>. Permit detects the push and sends the updated policy to the PDPs of the environment. - Run a permission check for a user whose only allowing role is
tmp-admin. Outside the time window, the check returnsallow: false. Inside the window, the check returnsallow: true. - Run a permission check for a user with another role that allows the action. The check returns
allow: trueat any time.
The audit log shows detailed decision reasons for the generated policies. For custom Rego, the audit log shows only the result. To debug a custom rule, add a Rego print call to it. For example, add this line inside the first allow rule to write the allowing policies of each evaluation to the PDP logs:
print(policies.__allow_sources)
Add more custom rules
You can add more files and folders in the custom folder. Connect them to the decision in one of two ways:
- Reference them from
custom/root.rego. Import each package incustom/root.regoand use its rules in the customallowrule.custom/root.regostays the one custom entry point. - Reference them from the top-level
root.rego. Import each package inroot.regoand add its rule to anallowrule: as its own rule for an "or" effect, or inside an existing rule for an "and" effect. Permit regeneratesroot.rego, so re-apply these edits after a policy change, as Re-apply the root.rego change after a policy change describes.
Get help with custom policies
Custom Rego changes how the PDP decides every permission check in the environment. A mistake can deny all requests or allow requests that your policy should deny. Test custom rules in a development environment before you merge them into a production environment's branch.
To model a rule without custom code, or for help with a custom policy, ask in the Permit Slack community or book a demo.