Envoy Proxy x Permit
Overview
This guide explains how to integrate Envoy's External Authorization (ext_authz) filter with a Permit.io PDP using GitOps-managed custom Rego policies. With this setup, Envoy acts as a gateway that filters application requests by calling the PDP, which evaluates your Permit policies and returns an allow/deny decision.
URL Mapping (POST /allowed_url on the PDP HTTP API) is a separate path. It is not used by the Envoy ext_authz gRPC integration described here — the entrypoint policy must map the request to a Permit resource and action itself.
Prerequisites
- Envoy deployed with the External Authorization filter available
- Permit.io account and project with a running PDP
- PDP version 0.9.10 or higher (required for this integration)
- GitOps flow configured for your Permit environment (see below)
- Basic familiarity with Envoy configuration and OPA/Rego
1. Configure GitOps for Custom Policies
Use Permit GitOps to manage your PDP policies (including the Envoy entrypoint) in a Git repository.
- Configure GitOps for your environment (choose one of the following):
- Using the CLI: follow the CLI GitOps guide: Custom Rego (OPA) and GitOps CLI.
- Using the manual flow: follow the GitOps integration guide: Git and Permit.
- Clone the GitOps repository locally and check out the branch for the environment you want Envoy to protect.
Once GitOps is configured, any changes you push to the repository will be synced to the PDP.
2. Add an Envoy entrypoint policy under custom
In your GitOps repository:
-
Create a new Rego file under the
customdirectory at the root of the GitOps repo, for example:custom/envoy_entrypoint.rego -
Define an entrypoint rule that Envoy's ext_authz integration will call. The PDP OPA plugin will evaluate the data document at the path you configure (see the
PDP_OPA_PLUGINSsection below).Put the policy in the
permit.customnamespace used by other custom GitOps files, and derive a Permit resource key from the path (Permit resource keys cannot contain/):package permit.custom.envoyimport data.permit.root as checkimport input.attributes.request.http as http_request# Boolean entrypoint evaluated by the Envoy ext_authz gRPC plugindefault envoy = falseenvoy {method := http_request.method# First path segment is the resource key: /documents/123 -> "documents"# This key must match a resource configured in Permit.segments := split(trim_prefix(http_request.path, "/"), "/")resource_key := segments[0]# Example user & tenant extraction (adapt to your authentication setup)user_key := http_request.headers["x-user-id"]tenant_key := http_request.headers["x-tenant-id"]check_input := {"user": {"key": user_key,},"action": method,"resource": {"type": resource_key,"tenant": tenant_key,},}# Call "permit.check"check.allow with input as check_input}This boolean entrypoint is enough for Envoy to allow or deny the request. The Audit Log Decision column is filled from the boolean result. To also fill user, action, resource, and tenant, return an object that includes
permitio_metadata— see Ingest Envoy decisions into Permit Audit Logs. -
Commit and push the new file to the GitOps repository.
-
Wait for the PDP to sync the updated policies (or trigger a sync if you are using a CI flow).
For the full structure of the Envoy authorization input (input) used in your policy, see the official
Envoy ext_authz filter documentation.
3. Configure the PDP OPA plugins
The PDP must be started with an envoy_ext_authz_grpc plugin that exposes the Envoy-compatible gRPC service and points it at your custom policy entrypoint.
Set the PDP_OPA_PLUGINS environment variable when running the PDP:
export PDP_OPA_PLUGINS='{"permit_graph":{},"envoy_ext_authz_grpc":{"addr":":9191","path":"permit/custom/envoy"}}'
permit_graphis required for Permit to function correctly.envoy_ext_authz_grpcenables the Envoy gRPC external authorization endpoint.addris the address/port on which the gRPC server will listen (e.g.:9191).pathmust match the OPA data path of your Envoy entrypoint policy (in the example above,data.permit.custom.envoy).
The gRPC listener also has to be reachable from Envoy. The PDP image does not expose port 9191 by default:
-
Docker: add
-p 9191:9191to thedocker runcommand (or aportsentry in Compose). -
Helm: add the port to the PDP Service via
pdp.additionalPorts:pdp:additionalPorts:- name: grpcport: 9191targetPort: 9191
For more advanced plugin and Envoy integration options (timeouts, metadata, headers, etc.), see the OPA Envoy plugin configuration docs: OPA Envoy Integration Configuration.
Ensure this variable is set alongside your usual PDP configuration (such as PDP_API_KEY, PDP_DEBUG, etc.).
4. Configure Envoy to call the PDP
In your Envoy configuration, add the HTTP ext_authz filter and point it to the PDP gRPC endpoint configured above.
A minimal example (simplified) might look like this:
http_filters:
- name: envoy.filters.http.ext_authz
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz
transport_api_version: V3
grpc_service:
envoy_grpc:
cluster_name: permit_pdp_ext_authz
timeout: 0.5s
clusters:
- name: permit_pdp_ext_authz
type: logical_dns
connect_timeout: 0.25s
lb_policy: round_robin
http2_protocol_options: {}
load_assignment:
cluster_name: permit_pdp_ext_authz
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: pdp
port_value: 9191
http2_protocol_options is required so the cluster speaks HTTP/2, which gRPC needs. Without it, the upstream defaults to HTTP/1.1 and ext_authz calls to the PDP can fail.
Adjust the cluster name, address, and port to match your environment. Consult the Envoy ext_authz docs for the full configuration options.
5. Ingest Envoy decisions into Permit Audit Logs
Envoy's ext_authz input is not a permit.check payload. The PDP records a decision log for the Envoy query (permit/custom/envoy) and fills the Decision column from the boolean result, but the Audit Log page cannot fill user, action, resource, or tenant from that Envoy-shaped input.
To map those columns, include a permitio_metadata object on the policy result, using the same structure as permit.check(). Permit reads this object as a fallback when the query input is not a standard check.
Return an object from the Envoy entrypoint (the OPA Envoy plugin uses the allowed field for allow/deny). Put permitio_metadata on the default as well — otherwise default-denied requests (missing headers, unparseable path) produce an empty audit row and are dropped:
package permit.custom.envoy
import data.permit.root as check
import input.attributes.request.http as http_request
default envoy := {
"allowed": false,
"permitio_metadata": {"allow": false},
}
envoy := {
"allowed": allowed,
"permitio_metadata": {
"allow": allowed,
"user": {"key": user_key},
"action": method,
"resource": {
"type": resource_key,
"tenant": tenant_key,
},
},
} {
method := http_request.method
segments := split(trim_prefix(http_request.path, "/"), "/")
resource_key := segments[0]
user_key := http_request.headers["x-user-id"]
tenant_key := http_request.headers["x-tenant-id"]
check_input := {
"user": {"key": user_key},
"action": method,
"resource": {
"type": resource_key,
"tenant": tenant_key,
},
}
allowed := check.allow with input as check_input
}
permitio_metadata supports the following fields (all optional; omit any you do not have):
| Field | Audit Log column |
|---|---|
allow | Decision (allow / deny) |
user.key | User |
user.email | User email |
user.first_name / user.last_name | User display name |
action | Action |
resource.type | Resource |
resource.tenant | Tenant |
If the query input already has a standard permit.check field (for example input.user.key), that value wins. permitio_metadata is only used when the corresponding input field is missing.
Keep the OPA path under the permit/ prefix (for example permit/custom/envoy). Decision logs whose query path does not start with permit/ are not ingested into the Audit Log.
After traffic hits Envoy, open the Audit Log and use the All query type. Envoy evaluations are not permit.check queries, so they will not appear if you filter to Check only. Missing columns in the table mean that field was not present on permitio_metadata.
For how to read and filter logs, see Audit Types & Filtering.