Join our Newsletter — 33% off our NHI Course
Home› Guides› Open Policy Agent in Production: Signed Bundles, Safe…
Guide Authorisation

Open Policy Agent in Production: Signed Bundles, Safe Decision Logs, Tested Rego

PDF for newsletter subscribers · free to read online

← All guides
By NHI Mgmt Group, edited by Lalit Choda Updated 12 November 2026 16 min read
On this page

An Open Policy Agent (OPA) instance that answers authorisation queries is a production access control system, and most of its risk sits in the plumbing around the policy rather than in the Rego itself. Whoever controls the bundle server, the discovery bundle or the OPA management API can change who is allowed to do what, and decision logs can quietly copy tokens and personal data into a log pipeline. This guide is the operational follow-on to the Policy-as-Code for Authorisation Guide, which explains why to externalise decisions and how Rego compares with Cedar. Here the focus is running OPA as a policy decision point (PDP): choosing a deployment topology, enforcing bundle signatures, masking and dropping decision log fields, testing Rego with coverage gates, and monitoring the fleet. It is written for platform engineers, application security teams and identity architects who already run, or are about to run, OPA in front of real traffic.

This is an independent guide from NHI Mgmt Group, based on the Open Policy Agent project's public documentation as of November 2026. It is not sponsored or reviewed by the OPA project. Features change, so confirm settings against the current OPA documentation.

Key takeaways

  • Signature enforcement needs a signing block. Top-level keys alone still activates unsigned bundles; only a bundles[_].signing block (or --verification-key for filesystem bundles) makes a missing .signatures.json a failure.
  • Decision logs are a data store, not just telemetry. Each event carries the full input and result, so write a system.log.mask policy before enabling upload, and watch the drop counters so audit gaps are visible.
  • Topology decides your failure mode. A sidecar keeps latency low and failure local; a central PDP simplifies management but turns OPA into a shared dependency that every service must handle failing closed.
  • Tests are a release gate. Run opa test with --fail-on-empty, --coverage and --threshold in CI, and write negative tests for every permit rule.
  • Readiness should mean "policy loaded". Use /health?bundles or a system.health ready rule so traffic never reaches an OPA that has started but has no policy.

Deployment topology: sidecar, host daemon, central service or library

OPA keeps policy and reference data in memory and evaluates locally, which is why the project documents running it close to the service: as a sidecar, a host-level daemon or a Go library. The choice is a security decision because it determines the blast radius of a compromised or unavailable PDP and how many identities can reach the management API.

TopologyStrengthsSecurity considerationsFits
Sidecar per workloadLowest latency, failure contained to one pod, API reachable on localhost onlyMany instances to configure and monitor; each needs bundle service credentials; bundle activation lag differs per podMicroservices and service mesh external authorisation
Host-level daemonFewer instances, one config per nodeEvery process on the host can reach it; API authorisation matters more; a crash affects all tenants on the nodeVM estates, SSH or sudo decisions, node-level agents
Central PDP serviceSingle place to update and observe; easy to share dataNetwork hop on every decision, shared outage, must be exposed beyond localhost with TLS and API authentication, larger targetLow-volume back-office apps, early pilots
Embedded Go library (SDK)No network hop or separate process; manual triggers for bundle and log plugins availablePolicy engine shares the application's memory and identity; upgrades ride the application releaseGo services, CLIs, gateways built in Go

Decision guidance: start with sidecars where you already run a mesh or Kubernetes, because the localhost binding default (since OPA 1.0) and pod isolation give you a small attack surface for free. Choose a central PDP only when decision volume is modest and the calling services can tolerate a network dependency; if you do, treat it like any tier 1 internal API, with mutual TLS, autoscaling and an explicit fail-closed behaviour in every client. Small teams with one or two services can run a central instance safely if it is not exposed outside the cluster. See the Service Mesh Security Guide and the API Gateway Security Guide for the enforcement point side.

Where data comes from

The OPA external data documentation lists options in order of preference: data carried in JWTs, data the caller adds to input, data shipped in bundles, data pushed over the API, and data pulled with http.send at evaluation time. Each moves the trust boundary:

  • Input overloading: the PEP decides what attributes OPA sees, so a compromised or buggy caller can supply favourable attributes. Validate tokens inside policy (for example with io.jwt.decode_verify) rather than trusting pre-decoded claims.
  • Bundles: policy and data arrive together and are consistent, but freshness is bounded by build and polling intervals, and the whole dataset sits in memory.
  • Push: anyone with write access to /v1/data can change decisions, so the management API must be authorised.
  • http.send: data is fresh, but the documentation notes that if the remote service is slow or unavailable OPA may not return a decision, and the built-ins do not retry. Credentials for that call should come from files or the environment, never from policy source.

