Istio doesn’t actually perform authorization; it offloads that decision to an external service, acting as a dumb proxy in that specific regard.

Here’s how it looks in practice. Imagine a user trying to access /api/v1/users on your user-service.

apiVersion: security.istio.io/v1beta1
kind: Authorization
metadata:
  name: authz-users
  namespace: default
spec:
  selector:
    matchLabels:
      app: user-service
  action: ALLOW
  rules:
  - to:
    - operation:
        methods: ["GET"]
        paths: ["/api/v1/users"]

This Authorization policy tells Istio: "For any request hitting a pod with the label app: user-service that is a GET request to /api/v1/users, consider allowing it." The key word is "consider." Istio won’t just allow it. It will then look for another policy that tells it where to send the actual authorization decision.

apiVersion: security.istio.io/v1beta1
kind: RequestAuthentication
metadata:
  name: jwt-authn
  namespace: default
spec:
  selector:
    matchLabels:
      app: user-service
  jwtRules:
  - issuer: "https://my-auth-provider.com"
    jwksUri: "https://my-auth-provider.com/.well-known/jwks.json"

This RequestAuthentication policy is where the magic of delegation starts. It tells Istio to expect a JWT in the incoming request. If a valid JWT is present (signed by https://my-auth-provider.com and its public keys are available at jwksUri), Istio will extract the JWT and forward it. But forward it where for the authorization decision?

That’s where the AuthorizationPolicy with an extAuthz field comes in. This is the crucial piece.

apiVersion: security.istio.io/v1beta1
kind: Authorization
metadata:
  name: external-authz-service
  namespace: default
spec:
  selector:
    matchLabels:
      app: user-service
  action: ALLOW
  rules:
  - to:
    - operation:
        methods: ["GET"]
        paths: ["/api/v1/users"]
    check:
      source:
        principal: "cluster.local/ns/default/sa/external-authz-service" # This is the SA of your external auth service
      # Add any specific checks here if needed, e.g., for namespaces, IPs, etc.

Wait, this looks like the previous Authorization policy! The trick is that this policy doesn’t contain the actual authorization rules. Instead, it directs Istio to an external service for the decision. The action: ALLOW here is a bit of a misnomer; it means "if the external authz service says allow, then allow."

The actual delegation happens when the AuthorizationPolicy points to an external authorization service. This is configured within the Istio meshConfig (or in a ServiceEntry for simpler setups, but meshConfig is the standard for a mesh-wide configuration). You’ll define an extAuthz configuration that specifies the gRPC or HTTP endpoint of your external authorization service.

Let’s say your external auth service is running at ext-authz.default.svc.cluster.local:9090 and speaks gRPC. Your meshConfig would look something like this (this is a simplified snippet, actual meshConfig is much larger):

apiVersion: v1
kind: ConfigMap
metadata:
  name: istio
  namespace: istio-system
data:
  mesh: |-
    # ... other mesh configs
    defaultConfig:
      # ... other default configs
      tracing:
        sampling: 100.0
      proxy:
        # ... other proxy configs
        extensionChains:
        - name: "ext-authz-chain"
          listeners:
          - http:
              name: "ext-authz-listener"
              # ... other listener configs
              filterChains:
              - filters:
                - name: "istio.http.ext_authz"
                  config:
                    grpcService:
                      envoy_grpc:
                        clusterName: "ext-authz-grpc-cluster"
                    transportApiVersion: "HTTP_1_1" # Or GRPC
                    failureMode: "Deny" # What to do if the auth service is unavailable
                    # ... other ext_authz configs

And you’d have a corresponding ServiceEntry or DestinationRule for the ext-authz-grpc-cluster that points to your actual external auth service.

The ext_authz filter, when configured, intercepts requests after they’ve been processed by RequestAuthentication (if present) and before they reach the application. It then serializes relevant parts of the request (headers, body, path, method, etc.) into a format the external auth service understands. For gRPC, this is typically an extproto.CheckRequest or similar structure. For HTTP, it’s an HTTP request to a configured endpoint.

Your external authorization service then receives this request. It inspects the data, consults its own policies (e.g., looking at user roles derived from the JWT, checking resource ownership, etc.), and returns a response. For gRPC, this would be an extproto.CheckResponse with a status field. A status.code of 0 (OK) typically means "allow," while any other code means "deny." For HTTP, it’s an HTTP response code (e.g., 200 OK for allow, 403 Forbidden for deny).

Istio receives this response and enforces it. If the external service said "allow," Istio lets the request proceed to the user-service. If it said "deny," Istio terminates the connection with an appropriate error code (usually 403 Forbidden).

The most surprising thing about this setup is that the AuthorizationPolicy itself, when delegating, doesn’t contain the logic for authorization; it’s merely a routing rule for the authorization decision. The intelligence resides entirely in the external service.

The real power comes from separating the "how to get authorization info" (JWTs, mTLS certificates, etc., handled by RequestAuthentication and Istio’s core proxy) from the "what are the actual authorization rules" (your business logic, handled by the external service). This allows you to use a single, centralized authorization service for multiple services within your mesh, or even across different meshes.

The way the extAuthz filter serializes request attributes into the CheckRequest (for gRPC) or HTTP request is highly configurable. You can specify which headers, query parameters, or even parts of the request body should be sent to the external service. This allows you to pass rich context for fine-grained authorization decisions. For example, you might send the X-User-Id header, the requested resource path, and a specific claim from the JWT, all for your external service to evaluate.

The next thing you’ll likely run into is managing the lifecycle and scaling of that external authorization service, as it becomes a critical component of your mesh’s security posture.

Want structured learning?

Take the full Istio course →