Envoy External Authorization server (envoy.ext_authz) with OPA HelloWorld

2019-11-09

I’ve been working with Envoy Proxy for sometime and covered a number of ‘hello world’ type of tutorials derived from my own desire to understanding it better (i tend to understand much more by actually rewriting in code and writing about; it helps reinforce).

Recently, wanted to understand and use the external authorization server since i specialize in authn/authz quite a bit for my job. In digging into it earlier today, i found a number of amazing sample that this post is based on:

But as with anything I do, I gotta try it out myself from scratch an reverse engineer…otherwise its it doesn’t hold object permanence for me. This post is about how to get a basic “hello world” app using envoy.ext_authz where any authorization decision a envoy request makes is handled by an external gRPC service you would run. You can pretty much offload each decision to let a request through based on some very specific rule you define. You ofcourse do not have to use an external server for simple checks like JWT authentication based on claims or issuer (for that just use Envoy’s built-in JWT-Authentication). Use this if you run Envoy directly and wish to make a decision based on some other complex criteria not covered by the others.

This tutorial runs an an Envoy Proxy, a simple http backend and a gRPC service which envoy delegates the authorization check to. You can take pertty much anyting out of the original inbound request context (headers, etc) to make a allow/deny decision on as well as append/alter headers)

Before we get started, a word from our sponsors …here are some of the other references you maybe interested in

Update 10/23/20: add OPA Example


You can find the source here


NOTE: this repo uses envoy 1.17

docker cp `docker create envoyproxy/envoy-dev:latest`:/usr/local/bin/envoy .

Architecture

Well…its pretty straight forward as you’d expect

  1. Client makes HTTP request
  2. Envoy sends inbound request to an external Authorization server
  3. External authorization server makes a decision given the request context
  4. If authorized, the request is sent through

Steps 2,3 is encapsulated as a gRPC proto external_auth.proto where the request response context is set:

// A generic interface for performing authorization check on incoming
// requests to a networked service.
service Authorization {
  // Performs authorization check based on the attributes associated with the
  // incoming request, and returns status `OK` or not `OK`.
  rpc Check(v2.CheckRequest) returns (v2.CheckResponse);
}

What that means is our gRPC external server needs to implement the Check() service..

images/ascii.png

Setup

Anyway, lets get started. You’ll need:

  • Go 11+
  • Get Envoy 1.17+: The tutorial runs envoy directly here but you can use the docker image as well. docker cp docker create envoyproxy/envoy:v1.16.0:/usr/local/bin/envoy .

Start Backend

The backend here is a simple http webserver that will print the inbound headers and add one in the response (X-Custom-Header-From-Backend).

cd backend_server/
$ go run http_server.go 

Start External Authorization server

cd authz_server/
$ go run grpc_server.go

The core of the authorization server isn’t really anything special…i’ve just hardcoded it to look for a header value of ‘foo’ through…you can add on any bit of complex handling here you want.

func (a *AuthorizationServer) Check(ctx context.Context, req *auth.CheckRequest) (*auth.CheckResponse, error) {
	log.Println(">>> Authorization called check()")
	authHeader, ok := req.Attributes.Request.Http.Headers["authorization"]
	var splitToken []string

	if ok {
		splitToken = strings.Split(authHeader, "Bearer ")
	}
	if len(splitToken) == 2 {
		token := splitToken[1]

		if token == "foo" {
			return &auth.CheckResponse{
				Status: &rpcstatus.Status{
					Code: int32(rpc.OK),
				},
				HttpResponse: &auth.CheckResponse_OkResponse{
					OkResponse: &auth.OkHttpResponse{
						Headers: []*core.HeaderValueOption{
							{
								Header: &core.HeaderValue{
									Key:   "x-custom-header-from-authz",
									Value: "some value",
								},
							},
						},
					},
				},
			}, nil
		} else {
			return &auth.CheckResponse{
				Status: &rpcstatus.Status{
					Code: int32(rpc.PERMISSION_DENIED),
				},
				HttpResponse: &auth.CheckResponse_DeniedResponse{
					DeniedResponse: &auth.DeniedHttpResponse{
						Status: &envoy_type.HttpStatus{
							Code: envoy_type.StatusCode_Unauthorized,
						},
						Body: "PERMISSION_DENIED",
					},
				},
			}, nil

		}

	}
	return &auth.CheckResponse{
		Status: &rpcstatus.Status{
			Code: int32(rpc.UNAUTHENTICATED),
		},
		HttpResponse: &auth.CheckResponse_DeniedResponse{
			DeniedResponse: &auth.DeniedHttpResponse{
				Status: &envoy_type.HttpStatus{
					Code: envoy_type.StatusCode_Unauthorized,
				},
				Body: "Authorization Header malformed or not provided",
			},
		},
	}, nil
}

Start Envoy

$ envoy -c basic.yaml -l info