Bundle integrity: signing, scoping and distribution

Bundles are the documented, preferred way to update policy across many instances. A bundle is a gzipped tarball with a .manifest (revision and roots), policy and data files. Signing adds a .signatures.json containing a JWT that lists every file with its hash. On receipt, OPA verifies the JWT signature, checks that the file list matches exactly, and checks every file hash; only then does it activate the bundle. On failure it keeps the previous bundle and reports an activation error through the status API and logs.

ConfigurationUnsigned bundle arrivesSigned bundle arrives
No keys, no signing blockActivatedFails: no verification key
Top-level keys onlyActivated, warning logged at start-upVerified
bundles[_].signing block, or --verification-keyFails: missing .signatures.jsonVerified

The middle row is the trap: teams configure keys, see signed bundles verify, and assume enforcement is on. An attacker with write access to the bundle server can still publish an unsigned bundle and have it activated. Any signing block, even an empty one, turns enforcement on; keyid only selects the key.

  • Signing in CI: opa build -b with --signing-key signs the bundle; the default algorithm is RS256 and --signing-alg selects another. Use --claims-file to add a scope claim, and set the same scope in the OPA signing block so a bundle signed for one environment is rejected in another.
  • Key custody: keep the private key in a KMS or HSM backed signer in the pipeline, not on developer laptops. OPA supports custom signer and verifier plugins for cloud key services, registered under the same plugin key. See the Secrets Management Guide.
  • Roots: declare roots in each manifest so bundles from different sources cannot overwrite each other's paths; OPA validates scoped bundles on load.
  • Persistence: persist: true lets OPA load the last activated bundle from disk (by default under ./.opa/bundles/) if the bundle server is unreachable at start-up. That improves availability, but the persistence directory becomes part of your trust boundary: mount it read-write only for the OPA user.
  • Delta bundles: delta bundles do not support signing, and an OPA configured to verify signatures rejects them unless allow_unsigned_delta_bundles is set. Leave that option off for authorisation policy.
  • Transport and pull credentials: serve bundles over HTTPS, give each OPA a read-only credential for its own resource path, and load tokens with --set-file or a credentials file, not command-line arguments. OCI registries are also supported as a bundle source.
  • Discovery bundles: a discovery bundle generates the rest of OPA's configuration, including which bundles to download and where logs go. It deserves the same signing (discovery.signing) and change control as policy, because changing it can redirect an entire fleet.

Note that opa eval and opa test do not verify bundle signatures, so a passing CI run proves nothing about what production will accept. Verify the signed artefact in a staging OPA with the production signing configuration. See the CI/CD Security Guide and the Sigstore Guide for pipeline provenance.

Decision logs: privacy, completeness and retention

When decision logging is enabled, each event records the queried path, the full input, the result, the bundle revisions, the requester address, a timestamp and a decision_id that OPA also returns to the caller. Events go to a remote HTTP service (gzipped JSON, default resource /logs), a custom plugin, or the console. That is exactly the audit trail you need to answer "why was this allowed", and exactly the kind of record that leaks bearer tokens, session cookies and personal data if input includes raw request headers.

Masking and dropping

Before encoding and uploading, OPA evaluates data.system.log.mask (configurable with decision_logs.mask_decision). The mask policy receives the whole event as input and returns JSON Pointers to erase, or objects with op of remove or upsert to replace values. Pointers must start with /input, /result or /nd_builtin_cache, and the event records which paths were erased or masked. A minimal policy:

package system.log

mask contains "/input/headers/authorization"
mask contains "/input/headers/cookie"
mask contains {"op": "upsert", "path": "/input/user/email", "value": "**REDACTED**"} if {
    input.input.user.email
}

A separate drop rule (path set by decision_logs.drop_decision, default /system/log/drop) removes whole events, for example high-volume health checks. Use drop sparingly: every dropped allow decision is a decision you cannot reconstruct later. The nd_builtin_cache field records outputs of non-deterministic built-ins such as http.send for replay, so mask it too if those responses carry personal data.

Buffering and loss

