When authorisation rules are scattered through application code, nobody can say with confidence who is allowed to do what, and every change is a code release with no independent review. Policy-as-code moves those rules into a dedicated policy language, evaluated by an engine the application calls, stored in version control and tested like software. This guide covers the two most widely used open source options, Open Policy Agent (OPA) with its Rego language and the Cedar policy language, and the Kubernetes admission tools built around them. It explains how each evaluates a decision, how to choose between them, how to build a tested and signed policy pipeline, how to run the engine safely and how to measure whether it is working. It is written for platform engineers, application security teams and identity architects. For the models the policies express (RBAC, ABAC, ReBAC), see the Authorisation Models Guide.
Key takeaways
- Separate the decision from the enforcement. The application or proxy asks "is this allowed?" and enforces the answer; the policy engine decides. That split is what makes rules reviewable, testable and consistent across services.
- Rego and Cedar make different trade-offs. Rego is a general-purpose query language over arbitrary JSON, used from Kubernetes admission to API gateways; Cedar is a narrower language built for application permissions, with deny-by-default, forbid-overrides-permit and schema validation built in.
- Default deny has to be designed in. Cedar denies when nothing permits. In Rego, an undefined result is not "false" unless you declare
default allow := falseand the enforcement point treats anything else as deny. - The policy pipeline is a privileged path. Whoever can change a bundle or policy store can grant access, so require review, tests, signing and signature enforcement before policies reach production.
- Decision logs are the audit trail. Logging every decision with its input, result and policy revision answers "why was this allowed?", but mask secrets and personal data before logs leave the engine.
What policy-as-code for authorisation means
Authorisation architectures describe two roles. A policy enforcement point (PEP) sits in the request path, such as an API gateway, service mesh proxy, Kubernetes API server or application middleware. A policy decision point (PDP) evaluates the request against policy and returns a decision. NIST SP 800-207 uses this vocabulary for zero trust, and it treats authentication and authorisation as discrete steps performed before a session with a resource is established. Policy-as-code makes the PDP's rules a versioned artefact.
OPA's documentation describes the benefit as decoupling: policy owners can read, write, analyse, version and distribute policy separately from the service, and the service offloads decisions by sending a query. In practice policy-as-code covers two different jobs, and mixing them causes confusion:
- Runtime authorisation: per-request decisions such as "can this user approve this invoice" or "can this workload call this endpoint". Latency, availability and data freshness matter.
- Configuration guardrails: decisions about whether a Kubernetes object, Terraform plan or cloud resource is acceptable. These run at admission time or in CI. See the Infrastructure as Code Security Guide and the Kubernetes Admission Control Guide.
The same engine can do both, but the policies, owners, data sources and failure modes differ. This guide focuses on authorisation and touches guardrails where the tooling overlaps.
OPA and Rego
OPA is a general-purpose policy engine and a Cloud Native Computing Foundation Graduated project. It can run as a sidecar, a host-level daemon or a library embedded in the service.
- Input and data: the caller sends a JSON query document, available to policies as
input. Reference data loaded in advance (role mappings, resource owners) sits underdataand is cached in memory, which is why evaluation is fast and why stale data is a risk. - Decisions are structured data: a decision can be a boolean, or an object carrying reasons, obligations or headers. The OPA-Envoy plugin, for example, accepts either a boolean or an object with a required
allowedfield plus optional headers, body and HTTP status, and defaults to 403 on denial. - Rules combine as OR: several rules with the same name are unioned, so adding an
allowrule can only widen access. A complete rule that produces two different values for the same input is a runtime conflict error. - Undefined is not false: if no rule body matches, the rule is undefined. The
defaultkeyword supplies the fallback value. - OPA 1.0 syntax: Rego v1 makes the
ifandcontainskeywords mandatory in rule heads, reservesinputanddataas names, and removes deprecated built-ins such asre_matchandnet.cidr_overlap. Useopa check --v0-v1andopa fmt --write --v0-v1to migrate older policy, and the Regal linter in CI.
A minimal deny-by-default rule in current syntax, adapted from the shape used in the OPA documentation:
package app.invoices
default allow := false
allow if {
input.action == "approve"
input.user.department == input.resource.department
input.user.id != input.resource.created_by
}
The third condition expresses separation of duties: nobody approves an invoice they created. Writing that once in policy, rather than in each service that handles invoices, is the core value of the approach.
Cedar
Cedar is an open source language designed specifically for application authorisation. Amazon Verified Permissions is a managed service that evaluates Cedar policies (the AWS documentation currently cites Cedar 4.7), but Cedar itself runs anywhere its libraries do.
- Structure: each policy has an effect (
permitorforbid), a scope overprincipal,actionandresource(matching anything, one entity with==, a hierarchy within, or a type withis), and optionalwhenandunlessconditions over attributes and requestcontext. - Evaluation: if any forbid policy is satisfied, the answer is Deny; otherwise if any permit is satisfied, Allow; otherwise Deny. Forbid policies therefore act as guardrails that later permit policies cannot cross.
- Skip on error: a policy that errors during evaluation is ignored rather than denying everything. That is safe for permits, but a forbid that errors (for example by reading a missing attribute) silently stops guarding. Validation and tests must catch this.
- Diagnostics: every response lists the determining policies (the satisfied permits on Allow, the satisfied forbids on an explicit Deny) and any policy errors, which makes decisions explainable.
- Schema and validation: a schema declares entity types, parent types, attributes (required unless marked optional) and, for each action, the principal and resource types it applies to and its context shape. Validating policies against it catches misspelt attributes and wrong entity references before deployment. A schema change can invalidate existing policies, so validate on every schema change too.
- Templates: templates use
?principaland?resourceslots, only in the scope, to create linked policies such as "share this album with this group". Changing a template changes every linked policy, and linked policies tie the policy store to user lifecycle: offboarding means deleting or archiving them.
@id("finance-approve")
permit (
principal in Role::"FinanceApprover",
action == Action::"approve",
resource is Invoice
)
when { resource.department == principal.department };
@id("no-self-approval")
forbid (principal, action == Action::"approve", resource is Invoice)
when { resource.created_by == principal };
The same separation-of-duties rule becomes an explicit forbid, which no future permit can override.
Comparing the engines and languages
| Aspect | OPA with Rego | Cedar | Gatekeeper (OPA for Kubernetes) | Kubernetes ValidatingAdmissionPolicy |
|---|---|---|---|---|
| Primary use | Any JSON decision: APIs, microservices, admission, CI checks | Application permissions: principal, action, resource | Kubernetes admission and audit of existing objects | Kubernetes admission, in-process in the API server |
| Default outcome | Undefined unless a default is set | Deny unless permitted | Allowed unless a constraint reports a violation | Allowed unless a validation fails |
| Conflict handling | Rules OR together; you write the combining logic | Forbid overrides permit | Any violation denies (per enforcement action) | A failed CEL validation rejects the request |
| Type checking | Optional JSON schema annotations | Schema validation built in | Parameter schema in the ConstraintTemplate | CEL type checking |
| Safe rollout mode | Decision logs; OPA-Envoy dry-run | Run in shadow beside existing checks | enforcementAction: dryrun or warn (deny is the default) | Test in a non-production cluster first |
| Where it runs | Sidecar, daemon, library, Wasm | Embedded library or managed service | Validating admission webhook | Kubernetes API server (stable since v1.30) |
| Learning curve | Steeper: Datalog-style queries, iteration with some and every | Lower: deliberately restricted language | Rego plus Kubernetes object model | CEL expressions |
Choosing an approach
| If you need | Lean towards | Watch for |
|---|---|---|
| Fine-grained permissions inside a product (documents, tenants, sharing) | Cedar, self-hosted or managed | Entity data must be supplied with each request or loaded; template sprawl |
| One engine for gateway, mesh, Kubernetes and CI | OPA with Rego | Rego skills concentrated in a few people; write review guidelines |
| Simple Kubernetes guardrails without running a webhook | ValidatingAdmissionPolicy with CEL | Fewer reuse and audit features than Gatekeeper for large libraries |
| Kubernetes policy library with audit of existing resources | Gatekeeper | Webhook availability and its failure behaviour |
| Relationship-heavy access (who shares what with whom at scale) | A relationship-based (ReBAC) service, possibly alongside Cedar or OPA | Policy engines evaluate data they are given; they are not graph stores |
Small organisations usually gain most by externalising one high-risk decision (admin actions, payments, data export) rather than building a platform. Large organisations need a central policy team that owns libraries, tooling and the pipeline, with application teams owning their own rules. For product evaluation, see the Externalised Authorisation Buyer's Guide.
Building the policy pipeline
A policy-as-code gate is only as strong as the path policies take to production. Treat it like any production deployment pipeline, with one difference: a merged policy can grant access directly.
- Repository and ownership: one repository or directory per policy domain, CODEOWNERS for review, and branch protection that requires approval from the owning team plus security for changes to forbid or admin rules.
- Tests:
opa testruns rules prefixedtest_, can mock input and data with thewithkeyword, and supports--coverage,--format=jsonand--fail-on-empty(which catches a test suite that silently ran nothing). For Cedar, validate every policy against the schema and run request-and-expected-decision suites through the Cedar CLI or SDK. - Test the negative cases: for every permit, write tests for the near misses: wrong tenant, wrong department, missing attribute, the creator approving their own record.
- Bundles and signing:
opa build -bproduces a bundle with a.manifestcarrying arevisionand therootsit owns, and--signing-keyadds a.signatures.jsonJWT with per-file hashes. OPA only enforces signatures when asigningblock is configured; top-levelkeysalone still accepts unsigned bundles. Delta bundles carry no signatures. - Distribution: serve bundles over authenticated HTTPS, use non-overlapping roots when several sources contribute, and protect the persistence directory, because OPA loads persisted bundles at start-up.
- Policy stores: for a managed Cedar service, change the store through infrastructure as code (the AWS documentation lists CloudFormation, CDK, the CLI and APIs), not through console edits.
See the CI/CD Security Guide for protecting the pipeline itself.
Securing the policy engine
An OPA instance holds policy and often sensitive reference data, and its API can change both. The OPA security documentation is explicit about defaults.
- Authentication and authorisation are off by default:
--authentication=offand--authorization=off. Set--authentication=tokenortlsand--authorization=basic, with adata.system.authz.allowpolicy that denies by default, for example allowing onlyPOST /for decision queries. - Bind locally: since OPA 1.0,
opa run --serverbinds to localhost by default. Containers often override this with--addr 0.0.0.0:8181; only do so with TLS and API authorisation in place. - TLS: configure
--tls-cert-fileand--tls-private-key-file, and--tls-ca-cert-filefor client certificates. Serve health and metrics on a separate--diagnostic-addr. - Least privilege for the process: run as non-root, keep credentials out of command-line arguments and restrict token files.
- Fail closed at the enforcement point: decide explicitly what the PEP does when the PDP is unreachable or returns an error, and default to deny for anything other than low-risk reads.
Where identity fits: non-human identities and AI agents
Policy-as-code is increasingly the control point for machine identities. Workload identities from mutual TLS or SPIFFE IDs, OAuth client credentials and agent tokens all become the principal in a decision.
- Use verified identity, not claims in the body: derive the principal from the authenticated token or certificate at the PEP, never from a field the caller controls. OWASP ASVS 5.0 V8.3.1 requires authorisation to be enforced at a trusted service layer.
- Agents need tighter context: include the delegating user, the task and the tool in
context, and forbid high-impact actions without a human approval attribute. See the AI Agent Authorisation Guide. - Service-to-service: mesh proxies calling OPA through Envoy external authorisation give per-call decisions for workloads. See the Service Mesh Security Guide.
Rolling out policy-as-code
- Pick one decision: choose a high-risk, well-understood rule (admin actions, data export, cross-tenant access) and write its current behaviour down as test cases before writing policy.
- Model the inputs: define the request shape or Cedar schema: principal attributes, resource attributes, context. Decide where each attribute comes from and how fresh it must be.
- Write policy and tests together: deny by default, then permits, then forbids for invariants. Gate merges on tests, coverage reporting and schema validation.
- Run in shadow: call the engine alongside the existing in-code check and log both answers, or use OPA-Envoy
dry-runor Gatekeeperdryrun. Investigate every disagreement. - Enforce in rings: switch one service, tenant or namespace at a time; Gatekeeper's
warnaction is a useful middle step for admission policy. - Remove the old check: leaving duplicate in-code logic means two sources of truth that drift.
- Roll back by revision: keep the previous signed bundle or policy store version deployable, and record the bundle revision in every decision log so you know which version made which decision.
Detection and measurement
OPA decision log events record the policy path, input, result, bundle revision, a decision ID and a timestamp. They can be printed to the console or uploaded in compressed batches to a remote service. Before logs leave the engine, a data.system.log.mask rule can erase or rewrite fields under /input or /result; use it for tokens and personal data. Note that max_decisions_per_second drops events above the limit, which matters if logs are your audit evidence. Cedar responses carry determining policies and errors that the application should log alongside its own request ID.
Monitor for:
- Bundle activation failures and signature verification errors (OPA Status API).
- Policy evaluation errors, especially Cedar policies skipped on error.
- Sudden shifts in the allow-to-deny ratio for a policy after a deployment.
- Decisions served by an unexpected bundle revision, which indicates a stale or tampered instance.
- Changes to policy stores or bundle sources outside the pipeline.
Metrics that show it is working:
- Percentage of high-risk authorisation decisions served by the policy engine rather than in-code checks.
- Policy line coverage from
opa test --coverage, and number of negative test cases per permit rule. - Shadow-mode disagreement rate, trending to zero before enforcement.
- Percentage of instances running the current signed revision.
- p99 decision latency at the PEP.
- Mean time from approved policy change to enforcement in production.
Framework mapping
| Framework | Reference | How policy-as-code supports it |
|---|---|---|
| NIST SP 800-53 Rev. 5 | AC-3 Access Enforcement; AC-24 Access Control Decisions; AC-6 Least Privilege | Engine makes decisions; PEP enforces; policies encode least privilege |
| NIST SP 800-53 Rev. 5 | CM-3 Configuration Change Control; SA-11 Developer Testing and Evaluation; AU-2 Event Logging | Reviewed, tested policy changes; decision logs |
| NIST CSF 2.0 | PR.AA-05 access permissions defined in policy, managed, enforced and reviewed; PR.PS-01 configuration management | Policies as reviewed, versioned artefacts |
| NIST SP 800-207 | Policy decision and enforcement points in zero trust architecture | OPA or Cedar as the PDP |
| OWASP ASVS 5.0 | V8.1.1 documented authorisation rules; V8.2.1 function-level access needs explicit permission; V8.3.1 enforcement at a trusted service layer | Policies are the documented rules; tests prove them |
| OWASP API Security Top 10 2023 | API1 Broken Object Level Authorization; API5 Broken Function Level Authorization | Object and function checks centralised and tested |
Common failure patterns
- Undefined treated as allow: a Rego policy without
default allow := false, called by a PEP that only denies on an explicit false, lets every unmatched request through. - Unsigned bundles accepted: verification keys configured but no
signingblock, so a tampered or unsigned bundle activates. Anyone with write access to the bundle server can then grant access. - Exposed OPA API: the server bound to all interfaces with authentication off lets anyone on the network read reference data or push new policy through
PUT /v1/policiesorPUT /v1/data. - Guardrail forbids that error out: a Cedar forbid reading an optional attribute without a
hascheck is skipped on error and stops protecting. Schema validation and a test per forbid prevent it. - Policy that only checks infrastructure, not permissions granted: broad cloud roles pass every pipeline check because no policy inspects what the role can do. The Capital One Breach 2019 shows how far an over-permissioned role reaches once its credentials are taken; a policy gate on IAM permissions in infrastructure code is one of the controls that addresses it.
- Stale data: role or ownership data in
datarefreshed hourly means leavers and moved users keep access until the next sync. Pass time-sensitive attributes in the request or shorten the refresh.
Practitioner checklist
- Inventory where authorisation decisions are made today and pick the first high-risk decision to externalise.
- Choose Rego or Cedar per use case, and document why.
- Enforce deny by default at both the policy and the enforcement point, including on PDP errors and timeouts.
- Express invariants such as separation of duties and tenant isolation as forbid rules or dedicated deny rules.
- Require code review,
opa testor Cedar validation with negative cases, and coverage reporting before merge. - Sign bundles and configure signature enforcement; manage policy stores only through infrastructure as code.
- Turn on OPA API authentication and authorisation, TLS and localhost binding.
- Run new policies in dry-run or shadow mode and resolve every disagreement before enforcing.
- Ship decision logs with policy revision to your SIEM, masking secrets and personal data.
- Derive principals from authenticated tokens or certificates, including for workloads and AI agents.
Standards and references
- Open Policy Agent: Policy Language
- Open Policy Agent: Bundles
- Open Policy Agent: Security
- Cedar: Authorization
- Cedar: Schema
- OPA Gatekeeper: How to use Gatekeeper
- NIST: SP 800-207 Zero Trust Architecture
- OWASP: Authorization Cheat Sheet
Related NHI Mgmt Group resources: Authorisation Models Guide · AI Agent Authorisation Guide · Broken Access Control Prevention Guide · Zero Trust Identity Guide · Compliance Automation Guide