Gate: Open Policy Agent Evaluator
This plugin supports all intercept modes (request, response, request_response)
This plugin embeds an Open Policy Agent engine into Gate and restricts access to specific HTTP resources based on OPA policy evaluations. To evaluate an OPA policy you need two pieces of input:
- The Rego policy
- Input data, passed to the Rego policy as the special
inputobject
When it comes to policy distribution, the plugin supports two different methods to distribute policies.
The first is to inline policies, this approach works best when you have a limited number of policies and performance is really important.
The second is to point Gate at a bundle via policy_bundle_url. The bundle can be remote (pulled over HTTP from any host, including the SlashID distribution hub) or local to the Gate container (referenced via a file:// URL).
A local file:// URL can point at:
- a directory containing Rego and data files — the same source layout
opa buildconsumes, so no build step is needed; - a gzipped tar archive produced by
opa build; or - a single
.regofile when you just want one policy under version control.
Local bundles are read once at plugin init — restart Gate to pick up changes.
Rego syntax version
The OPA runtime embedded in Gate uses OPA's /v1 SDK, which parses Rego v1 by default. Rego v1 makes if mandatory on rule bodies, forbids the future.keywords.* imports, and tightens a handful of other syntax rules — see the OPA Rego v1 documentation for the full list.
Existing Rego v0 policies keep working: the rego_version plugin parameter selects the dialect used for both compilation and evaluation.
rego_version | Dialect |
|---|---|
0 (default) | Rego v0 — import future.keywords.if etc. still accepted |
1 | Rego v1 — if mandatory, no future.keywords.* imports |
The examples in the next section use import rego.v1 — the compatibility shim that lets a Rego v0 file opt into the v1 syntax without changing rego_version. You can also drop the import entirely and set rego_version: 1 on the plugin.
Policy result shapes
Gate sniffs the shape of whatever value the decision path evaluates to and maps it onto the observability event's outcome and reasons. Four idiomatic result shapes are supported, mirroring the conventions used by canonical OPA tutorials, Styra DAS, OPA Gatekeeper, and Conftest.
The outcome is allow when the policy admits the request, reject otherwise. When the decision value can't be classified into any of the four shapes below, Gate rejects with opa.result_unrecognized_shape and returns HTTP 502.
In monitoring mode every denial is downgraded to outcome=allow and tagged with opa.policy_denied_advisory — the request always reaches upstream, and the operator can inspect what would have been rejected.
1. Bare boolean
The "hello world" of the OPA docs. The decision path resolves to a bare true/false.
package gate.myapi
import rego.v1
default allow := false
allow if input.user.role == "admin"
Gate emits a single generic opa.policy_allowed or opa.policy_denied reason.
2. Single decision object with an allow flag
The convention used by Styra DAS, and the closest match for AWS Cedar-style {allowed, message} responses or bespoke {permitted, reason} objects.
package gate.myapi
import rego.v1
default result := {"allow": false, "msg": "user role does not admit the request"}
result := {
"allow": true,
"msg": "user role admits the request",
"details": {"method": input.request.http.method},
} if input.user.role == "admin"
- Accepted allow-key spellings:
allow,allowed,permitted. The value must be a boolean. - Accepted message-key spellings:
msg,message,reason. - Any author-supplied
details(object, array, scalar — anything) is passed through to the observability event under thePayload.rawkey.
3. Top-level denial set
The idiom used by the OPA Gatekeeper Constraint Framework and Conftest for policy-as-tests. The decision path resolves to a set of denial entries — an empty set means "allow".
package gate.myapi
import rego.v1
deny contains msg if {
not input.user.has_admin_scope
msg := "admin_scope_required"
}
deny contains msg if {
input.request.method == "POST"
not input.user.is_verified
msg := "not_verified"
}
# Point the plugin at data.gate.myapi.deny via policy_decision_path.
Each surviving entry becomes one PluginReason. Entries can be:
- objects —
{"msg": "..."}, with the same allowed message-key spellings anddetailstreatment as shape #2; - bare strings — treated as the reason message.
An empty set is treated as allow. A non-empty set whose entries are all unrecognizable (bare numbers, nested arrays, nulls) is rejected with opa.result_unrecognized_shape as a fail-safe.
4. Denial set wrapped in a decision object
The shape produced by Gatekeeper constraint templates — a single object containing an array of denials under a well-known key.
package gate.myapi
import rego.v1
violation contains {"msg": "gatekeeper violation"} if {
not input.user.has_admin_scope
}
result := {"violation": violation}
Recognized wrapper keys, in priority order: deny, violation, violations, denials. The value must be an array; if the wrapper key is present with a non-array value, Gate falls back to matching shape #2 on the same object.
Shape mixing
If a single map object matches both shape #3 (denial set) and shape #2 (allow flag), Gate picks the richer of the two when the shapes agree:
- Both agree the request is allowed (e.g.
{"deny": [], "allow": true, "msg": "..."}) → shape #2 wins, so the author's message and details are preserved. - Both agree the request is denied (e.g.
{"deny": [...], "allow": false}) → shape #3 wins, so each denial entry surfaces as its own reason. - The shapes disagree (e.g.
{"deny": ["blocked"], "allow": true}) → Gate refuses to guess and emitsopa.result_unrecognized_shape.
Policy input object
As mentioned, the input object is the second ingredient necessary for OPA. The plugin passes the following input format to OPA.
Note that this format is compatible with OPA-Envoy, so you can reuse your existing OPA policies with Gate.
The following is an example of input object Gate makes available to Rego policies during evaluation in its current v1.2 format. The version.gate field can be checked by a policy to branch on schema changes — it is bumped whenever fields are added to the input. The most recent additive change (v1.1 → v1.2) added the optional request.http.body and request.parsed_body fields described below.
{
"version": {
"gate": "v1.2"
},
"request": {
"http": {
"headers": {
"Accept": [
"application/json, text/plain, */*"
],
"Accept-Encoding": [
"gzip, compress, deflate, br"
],
"Authorization": [
"Bearer eyJhb..."
],
"Cookie": [
"foo=bar"
],
"User-Agent": [
"axios/1.3.4"
]
},
"cookies": {
"foo": "bar"
},
"host": "example.com",
"method": "POST",
"path": "/some/path",
"protocol": "HTTP/1.1",
"query": "option=value",
"scheme": "https",
"url": "https://example.com/some/path?option=value",
"body": "{\"role\":\"admin\"}"
},
"parsed_path": [
"some",
"path"
],
"parsed_query": {
"option": [
"value"
]
},
"parsed_body": {
"role": "admin"
},
"parsed_token": {
"header": {
"alg": "RS256",
"kid": "pYsNGA"
},
"payload": {
...
},
"signature": "JvVt..."
},
"time": "2023-04-09T18:38:41.117405838Z",
"token": "eyJh..."
}
}
The request.http.body and request.parsed_body fields are only populated when the plugin is configured with capture_request_body: true. parsed_body is only set when the request's Content-Type is application/json or application/x-www-form-urlencoded and the body successfully parses; for any other Content-Type (or when parsing fails), only the raw request.http.body is exposed. For form bodies, parsed_body is shaped like Go's url.Values — each value is an array of strings, so a Rego policy reads input.request.parsed_body.role[0] rather than input.request.parsed_body.role.
Note that if the plugin is configured to execute on responses as well then the input object contains both the request and response objects. The request is the same as the object shown above. The whole input object looks as follows:
{
"version": {
"gate": "v1.2"
},
"request": {
"http": {
...
}
},
"response": {
"http": {
"contentType": "application/json",
"status": 200,
"body": "{}",
"headers": {
"Accept": [
"application/json, text/plain, */*"
],
"Accept-Encoding": [
"gzip, compress, deflate, br"
],
"Authorization": [
"Bearer eyJhb..."
],
"User-Agent": [
"axios/1.3.4"
]
}
},
"time": "2023-04-09T18:38:46.117405838Z",
"parsed_path": [
"some",
"path"
],
"parsed_query": {
"option": [
"value"
]
}
}
}
The output of an evaluation is expected to be a boolean to be found at the location specified in the policy_decision_path plugin parameter.
Monitoring options
The OPA plugin exposes two opt-in knobs under a per-plugin monitoring sub-block that mirrors the top-level monitoring block. Both are off by default; the observability event pipeline they feed into is described on the Observability events page.
gate:
plugins:
- type: opa
id: my-auth
config:
policy_bundle_url: file:///etc/gate/policies
monitoring:
policy_bundle: true # ship the loaded rego bundle in the boot event
explain: true # attribute each decision to the source lines that ran
policy_bundle — ship the loaded rego in the boot event
When enabled, the GateServerStarted_v1 boot event carries a policy_bundle field on this plugin's config snapshot: a map[path]content of every rego file loaded by the plugin plus any base data (emitted as a synthetic data.json). This lets the SlashID dashboard show operators exactly what policy Gate is enforcing.
Bundles are shipped whole-or-nothing under a fixed 32 KiB cap. When a bundle exceeds the cap, the field is omitted from the event and Gate logs the total serialized size for reference.
explain — attribute decisions to source lines
When enabled, every OPA decision on this plugin appends one or more opa.trace_at reasons to the per-request observability event, each pointing at a rego source location that contributed to the decision:
| Field | Description |
|---|---|
file | Path within the bundle (e.g. /inline.rego, /policies/authz.rego). |
row | 1-indexed line number of the rule. |
col | 1-indexed column, when the OPA SDK reports one. |
rule_path | The rule's fully qualified reference (e.g. data.gate.auth.deny). |
result | The rule's evaluated value, JSON-serialized. Omitted for compiler-rewritten bare Var heads whose synthetic name would be meaningless. |
explain reasons stack on top of the plugin's normal opa.policy_{allowed,denied,denied_advisory} and opa.result_unrecognized_shape reasons — they don't replace them. Trace attribution is a source pointer, not a per-solution trail: a partial rule that emits N denials via set iteration surfaces as one opa.trace_at reason at the rule's source line, alongside the N per-solution opa.policy_denied reasons. Consumers correlating policy reasons with traces should join on rule_path, not expect 1:1 counts.
Traces surface for every outcome — including opa.result_unrecognized_shape, where the operator most wants to see which source lines produced the misbehaving value.
explain runs the OPA agent with a buffered tracer active, which is meaningfully slower than a plain decision. Recommended for staging, debugging, or low-traffic policies; keep it off in high-QPS production paths.
Configuring Gate
- Environment variables
- HCL
- JSON
- TOML
- YAML
GATE_PLUGINS_<PLUGIN NUMBER>_TYPE=opa
GATE_PLUGINS_<PLUGIN NUMBER>_PARAMETERS_HEADER_WITH_TOKEN=<Header with token>
GATE_PLUGINS_<PLUGIN NUMBER>_PARAMETERS_COOKIE_WITH_TOKEN=<Cookie with token>
GATE_PLUGINS_<PLUGIN NUMBER>_PARAMETERS_SLASHID_BASE_URL=<SlashID base URL>
GATE_PLUGINS_<PLUGIN NUMBER>_PARAMETERS_POLICY=<Embedded Rego policy>
GATE_PLUGINS_<PLUGIN NUMBER>_PARAMETERS_POLICY_BUNDLE_URL=<URL of an OPA bundle>
GATE_PLUGINS_<PLUGIN NUMBER>_PARAMETERS_POLICY_DECISION_PATH=<Location of the policy evaluation decision>
GATE_PLUGINS_<PLUGIN NUMBER>_PARAMETERS_REGO_VERSION=<Rego dialect: 0 or 1. Default is 0 (Rego v0)>
GATE_PLUGINS_<PLUGIN NUMBER>_PARAMETERS_MONITORING_MODE=<boolean - Run the plugin in monitoring mode or enforcement mode. Default is enforcementn>
GATE_PLUGINS_<PLUGIN NUMBER>_PARAMETERS_CAPTURE_REQUEST_BODY=<boolean - Buffer the request body and expose it to Rego. Default is false>
GATE_PLUGINS_<PLUGIN NUMBER>_PARAMETERS_MAX_REQUEST_BODY_SIZE=<integer - Cap on buffered body size in bytes. Default is 1048576 (1 MiB)>
In the Environment variables configuration, <PLUGIN NUMBER> defined plugin execution order.
gate = {
plugins = [
// ...
{
type = "opa"
parameters = {
header_with_token = <Header with token>
cookie_with_token = <Cookie with token>
policy = "<Embedded Rego policy>"
policy_bundle_url = "<URL of an OPA bundle>"
policy_decision_path = "<Location of the policy evaluation decision>"
rego_version = <Rego dialect: 0 or 1>
monitoring_mode = "<Monitoring mode>"
capture_request_body = "<Capture request body>"
max_request_body_size = "<Max request body size>"
}
}
// ...
]
}
{
"gate": {
"plugins": [
// ...
{
"type": "opa",
"parameters": {
"header_with_token": "<Header with token>",
"cookie_with_token": "<Cookie with token>",
"policy": "<Embedded Rego policy>",
"policy_bundle_url": "<URL of an OPA bundle>",
"policy_decision_path": "<Location of the policy evaluation decision>",
"rego_version": <Rego dialect: 0 or 1>,
"monitoring_mode": "<Monitoring mode>",
"capture_request_body": "<Capture request body>",
"max_request_body_size": "<Max request body size>"
}
}
// ...
]
}
}
[[gate.plugins]]
type = "opa"
parameters.header_with_token = "<Header with token>"
parameters.cookie_with_token = "<Cookie with token>"
parameters.policy = "<Embedded Rego policy>"
parameters.policy_bundle_url = "<URL of an OPA bundle>"
parameters.policy_decision_path = "<Location of the policy evaluation decision>"
parameters.rego_version = <Rego dialect: 0 or 1>
parameters.monitoring_mode = "<Monitoring mode>"
parameters.capture_request_body = "<Capture request body>"
parameters.max_request_body_size = "<Max request body size>"
gate:
plugins:
// ...
- type: opa
parameters:
header_with_token: <Header with token>
cookie_with_token: <Cookie with token>
policy: <Embedded Rego policy>
policy_bundle_url: <URL of an OPA bundle>
policy_decision_path: <Location of the policy evaluation decision>
rego_version: <Rego dialect: 0 or 1>
monitoring_mode: <Monitoring mode>
capture_request_body: <Capture request body>
max_request_body_size: <Max request body size>
// ...
where:
<Header with token>is the request header containing the token, if any. This option andcookie_with_tokenare mutually exclusive.<Cookie with token>is the request cookie containing the token, if any. This option andheader_with_tokenare mutually exclusive.<Embedded Rego policy>a Rego policy you can inline directly in Gate's configuration to accept or deny incoming requests. Mutually exclusive withpolicy_bundle_url; exactly one of the two must be set.<URL of an OPA bundle>location of an OPA bundle. Supportshttp(s)://for a remote bundle that Gate fetches periodically (typically built withopa build), andfile://for a local bundle on the Gate container. Afile://URL can point at a directory of Rego/data files (noopa buildstep), a.tar.gzarchive produced byopa build, or a single.regofile. Local bundles are loaded once at plugin init — restart Gate to pick up changes. Mutually exclusive withpolicy; exactly one of the two must be set.<Location of the policy evaluation decision>a Rego policy decision path pointing at the decision value Gate should inspect. Gate accepts several idiomatic result shapes (bare boolean, decision object with anallowflag, denial set, wrapped denial set) — see the Policy result shapes section for details. When the shape is unrecognized, or the resulting decision is a denial, the request is rejected with HTTP 502 or 403 respectively.<Rego dialect: 0 or 1>an integer selecting the Rego syntax version used to compile and evaluate the policy.0(default) accepts Rego v0 (permissive,import future.keywords.*still allowed).1switches the parser to Rego v1 (mandatoryif, nofuture.keywords.*imports). Independent from bundle format — the same value applies to inline policies and to bundles referenced viapolicy_bundle_url.<Monitoring mode>a boolean indicating whether to run the plugin in monitoring mode or enforcement mode. Iftruethe plugin runs in monitoring mode, by default it isfalse(enforcement mode).<Capture request body>a boolean indicating whether to buffer the HTTP request body and expose it to Rego policies viainput.request.http.body(raw) andinput.request.parsed_body(auto-parsed forapplication/jsonandapplication/x-www-form-urlencoded). Defaults tofalse. Enabling this forces the request body to be fully buffered before reaching upstream handlers, which loses streaming semantics; it should only be turned on when a policy actually needs the body.<Max request body size>an integer setting the maximum buffered request body size, in bytes. Requests with a body larger than this cap are rejected with413 Request Entity Too Large. Only applies whencapture_request_bodyistrue. Defaults to1048576(1 MiB).
If neither <Header with token> nor <Cookie with token> are set, the plugin attempts to get a bearer token from the Authorization header, and strips the Bearer prefix from it.
Disabling plugin for specific URLs
You can enable or disable this plugin for specific URLs by using the enabled option in the URLs configuration.
- Environment variables
- HCL
- JSON
- TOML
- YAML
GATE_URLS_0_PATTERN=svc-example.com/*
GATE_URLS_0_TARGET=http://example:8080
GATE_URLS_1_PATTERN=svc-another-example.com/
GATE_URLS_1_TARGET=https://another-example:8080
gate = {
urls = [
{
pattern = "svc-example.com/*"
target = "http://example:8080"
},
{
pattern = "svc-another-example.com/"
target = "https://another-example:8080"
}
]
// ...
}
{
"gate": {
"urls": [
{
"pattern": "svc-example.com/*",
"target": "http://example:8080",
},
{
"pattern": "svc-another-example.com/",
"target": "https://another-example:8080"
}
],
// ...
URL are matched in the order they are defined in the configuration file.
[[gate.urls]]
pattern = "svc-example.com/*"
target = "http://example:8080"
[[gate.urls]]
pattern = "svc-another-example.com/"
target = "https://another-example:8080"
URL are matched in the order they are defined in the configuration file.
gate:
urls:
- pattern: svc-example.com/*
target: http://example:8080
- pattern: svc-another-example.com/
target: https://another-example:8080
URL are matched in the order they are defined in the configuration file.