SettingDefaultWhat to decide
decision_logs.reporting.buffer_typesizesize or event buffering
buffer_size_limit_events10000 (event buffer)Old events are dropped when exceeded
buffer_size_limit_bytesunlimited (size buffer)Cap memory; mutually exclusive with max_decisions_per_second
max_decisions_per_secondnot setRate limit; excess events are dropped
upload_size_limit_bytes32768Maximum gzipped chunk size
min_delay_seconds / max_delay_seconds300 / 600Upload interval, or use trigger: immediate

Every limit is a deliberate trade between memory and audit completeness. Decide which you prefer per service, then alert on the drop counters described under detection. Ship logs to a store with restricted read access and a retention period your privacy team has approved; see the Privacy in Security Logging and Monitoring Guide. Leave decision_logs.console off in production unless the console stream is itself treated as sensitive.

Hardening the OPA server

The policy-as-code guide covers the core flags; the production points that are most often missed are these:

  • API authorisation policy: with --authorization=basic, OPA evaluates data.system.authz.allow for every API call. The documented hardened example allows only POST / (the default decision) and denies everything else, which blocks policy and data writes over the API when bundles are the only update path.
  • Separate diagnostics: --diagnostic-addr exposes only /health and /metrics on a second port, so monitoring does not require exposing the main API. The documentation notes those endpoints remain on the main listener too.
  • Least privilege: run as a non-root user in its own account, use TLS even on localhost TCP or a UNIX domain socket, and restrict permissions on token files, which are stored in plaintext.
  • Provenance: the provenance query parameter returns build and bundle revision details, which helps prove which policy produced a decision during an investigation.
  • Built-in capabilities: restrict which built-ins policy may call (for example disallow http.send in domains that should never make network calls) using a capabilities file at build time.

Testing Rego as a release gate

Tests are Rego rules prefixed test_, conventionally in a package ending _test, and the with keyword mocks input, data, rules and even built-in functions. Build the gate in layers:

  1. Static checks: opa fmt and opa check on every commit, plus a linter such as Regal.
  2. Unit tests with a floor: opa test --fail-on-empty --coverage --threshold 90 fails the build if no tests ran or coverage falls under the threshold. todo_ tests are reported as skipped; do not let skipped tests accumulate.
  3. Negative and boundary cases: for each allow rule, test the wrong tenant, a missing attribute, an expired token, an unexpected HTTP method and the undefined path. Undefined is not false, so assert on the enforcement point's interpretation, not only on the rule.
  4. Data-driven tables: keep a table of principal, action, resource and expected decision owned by the business, and run it as parameterised tests. Use --var-values to see why a case failed.
  5. Performance budget: opa test --bench and opa bench measure evaluation cost; the performance documentation recommends objects keyed by ID over arrays and indexable equality statements. Fail the build on a large regression, because a slow PDP becomes an availability problem.
  6. Shadow comparison: before promoting a major policy change, replay recent decision logs (inputs are already masked) against the new bundle and review every flipped decision.

Rolling out and rolling back

  1. Inventory: list every OPA instance, its topology, bundle sources, discovery use, and which services call it. Unknown OPAs with authorisation off are the first finding in most reviews.
  2. Harden one ring: turn on API authorisation, TLS and diagnostic separation on a pilot service; confirm health checks and the PEP still work.
  3. Enable signing in observe mode: start signing bundles in CI while OPA has top-level keys only, confirm every instance verifies signed bundles (status shows the expected active_revision), then add the signing block ring by ring.
  4. Turn on masked decision logging: deploy the mask policy first, sample a day of events, confirm no secrets remain, then enable upload.
  5. Gate readiness: point readiness probes at /health?bundles (or a system.health ready rule) so new pods take traffic only after policy is active.
  6. Rollback: keep the previous signed bundle revision available on the bundle server. Rolling back means republishing it; if you set superseded_etag_ttl_seconds, the documentation notes that a bundle replaced within that window is not served again until it has passed, so size the window with rollback in mind. Never roll back by disabling signature verification.

Where identity fits

OPA itself is a non-human identity with privileged reach: its bundle service token can read every policy, its decision log credential can write to your audit store, and any client allowed to write /v1/policies or /v1/data can grant access. Give each OPA a distinct workload identity (for example a SPIFFE ID or a short-lived service account token), scope bundle credentials to one resource path, and rotate them. Authorisation decisions about AI agents and service accounts also depend on the attributes the PEP sends, so prefer verified workload identity over caller-asserted names. See the SPIFFE and SPIRE Guide and the AI Agent Authorisation Guide.

Detection and measurement

