Jaehong Jung

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 FieldEnvoy RBAC Translation
hosts: ['*']permissions:authority header present_match: true
when: request.headers[x-account] values: ['*']principalsx-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_id value starts with the istio-ext-authz prefix, 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 ConditionRBAC Shadow Resultext_authz BehaviorFinal Result
x-account header presentmatched → metadata recordedgRPC call triggeredallow/deny based on auth server response
x-account header absentnot matched → no metadataskip (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_metadata and 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

Reference