Skip to main content

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.

  1. Configure GitOps for your environment (choose one of the following):
  2. 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:

  1. Create a new Rego file under the custom directory at the root of the GitOps repo, for example:

    custom/envoy_entrypoint.rego
  2. 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_PLUGINS section below).

    Put the policy in the permit.custom namespace used by other custom GitOps files, and derive a Permit resource key from the path (Permit resource keys cannot contain /):

    package permit.custom.envoy

    import data.permit.root as check
    import input.attributes.request.http as http_request

    # Boolean entrypoint evaluated by the Envoy ext_authz gRPC plugin
    default envoy = false

    envoy {
    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.

  3. Commit and push the new file to the GitOps repository.

  4. 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_graph is required for Permit to function correctly.
  • envoy_ext_authz_grpc enables the Envoy gRPC external authorization endpoint.
  • addr is the address/port on which the gRPC server will listen (e.g. :9191).
  • path must 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:9191 to the docker run command (or a ports entry in Compose).

  • Helm: add the port to the PDP Service via pdp.additionalPorts:

    pdp:
    additionalPorts:
    - name: grpc
    port: 9191
    targetPort: 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):

FieldAudit Log column
allowDecision (allow / deny)
user.keyUser
user.emailUser email
user.first_name / user.last_nameUser display name
actionAction
resource.typeResource
resource.tenantTenant

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.

note

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.