Enable the status plugin (and its Prometheus option) so every instance reports bundle state, plugin state and counters centrally. Useful signals from the OPA monitoring and status documentation:

  • Bundle activation: bundle_failed_load_counter and status bundles[_].code or errors; a signature failure here may be an attack or a broken pipeline, and both need a person.
  • Revision drift: instances whose active_revision differs from the current published revision longer than your polling window.
  • Decision log loss: decision_logs_dropped_rate_limit_exceeded_total, decision_logs_dropped_buffer_size_limit_exceeded_total, decision_logs_encoding_failure_total and status decision_logs.code.
  • Latency and errors: http_request_duration_seconds by handler, and the PEP's own count of PDP timeouts.
  • Unexpected API writes: any successful PUT to policy or data APIs on an instance meant to be bundle-only.

Metrics that show the programme works: percentage of instances with a signing block enforced (target 100), percentage of decision log events with masking applied to sensitive fields, policy test coverage per bundle, median time from merge to fleet-wide activation, and dropped decision log events per million decisions.

Framework mapping

Control areaOPA practiceNIST SP 800-53 Rev. 5NIST CSF 2.0
Policy decision and enforcementDefault deny, PEP fails closedAC-3 Access Enforcement, AC-24 Access Control DecisionsPR.AA-05
Policy change controlReviewed, tested, signed bundlesCM-3 Configuration Change Control, SI-7 Software, Firmware, and Information IntegrityPR.PS-01
Decision loggingDecision logs with revision and decision IDAU-2 Event Logging, AU-3 Content of Audit RecordsDE.CM-09
Log protection and retentionMasking, restricted store, retentionAU-9 Protection of Audit Information, AU-11 Audit Record RetentionDE.CM-09
TransportTLS for API, bundles and log uploadSC-8 Transmission Confidentiality and IntegrityPR.DS-02

Common failure patterns

  • Keys without a signing block: signed bundles verify, unsigned ones are still activated, and nobody notices until a bundle server is compromised.
  • Raw headers in input: the PEP forwards the whole request, decision logs fill with bearer tokens, and the log store becomes a credential source.
  • Silent audit gaps: rate limits or buffer caps drop events under load and no alert fires on the drop counters.
  • Ready before policy: pods pass a plain liveness check and receive traffic before the first bundle activates, so the PEP sees errors or undefined results.
  • Central PDP that fails open: a client library treats timeouts as allow to protect availability, turning an OPA outage into an access control bypass.
  • Exposed management API: --addr 0.0.0.0:8181 with authorisation off lets any reachable client replace policy over /v1/policies.

Practitioner checklist

  • Inventory every OPA instance, its topology, callers and bundle sources.
  • Add a signing block (and discovery.signing) on every instance, with a scope per environment.
  • Sign bundles in CI with a KMS or HSM backed key; never sign on laptops.
  • Run with --authorization=basic and a deny-by-default system.authz policy allowing only decision queries.
  • Deploy a system.log.mask policy before enabling decision log upload; mask tokens, cookies and personal data.
  • Alert on bundle load failures, revision drift and decision log drop counters.
  • Gate CI with opa test --fail-on-empty --coverage --threshold and negative test cases.
  • Point readiness probes at bundle activation, and make every PEP fail closed.
  • Keep the previous signed bundle revision ready for rollback.

Standards and references

Related NHI Mgmt Group resources: Policy-as-Code for Authorisation Guide · Kubernetes Admission Control Guide · Authorisation Models Guide · Externalised Authorisation Buyer's Guide · Amazon Verified Permissions

PDF for subscribers

Take this guide with you

Every section, table and checklist in one PDF, with links to the sources and related guides. Subscribe to our weekly NHI & AI Identity Journal and we’ll email you a download link.

Free weekly newsletter

Subscribe to the NHI & AI Identity Journal

The latest on NHI and Agentic AI security – articles, research, breaches, news and events every week.

Bonus 33% off our NHI Course when you subscribe.

NHIMG Editorial Note
Researched and written by the NHI Mgmt Group editorial team using AI tools, under the editorial direction of Lalit Choda. Facts are drawn from the sources listed on this page and checked by our automated quality process. Last updated 12 November 2026. Spotted an error? Use our contact page.
NHI Mgmt Group, the independent authority on Non-Human Identity, IAM and Agentic AI security. nhimg.org

PDF Downloads for Newsletter Subscribers

Get the guide

Enter your details and we’ll email you a download link.

The full guide is free to read online. The PDF is a thank you for subscribing.

Fill in all fields marked * and tick the PDF box to continue.