OPA Policy Checker Plugin
Validates incoming Beckn messages against network-defined business rules using Open Policy Agent (OPA) and the Rego policy language. Non-compliant messages are rejected with a BadRequest error code.
Features
- Evaluates business rules defined in Rego policies
- Supports multiple policy sources: single policy file, local policy directory, or OPA bundle (
.tar.gz)
- Supports manifest-backed policy resolution through the
manifestloader plugin
- Signature verification for single-file policies and OPA bundles
- Structured result format:
{"valid": bool, "violations": []string}
- Fail-closed on empty/undefined query results — misconfigured policies are treated as violations
- Runtime config forwarding: adapter config values are accessible in Rego as
data.config.<key>
- Action-based enforcement: apply policies only to specific beckn actions (e.g.,
confirm, search)
- Configurable fetch timeout for remote policy and bundle sources
- Warns at startup when policy enforcement is explicitly disabled
Configuration
This plugin now requires networkPolicyConfig. Older top-level policy keys such as type, location, query, actions, and refreshIntervalSeconds are no longer supported.
manifestLoader:
id: manifestloader
config:
cacheTTL: 24h
fetchTimeoutSeconds: "30"
checkPolicy:
id: opapolicychecker
config:
networkPolicyConfig: ./config/opa-network-policies.yaml
refreshInterval: "5m"
steps:
- checkPolicy
- addRoute
Configuration Parameters
| Parameter |
Type |
Required |
Default |
Description |
networkPolicyConfig |
string |
Yes |
- |
Path to a YAML file containing networkPolicies keyed by network_id |
enabled |
string |
No |
"true" |
Enable or disable the plugin |
debugLogging |
string |
No |
"false" |
Enable verbose OPA evaluation logging |
refreshInterval |
string |
No |
- |
Reload all configured policies on a Go duration such as 30s, 20m, or 24h |
| any other key |
string |
No |
- |
Forwarded to Rego as data.config.<key> |
Network Policy Config File
The plugin loads all configured policies at startup and selects the correct one at request time using context.networkId or context.network_id.
Top-level plugin config:
checkPolicy:
id: opapolicychecker
config:
networkPolicyConfig: ./config/opa-network-policies.yaml
refreshInterval: "5m"
Structured config file:
networkPolicies:
nfh.global/testnet:
type: manifest
nfo.example.org/mobility-network:
type: file
location: https://nfo.example.org/policies/mobility.rego
query: "data.mobility.policy.result"
nfo.example.org/logistics-network:
type: bundle
location: https://nfo.example.org/policies/logistics.tar.gz
query: "data.logistics.policy.result"
default:
type: file
location: ./policies/default.rego
query: "data.default.policy.result"
Behavior:
- all configured policies are loaded at startup
- request-time selection uses exact match on
context.networkId and falls back to context.network_id
- if an exact network-specific policy matches, it is selected even when
enabled: false
- an exact network match with
enabled: false intentionally overrides default
- if no network-specific policy matches,
default is used when configured
- if neither a network-specific policy nor
default matches, OPA evaluation is skipped by design
- policy enforcement is opt-in by
network_id; unmatched networks are skipped unless default is defined
- if you want one global policy only, define just
default
Each entry under networkPolicies supports:
type: file, dir, bundle, or manifest
location and query for file, dir, and bundle
- optional
actions
- optional
enabled
- optional
fetchTimeoutSeconds
- optional
verification for file and bundle
Manifest-backed Policies
Use type: manifest when the network policy should be resolved indirectly through a verified network manifest fetched by the manifestloader plugin.
Guides for NFOs creating and publishing network policies can be found here:
manifestLoader:
id: manifestloader
config:
cacheTTL: 24h
fetchTimeoutSeconds: "30"
forceRefreshOnStartup: false
disableCache: false
checkPolicy:
id: opapolicychecker
config:
networkPolicyConfig: ./config/opa-network-policies.yaml
refreshInterval: "5m"
networkPolicies:
nfh.global/testnet:
type: manifest
Rules for type: manifest:
manifestLoader must be configured in the same handler/module as checkPolicy
location, query, and verification must not be set on the type: manifest entry
- the manifest is fetched by network ID using the network policy key
- the manifest uses fields such as
manifest_version, manifest_type, network_id, policy_query_path, and signature_url
- the manifest must contain:
manifest_type: "network-manifest"
network_id matching the configured network policy key exactly
policies.type: "rego"
policies.source: "file" or "bundle"
- valid
governance.effective_from
- if
governance.effective_until is present, it must be later than effective_from and not expired
- resolved manifest policy sources are then loaded through the normal
file or bundle OPA code paths
Signature Verification
Verification is optional and configured per policy entry.
Single-file policy with detached signature:
networkPolicies:
retail.network/production:
type: file
location: ./policies/retail.rego
query: data.policy.result
verification:
enabled: true
publicKeyLookupUrl: https://api.dedi.global/dedi/lookup/example-nfo.com/public_key_test/retail-key
signatureLocation: ./policies/retail.rego.sig
Signed bundle:
networkPolicies:
retail.network/production:
type: bundle
location: ./policies/retail-bundle.tar.gz
query: data.retail.validation.result
verification:
enabled: true
publicKeyLookupUrl: https://api.dedi.global/dedi/lookup/example-nfo.com/public_key_test/retail-key
Rules:
type: file supports local files and remote URLs
type: bundle supports local .tar.gz files and remote bundle URLs
type: dir is not recommended for production use and should be used only for testing or local development
type: dir does not support signature verification; package directories as signed bundles instead
verification.publicKeyLookupUrl should point to a DeDi public-key record lookup endpoint
- public-key lookup JSON is read only from supported fields:
data.details.publicKey, data.details.signing_public_key, data.details.public_key, or legacy top-level publicKey, signing_public_key, and public_key
- DeDi public-key lookup supports
keyFormat: base64 using standard padded base64
- as a fallback, raw PEM public keys, PEM certificates, and parseable DER key material are also accepted
- detached signature locations may return raw signature bytes, base64-encoded signature text, or JSON with a top-level
signature field
- formats such as
base58, hex, and JWK are not supported by the current plugin implementation
- when
verification.enabled: true for type: file, verification.signatureLocation and verification.publicKeyLookupUrl are required
- when
verification.enabled: true for type: bundle, verification.publicKeyLookupUrl is required
verification.algorithm is optional for type: bundle and defaults to ES256
- supported
verification.algorithm values for bundle verification are:
ES256, ES384, ES512
RS256, RS384, RS512
PS256, PS384, PS512
EdDSA is not supported by the current plugin implementation for bundle verification
Single-file detached signature verification does not use a separate algorithm config field. The plugin chooses the verification path automatically from the key type returned by DeDi:
RSA key type:
- verifies detached signatures using RSA PKCS#1 v1.5 with SHA-256
ECDSA key type:
- verifies detached signatures using ECDSA with SHA-256
Ed25519 key type:
- verifies detached signatures using Ed25519
Policy Hot-Reload
When refreshInterval is set, a background goroutine periodically reloads and recompiles all configured policy sources without restarting the adapter:
- Atomic swap: the old evaluator stays fully active until the new one is compiled — no gap in enforcement
- Non-fatal errors: if the reload fails (e.g., file temporarily unreachable or parse error), the error is logged and the previous policy stays active
- Manifest cache boundary: for
type: manifest, each refresh asks manifestloader for the manifest again, but manifest freshness is still controlled by manifestloader.cacheTTL, forceRefreshOnStartup, and disableCache. A short OPA refreshInterval does not force a network manifest re-fetch while the manifest cache entry is still valid.
- Goroutine lifecycle: the reload loop stops when the adapter context is cancelled or when plugin
Close() is invoked during shutdown
config:
networkPolicyConfig: ./config/opa-network-policies.yaml
refreshInterval: "5m"
If operators expect manifest changes to become eligible on every OPA refresh, set the manifest loader cacheTTL less than or equal to the OPA refreshInterval, or use disableCache during debugging.
How It Works
Initialization (Load Time)
- Load Policy Config: Reads the structured
networkPolicyConfig file
- Resolve Manifest-backed Entries: For
type: manifest, fetches the verified manifest through manifestloader, validates it, and resolves it into a concrete file or bundle policy source
- Load Policy Sources: Fetches
.rego files or bundles for each configured network policy entry
- Verify Signatures: When enabled, verifies detached signatures for single-file policies or embedded signatures for signed bundles
- Compile Policies: Compiles one evaluator per configured
network_id plus optional default
Request Evaluation (Runtime)
- Select Policy: Match
context.networkId exactly, fall back to context.network_id, then default
- Check Action Match: If
actions is configured on the selected policy, skip evaluation for non-matching actions. The plugin assumes standard adapter routes look like /{participant}/{direction}/{action} such as /bpp/caller/confirm; non-standard paths fall back to context.action from the JSON body.
- Evaluate OPA Query: Run the selected policy with the full beckn message as
input
- Handle Result:
- If the query returns no result (undefined) → violation (fail-closed)
- If result is
{"valid": bool, "violations": []string} → use structured format
- If result is a
set or []string → each string is a violation
- If result is a
bool → false = violation
- If result is a
string → non-empty = violation
- Reject or Allow: If violations are found, NACK the request with all violation messages
| Rego Output |
Behavior |
{"valid": bool, "violations": ["string"]} |
Structured result format (recommended) |
set() / []string |
Each string is a violation message |
bool (true/false) |
false = denied, true = allowed |
string |
Non-empty = violation |
| Empty/undefined |
Violation (fail-closed) — indicates misconfigured query path |
Example Usage
Default-only Policy
checkPolicy:
id: opapolicychecker
config:
networkPolicyConfig: ./config/opa-network-policies.yaml
networkPolicies:
default:
type: file
location: ./pkg/plugin/implementation/opapolicychecker/testdata/example.rego
query: "data.policy.result"
Writing Policies
Policies are written in Rego. The plugin passes the full beckn message body as input and any adapter config values as data.config:
package policy
import rego.v1
# Default result: valid with no violations.
default result := {
"valid": true,
"violations": []
}
# Compute the result from collected violations.
result := {
"valid": count(violations) == 0,
"violations": violations
}
# Require provider on confirm
violations contains "confirm: missing provider" if {
input.context.action == "confirm"
not input.message.order.provider
}
# Configurable threshold from adapter config
violations contains "delivery lead time too short" if {
input.context.action == "confirm"
lead := input.message.order.fulfillments[_].start.time.duration
to_number(lead) < to_number(data.config.minDeliveryLeadHours)
}
See testdata/example.rego for a full working example.
Relationship with Schema Validator
opapolicychecker and schemav2validator serve different purposes:
- Schemav2Validator: Validates message structure against OpenAPI/JSON Schema specs
- OPA Policy Checker: Evaluates business rules via OPA/Rego policies
Configure them side-by-side in your adapter steps as needed.
Plugin ID vs Step Name
- Plugin ID (used in
id:): opapolicychecker (lowercase, implementation-specific)
- Step name (used in
steps: list and YAML key): checkPolicy (camelCase verb)
Dependencies
github.com/open-policy-agent/opa — OPA Go SDK for policy evaluation and bundle loading
Known Limitations
- Signed directories are not supported: If you want signature verification for multiple Rego files, package them as a signed OPA bundle instead of using
type: dir.
- Non-standard route shapes: URL-based action extraction assumes the standard Beckn adapter route shape
/{participant}/{direction}/{action} and falls back to context.action for other path layouts.