The envoy confg settings describe ext-authz as well as a set of custom headers to send to the client and the authorization checker (i’ll discuss that bit later on in the doc)

    filter_chains:
    - filters:
      - name: envoy.http_connection_manager
        typed_config:  
          "@type": type.googleapis.com/envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager
          stat_prefix: ingress_http
          codec_type: AUTO
          route_config:
            name: local_route
            virtual_hosts:
            - name: local_service
              domains: ["*"]
              routes:
              - match:
                  prefix: "/"
                route:
                  host_rewrite: server.domain.com
                  cluster: service_backend
                request_headers_to_add:
                  - header:
                      key: x-custom-to-backend
                      value: value-for-backend-from-envoy
                per_filter_config:
                  envoy.ext_authz:
                    check_settings:
                      context_extensions:
                        x-forwarded-host: original-host-as-context  				

          http_filters:
          - name: envoy.ext_authz
            config:
              grpc_service:
                envoy_grpc:
                  cluster_name: ext-authz
                timeout: 0.5s

  clusters:
  - name: ext-authz
    type: static
    http2_protocol_options: {}
    load_assignment:
      cluster_name: ext-authz
      endpoints:
      - lb_endpoints:
        - endpoint:
            address:
              socket_address:
                address: 127.0.0.1
                port_value: 50051                

The moment you start envoy, it will start sending gRPC healthcheck requests to the backend. That bit isn’t related to authorization services but i thouht it’d be nice to add into envoy’s config. For more info, see the part where the backend requests are made here in this the generic grpc_health_proxy

Send Requests

  1. No Header
$ curl -vv -w "\n"  http://localhost:8080/

> GET / HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.66.0
> Accept: */*
> 

< HTTP/1.1 401 Unauthorized
< content-length: 46
< content-type: text/plain
< date: Sat, 09 Nov 2019 17:22:39 GMT
< server: envoy
< 
Authorization Header malformed or not provided

Send Incorrect Header

$ curl -vv -H "Authorization: Bearer bar" -w "\n"  http://localhost:8080/

> GET / HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.66.0
> Accept: */*
> Authorization: Bearer bar

* Mark bundle as not supporting multiuse
< HTTP/1.1 401 Unauthorized
< content-length: 17
< content-type: text/plain
< date: Sat, 09 Nov 2019 17:25:14 GMT
< server: envoy
< 

PERMISSION_DENIED

Send Correct Header

$ curl -vv -H "Authorization: Bearer foo" -w "\n"  http://localhost:8080/

> GET / HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.66.0
> Accept: */*
> Authorization: Bearer foo

< HTTP/1.1 200 OK
< x-custom-header-from-backend: from backend
< date: Sat, 09 Nov 2019 17:26:06 GMT
< content-length: 2
< content-type: text/plain; charset=utf-8
< x-envoy-upstream-service-time: 0
< x-custom-header-from-lua: bar
< server: envoy

ok

Send Custom Headers to ext_authz server

If you want to add a custom metadata/header to just the authorization server that was not included in the original request (eg to address envoy issue #3876, consider using the attribute_context extension

In the configuration above, if you send a request fom the with these headers

Client:

$ curl -vv -H "Authorization: Bearer foo" -H "Host: s2.domain.com" -H "foo: bar" http://localhost:8080/

	> GET / HTTP/1.1
	> Host: s2.domain.com
	> User-Agent: curl/7.66.0
	> Accept: */*
	> Authorization: Bearer foo
	> foo: bar
	> 
	* Mark bundle as not supporting multiuse
	< HTTP/1.1 200 OK
	< x-custom-header-from-backend: from backend
	< date: Mon, 11 Nov 2019 19:19:44 GMT
	< content-length: 2
	< content-type: text/plain; charset=utf-8
	< x-envoy-upstream-service-time: 0
	< x-custom-header-from-lua: bar	
	< server: envoy
	< 

	ok

External Authorization server will see an additional context value sent "x-forwarded-host" which you can use to make decision.

$ go run authz_server/grpc_server.go
	2019/11/11 11:19:39 Starting gRPC Server at :50051
	2019/11/11 11:19:42 Handling grpc Check request
	2019/11/11 11:19:44 >>> Authorization called check()
	2019/11/11 11:19:44 Inbound Headers: 
	2019/11/11 11:19:44 {
	":authority": "s2.domain.com",
	":method": "GET",
	":path": "/",
	"accept": "*/*",
	"authorization": "Bearer foo",
	"foo": "bar",
	"user-agent": "curl/7.66.0",
	"x-forwarded-proto": "http",
	"x-request-id": "86c79873-b145-4e82-8e7c-800ecb0ba931"
	}
	
	2019/11/11 11:19:44 Context Extensions: 
	2019/11/11 11:19:44 {
	"x-forwarded-host": "original-host-as-context"
	}

Finally, the backend system will not see that custom header but all the others you specified

$ go run backend_server/http_server.go 
	2019/11/11 11:19:42 Starting Server..
	2019/11/11 11:19:44 / called
	GET / HTTP/1.1
	Host: server.domain.com
	Accept: */*
	Authorization: Bearer foo
	Content-Length: 0
	Foo: bar
	User-Agent: curl/7.66.0
	X-Custom-Header-From-Authz: some value
	X-Custom-To-Backend: value-for-backend-from-envoy
	X-Envoy-Expected-Rq-Timeout-Ms: 15000
	X-Forwarded-Proto: http
	X-Request-Id: 86c79873-b145-4e82-8e7c-800ecb0ba931

Thats it…but realistically, you probably would be fine with using Envoy’s built-in capabilities or with Open Policy Agent or even Istio Authorization. This repo is just a demo of stand-alone Envoy.


References

Google Issued OpenID Connect tokens

This site supports webmentions. Send me a mention via this form.