Envoy’s ExtAuthz filter allows you to delegate authorization decisions to external services, meaning your API gateway can offload the complex task of deciding "who can do what" to a dedicated authorization service.

Here’s a breakdown of how it works and what you can do with it.

Let’s say we have a simple service that serves cat pictures. We want to protect this service using Envoy and an external authorization service.

Our cat-service is running on localhost:8080. Our Envoy proxy is configured to listen on localhost:8000. Our external authorization service, authz-service, is running on localhost:8081.

Here’s a basic Envoy configuration snippet for the ExtAuthx filter:

apiVersion: v1
kind: ConfigMap
metadata:
  name: envoy-config
data:
  envoy.yaml: |
    admin:
      access_log_path: /tmp/admin_access.log
      address: 127.0.0.1:9901
    static_resources:
      listeners:
      - name: listener_0
        address:
          socket_address:
            protocol: TCP
            address: 0.0.0.0
            port_value: 8000
        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: ingress_http
              route_config:
                name: local_route
                virtual_hosts:
                - name: local_service
                  domains: ["*"]
                  routes:
                  - match:
                      prefix: "/"
                    route:
                      cluster: cat_service
                  # --- ExtAuthz Filter Configuration ---
                  typed_per_filter_config:
                    envoy.filters.http.ext_authz:
                      "@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz
                      transport_socket:
                        name: envoy.transport_sockets.downstream
                        typed_config:
                          "@type": type.googleapis.com/envoy.extensions.transport_sockets.http.v3.Http
                      grpc_service:
                        envoy_grpc:
                          cluster_name: authz_service
                        timeout: 0.5s
              http_filters:
              - name: envoy.filters.http.ext_authz
                typed_config:
                  "@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz
                  # Other configurations like check_request, etc.
              - name: envoy.filters.http.router
                typed_config:
                  "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
      clusters:
      - name: cat_service
        connect_timeout: 0.25s
        type: LOGICAL_DNS
        dns_lookup_family: V4_ONLY
        lb_policy: ROUND_ROBIN
        load_assignment:
          cluster_name: cluster_0
          endpoints:
          - lb_endpoints:
            - endpoint:
                address:
                  socket_address:
                    address: 127.0.0.1
                    port_value: 8080
      - name: authz_service
        connect_timeout: 0.25s
        type: LOGICAL_DNS
        dns_lookup_family: V4_ONLY
        lb_policy: ROUND_ROBIN
        load_assignment:
          cluster_name: cluster_1
          endpoints:
          - lb_endpoints:
            - endpoint:
                address:
                  socket_address:
                    address: 127.0.0.1
                    port_value: 8081

When a request hits Envoy on port 8000, the ext_authz filter intercepts it before it reaches the cat_service. It then makes a gRPC call to the authz_service on port 8081.

The authz_service receives an AuthorizationRequest which contains details about the incoming HTTP request, such as headers, path, method, and body. The authz_service then performs its logic: it checks if the user is authenticated (e.g., via a JWT in a header), if they have the necessary permissions for the requested resource, and so on.

Based on its decision, the authz_service sends back an AuthorizationResponse.

  • If the response indicates that the request is allowed (ok: true), Envoy forwards the original request to the cat_service.
  • If the response indicates that the request is denied (ok: false), Envoy immediately returns an HTTP 401 Unauthorized or 403 Forbidden response to the client, without ever proxying the request to the cat_service.

The authz_service can also add or modify request headers before they are forwarded to the cat_service by setting append_headers in its AuthorizationResponse. This is useful for passing user identity or authorization context downstream.

Let’s imagine our authz-service is a simple Go program. Here’s a minimal example of how it might look:

package main

import (
	"context"
	"log"
	"net"

	"google.golang.org/grpc"

	auth "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3"
	status "google.golang.org/grpc/codes"
	"google.golang.org/grpc/reflection"
	"google.golang.org/grpc/status"
)

const (
	port = ":8081"
)

type authService struct {
	auth.UnimplementedAuthorizationServer
}

func (s *authService) Check(ctx context.Context, req *auth.AuthorizationRequest) (*auth.AuthorizationResponse, error) {
	log.Printf("Received request: %v", req.GetHttpbie().GetRequest())

	// Simple authorization logic: allow requests with a specific header
	authHeader := req.GetHttpbie().GetHeaders()["x-api-key"]
	if authHeader == "supersecretkey" {
		log.Println("Authorization successful: API key is valid.")
		return &auth.AuthorizationResponse{
			Ok: true,
		}, nil
	}

	log.Println("Authorization failed: Invalid API key.")
	return &auth.AuthorizationResponse{
		Ok: false,
		// You can optionally set a response body and headers here
		// Response: &auth.Response{
		// 	Body: "Unauthorized",
		// 	Headers: []*core.HeaderValueOption{
		// 		{
		// 			Header: &core.HeaderValue{
		// 				Key: "X-Reason",
		// 				Value: "Invalid API Key",
		// 			},
		// 			Append: false,
		// 		},
		// 	},
		// },
	}, status.Error(status.Unauthenticated, "invalid api key")
}

func main() {
	lis, err := net.Listen("tcp", port)
	if err != nil {
		log.Fatalf("failed to listen: %v", err)
	}
	s := grpc.NewServer()
	auth.RegisterAuthorizationServer(s, &authService{})
	reflection.Register(s) // For testing with grpcurl
	log.Printf("Auth service listening at %v", lis.Addr())
	if err := s.Serve(lis); err != nil {
		log.Fatalf("failed to serve: %v", err)
	}
}

To test this, you’d need to:

  1. Run the cat-service (a simple HTTP server).
  2. Run the authz-service Go program.
  3. Start Envoy with the provided configuration.
  4. Make a request to localhost:8000.

Example requests:

  • Successful: curl -H "x-api-key: supersecretkey" http://localhost:8000/cats (You should see a cat picture, and logs in both Envoy and your authz-service indicating success.)

  • Failed: curl http://localhost:8000/cats (You should get a 401 Unauthorized response from Envoy, and logs in your authz-service indicating failure.)

The most surprising thing about Envoy’s ExtAuthz filter is how deeply it integrates into the request lifecycle, allowing not just simple allow/deny decisions but also dynamic modification of requests and responses based on external context. This isn’t just a simple gatekeeper; it’s a full-fledged policy enforcement point. For instance, you can use it to inject user information into headers, enrich requests with data from an external identity provider, or even perform fine-grained access control based on request attributes that aren’t directly visible to the upstream service.

The check_request field in the ExtAuthx filter configuration allows you to precisely control what information is sent to the authorization service. You can choose to send only headers, or include the full HTTP body, or even specific request attributes. This fine-grained control is crucial for optimizing performance and minimizing the attack surface of your authorization service. By default, only headers are sent, which is often sufficient for many authentication and authorization scenarios. If your authorization logic requires inspecting the request body, you would configure check_request.body_mode to ALWAYS or OUTPUT. Be mindful that sending the body can significantly increase latency and resource consumption on both the Envoy proxy and the external authorization service.

The next step in mastering Envoy’s ExtAuthz is exploring the authorization_response field, which allows the external service to modify the response that Envoy sends back to the client, enabling advanced scenarios like dynamic error messages or adding security headers.

Want structured learning?

Take the full Envoy course →