Investigating Istio AuthorizationPolicy ext_authz in Envoy Config
This post analyzes how Istio AuthorizationPolicy with action: CUSTOM works under the hood, based on an actual envoy config dump.
AuthorizationPolicy
# raw manifest
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
name: my-auth-ext
namespace: istio-gateway
spec:
action: CUSTOM
provider:
name: my-auth-ext
rules:
- to:
- operation:
hosts:
- '*'
when:
- key: request.headers[x-account]
values:
- '*'
targetRefs:
- group: gateway.networking.k8s.io
kind: Gateway
name: istio-public-ingress
---
# istiod chart
istiod:
meshConfig:
extensionProviders:
- name: "my-auth-ext"
envoyExtAuthzGrpc:
service: my-auth-server.app.svc.cluster.local
port: 4003
Core Structure: 2 HTTP Filters
Istio translates an action: CUSTOM AuthorizationPolicy into 2 Envoy HTTP filters. These two filters work in sequence to implement conditional external authorization.
Filter 1: RBAC Shadow Filter (determines whether to apply)
Added as an http_filter on the 0.0.0.0:443 active listener.
- name: envoy.filters.http.rbac
typed_config:
'@type': type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBAC
shadow_rules: # ← "shadow" = no actual traffic blocking
action: DENY
policies:
istio-ext-authz-ns[istio-gateway]-policy[my-auth-ext]-rule[0]:
permissions:
- and_rules:
rules:
- or_rules:
rules:
- header:
name: :authority
present_match: true # ← maps to hosts: ['*']
principals:
- and_ids:
ids:
- or_ids:
ids:
- header:
name: x-account
present_match: true # ← maps to when: x-account header exists
shadow_rules_stat_prefix: istio_ext_authz_
Role: Because it uses shadow_rules, it does not actually block requests. Shadow mode doesn’t affect real requests — it only emits stats and logs results. Here it simply determines “does this request match the conditions?” and records the result as dynamic metadata.
AuthorizationPolicy → RBAC Mapping Table
| AuthorizationPolicy Field | Envoy RBAC Translation |
|---|---|
hosts: ['*'] | permissions → :authority header present_match: true |
when: request.headers[x-account] values: ['*'] | principals → x-account header present_match: true |
When matched, the RBAC filter records the shadow_effective_policy_id value in dynamic metadata. This value is the policy name istio-ext-authz-ns[istio-gateway]-policy[my-auth-ext]-rule[0], which has the istio-ext-authz prefix.
Filter 2: ext_authz Filter (actual auth call)
This filter is added right after “Filter 1” in the http_filter chain.
- name: envoy.filters.http.ext_authz
typed_config:
'@type': type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz
filter_enabled_metadata:
filter: envoy.filters.http.rbac
path:
- key: istio_ext_authz_shadow_effective_policy_id
value:
string_match:
prefix: istio-ext-authz
grpc_service:
envoy_grpc:
authority: my-auth-server.app.svc.cluster.local
cluster_name: outbound|4003||my-auth-server.app.svc.cluster.local
timeout: 600s
transport_api_version: V3
filter_enabled_metadata is the key. What this configuration means:
- Check the dynamic metadata left by the preceding RBAC filter
- If the
istio_ext_authz_shadow_effective_policy_idvalue starts with theistio-ext-authzprefix, activate ext_authz - In other words, only when Filter 1 (RBAC shadow) matches the conditions does Filter 2 (ext_authz) actually call the external auth service
When matched, it sends a CheckRequest via gRPC to the my-auth-server.app.svc.cluster.local:4003 cluster to determine whether to allow or deny the request.
Full Request Processing Flow
Executed sequentially within the http_filters chain of HttpConnectionManager on the 0.0.0.0:443 listener.
Request arrives
│
▼
① istio.metadata_exchange ← peer metadata exchange (just for metrics)
│
▼
② envoy.filters.http.rbac ← CUSTOM policy shadow rule evaluation
│ condition: :authority exists AND x-account header exists
│ result: records policy_id in dynamic metadata
│ (shadow mode, no request blocking)
▼
③ envoy.filters.http.ext_authz ← checks filter_enabled_metadata
│ ├─ metadata has istio-ext-authz prefix
│ │ → gRPC call to my-auth-server:4003
│ │ ├─ auth success (OK) → proceed to next filter
│ │ └─ auth failure (Denied) → return 403
│ └─ metadata has no prefix
│ → ext_authz skip, proceed directly to next filter
▼
④ remaining filters (grpc_stats, alpn, cors, fault, router, etc.)
│
▼
route to upstream
Behavior Summary
| Request Condition | RBAC Shadow Result | ext_authz Behavior | Final Result |
|---|---|---|---|
x-account header present | matched → metadata recorded | gRPC call triggered | allow/deny based on auth server response |
x-account header absent | not matched → no metadata | skip (no call) | proceeds to next filter without ext_authz |
The key insight is that Istio leverages RBAC shadow mode as a conditional trigger mechanism.
- The RBAC filter itself does not block traffic — it only determines “does this request meet the conditions requiring external auth?”
- The ext_authz filter reads that determination via
filter_enabled_metadataand only calls the external auth service for matching requests
ext_authz Auth Server Call Path: via Envoy Cluster
The ext_authz filter’s gRPC call is not a simple network request — it is routed through the outbound|4003||my-auth-server.app.svc.cluster.local Envoy cluster that exists within the Gateway Envoy process. If waypoint is enabled, it follows the same Envoy internal routing path as regular gateway → waypoint service traffic.
Full Call Path
ext_authz filter
│ gRPC call: outbound|4003||my-auth-server.app.svc.cluster.local
▼
Cluster: outbound|4003||my-auth-server
│ endpoint: envoy_internal_address → connect_originate listener
│ metadata: waypoint=10.90.145.210:15008, local=172.20.121.177:4003
▼
Internal Listener: connect_originate
│ TCP Proxy + tunneling_config (HTTP/2 CONNECT)
▼
Cluster: connect_originate
│ original_dst_lb → extract waypoint IP from metadata
│ upstream_port_override: 15008
│ mTLS + HBONE tunnel
▼
Waypoint Proxy (:15008)
│ L7 policy processing (AuthorizationPolicy, HTTPRoute, etc.)
▼
my-auth-server Pod (:4003)
Conclusion: The ext_authz gRPC call goes through the auth server cluster inside Gateway Envoy. If waypoint is enabled, it follows the same Gateway → Waypoint flow (connect_originate → HBONE tunnel). It takes the exact same path as regular backend service routing.
failOpen
There is a field that maps directly to the ext_authz filter.
- name: envoy.filters.http.ext_authz
typed_config:
'@type': type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz
failure_mode_allow: true # ← maps here
grpc_service:
envoy_grpc:
cluster_name: outbound|4003||my-auth-server.app.svc.cluster.local
timeout: 600s