This episode covers Envoy's security filters: external authentication with ext_authz, the JWT authentication filter, HTTP authorization filters, and content-based routing with request inspection.

Episodes 12 and 13 touched security in general. Episode 14 focuses on security filters: the concrete tools installed in Envoy's pipeline to enforce authentication and authorization — ext_authz for external decisions, the JWT filter for native token verification, HTTP authorization filters, and request inspection with content-based routing.
ext_authz sends request metadata to a gRPC authentication service and waits for a decision:
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: auth_service
failure_mode_allow: false
clear_route_cache: true
allowed_headers:
patterns:
- exact: authorization
- exact: cookie
buffer_limits:
max_request_bytes: 8192
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.RouterWith failure_mode_allow: false, if the auth service is unreachable the request is denied — the right fail-closed policy. allowed_headers limits which headers the auth service can see, minimizing the sensitive data that leaves.
An ext_authz service follows the gRPC Check contract. A minimal example in Go:
func (s *server) Check(ctx context.Context, req *envoyservice.CheckRequest) (*envoyservice.CheckResponse, error) {
token := req.GetAttributes().GetRequest().GetHttp().GetHeaders()["authorization"]
if !validToken(token) {
return denied(), nil
}
return allowed(), nil
}The Check function receives request metadata and returns allowed or denied. This code is a skeleton you can grow into any policy.
The JWT filter verifies tokens directly in Envoy without an external service:
http_filters:
- name: envoy.filters.http.jwt_authn
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.JwtAuthentication
providers:
oidc_provider:
issuer: https://auth.example.com/
audiences:
- orders-api
remote_jwks:
http_uri:
uri: https://auth.example.com/.well-known/jwks.json
cluster: auth_jwks
timeout: 5s
cache_duration: 300s
forward_payload_header: x-jwt-payload
rules:
- match:
prefix: "/orders"
requires:
provider_name: oidc_provider
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.RouterThe oidc_provider provider defines the issuer, audience, and JWKS source. The filter fetches the public keys from remote_jwks, verifies the token signature, and rejects tokens from unknown issuers.
JWT rules are applied per path: all /orders paths require a valid token from oidc_provider. Other paths stay open. This pattern allows a mixed API: some endpoints public, some protected by JWT.
forward_payload_header: x-jwt-payloadAfter successful verification, the JWT payload is forwarded in the x-jwt-payload header. Internal services read claims like sub and scope without having to verify the token again.
Header-based authorization suits simple rules evaluated quickly:
http_filters:
- name: envoy.filters.http.rbac
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBAC
rules:
action: ALLOW
policies:
admin_only:
permissions:
- any: true
principals:
- header:
name: X-User-Role
exact_match: admin
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.RouterThe rbac filter checks the X-User-Role header value. Only requests with the value admin are allowed; the rest get a 403. Combine it with JWT: the token is verified first, the role header is generated, then RBAC reads it.
Beyond security, filters can inspect request content and route based on it:
routes:
- match:
prefix: "/api/"
headers:
- name: X-Tenant
string_match:
exact: tenant-a
route:
cluster: tenant_a_backend
- match:
prefix: "/api/"
query_parameters:
- name: beta
string_match:
exact: "1"
route:
cluster: beta_backendThe match.headers and match.query_parameters rules allow routing based on request content. This is called content-based routing — useful for directing traffic based on context.
To test the whole filter chain, send a request with a valid token and one without:
curl -s -o /dev/null -w "%{http_code}\n" \
-H "Host: api.example.com" http://localhost:10000/orders
curl -s -o /dev/null -w "%{http_code}\n" \
-H "Host: api.example.com" -H "Authorization: Bearer <TOKEN>" \
-H "X-User-Role: admin" http://localhost:10000/ordersThe first request should be denied (missing token), the second allowed. The curl -w "%{http_code}" command remains the fastest way to verify each filter's decision.
For a security pipeline, the common order is:
Each filter shrinks the surface seen by the next. JWT produces an identity; ext_authz and RBAC consume that identity to make decisions.
curl -s localhost:9901/config_dump | grep -E '"name": "envoy.filters.http'Grepping envoy.filters.http on config_dump shows the actual loaded filter order — use this to make sure the order matches your plan.
Episode 14 completed Envoy's security toolkit: ext_authz for external decisions, the JWT filter for native token verification, header-based RBAC, and content-based routing for request inspection.
Key takeaways:
ext_authz delegates auth decisions to an external gRPC service.forward_payload_header forwards claims to internal services.match.headers and query_parameters enable content-based routing.In the next episode, episode 15, we'll discuss performance tuning and resource management — Envoy's threading model, worker threads, connection limits, buffer sizes, HTTP/2 pool tuning, and CPU and memory optimization.