The most common security failure I see in internal APIs is when authentication and authorization both end up baked into the same application code, and over time each service starts behaving differently. One service accepts a JWT based on signature alone, another inspects claims, and a third interprets a service identity instead of a user. Envoy’s ext_authz filter offers a practical middle layer here: the request lands at the proxy first, the decision is delegated to a separate authorization service, and the application is free to focus only on business logic.

What problem does this solve?
Services running on the internal network often grow under the assumption that “we’re already in a trusted zone.” Then, as integrations multiply, these issues surface:
- The same identity is interpreted with different rights across services.
- A policy change requires deploying every service one at a time.
- When a request is denied, the reason behind the decision isn’t visible centrally.
- The operations team can’t tell whether a 403 is caused by network, token, or policy.
So the ext_authz model offers more than just security; it also brings operational readability.
Target architecture
The basic flow looks like this:
- The client request arrives at Envoy.
- Envoy forwards request metadata to the
ext_authzservice. - The authorization service evaluates identity and policy.
- Envoy lets the request through or rejects it.
- The decision record is sent to a centralized log or metrics pipeline.
In this model, the application service can safely assume the request has already been pre-filtered for authorization.
Minimal Envoy configuration
The example below shows how ext_authz is wired into the HTTP filter chain:
static_resources:
listeners:
- name: internal-api
address:
socket_address:
address: 0.0.0.0
port_value: 8443
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
stat_prefix: internal_api
route_config:
name: local_route
virtual_hosts:
- name: api
domains: ["*"]
routes:
- match: { prefix: "/" }
route: { cluster: app }
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
failure_mode_allow: false
grpc_service:
envoy_grpc:
cluster_name: authz
- name: envoy.filters.http.router
clusters:
- name: authz
connect_timeout: 1s
type: STRICT_DNS
load_assignment:
cluster_name: authz
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: authz.internal
port_value: 9000
- name: app
connect_timeout: 1s
type: STRICT_DNS
load_assignment:
cluster_name: app
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: app.internal
port_value: 8080
The critical choice here is failure_mode_allow: false. If your authorization service can’t respond, denying the request in a controlled way is safer than letting it through. But you also need to plan the operational impact of that decision in advance.
What should the authorization service look at?
I prefer to keep the ext_authz service as small as possible, with crisp responsibilities. These are the fields I treat as fundamental:
- The service or user identity behind the request
- Target path, method, and tenant context
- The required role or policy name
- A short reason code that explains why the decision was made
Letting this service drift into business logic is a mistake. Order limits, campaign rules, or customer segmentation should remain the application’s responsibility; the ext_authz layer should be limited to the access decision.
How should the test flow be designed?
Before you take this pipeline live, automate three scenarios:
- A request that is allowed with a valid identity
- A request that should be denied even with a valid identity
- System behavior when the authorization service is slow or unreachable
The third scenario is especially critical. Many teams test normal traffic, but they don’t observe how Envoy’s queues and client timeouts behave when the authorization service slows down.
Operational observability layer
Once the setup is complete, watch these signals:
- Total request count and the proportion being denied
- Distribution of denials by reason code
ext_authzresponse time- Error rate between the proxy and the authorization service
Without these metrics, the security layer looks centralized on the surface but stays opaque in practice.
Conclusion
Building an internal API authorization chain with Envoy ext_authz doesn’t force you to rip access decisions out of service code entirely; what it does is move them onto a shared, observable, manageable layer. When identity, policy, and decision records are separated, the security team gets more consistent control, the platform team diagnoses problems faster, and application teams can stay focused on business logic.