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 thecat_service. - If the response indicates that the request is denied (
ok: false), Envoy immediately returns an HTTP401 Unauthorizedor403 Forbiddenresponse to the client, without ever proxying the request to thecat_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:
- Run the
cat-service(a simple HTTP server). - Run the
authz-serviceGo program. - Start Envoy with the provided configuration.
- 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 yourauthz-serviceindicating success.) -
Failed:
curl http://localhost:8000/cats(You should get a401 Unauthorizedresponse from Envoy, and logs in yourauthz-serviceindicating 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.