Simple Dockerized gRPC Application with Envoy ext_authz Example

Envoy Proxy and gRPC are two immensely popular and useful open-source technologies with wide adoption, including by major enterprises.

So, when I set out to spin up a simple, locally runnable gRPC application where requests were mediated and authenticated via Envoy, I was mostly expecting to, you know, just copy and paste various example files into a few directories and docker-compose up with a self-satisfied smile.

For whatever reason, the exact combination of technologies I was exploring in this case didn’t show up in any easily discoverable tutorials or articles…until now!

This writeup will detail how to run the following: a gRPC client that makes a request to a dockerized golang server exposing a gRPC service. The request is proxied by Envoy and, by using a combination of HTTP and gRPC ext_authz filters, authenticated and authorized before arriving at the upstream server.

Prerequisites:

Define Your RPC Service

In your project directory (/envoy-example in my case), make a directory for your proto definitions.

makedir protos && cd protos
touch hello.proto

Our service is going to be very straightforward: we will create a Hello service. In your hello.proto file, you should copy the following:

Protos provide concise definitions for structured data that can be used to generate code in a variety of languages. Today, we’re going to stick with golang. As a result, you may need to install the following Go plugins before continuing:

export GO111MODULE=on  # Enable module mode
go get google.golang.org/protobuf/cmd/protoc-gen-go \
google.golang.org/grpc/cmd/protoc-gen-go-grpc
export PATH="$PATH:$(go env GOPATH)/bin"

Okay, now you’re ready to let protoc go to work:

protoc --go_out=. --go_opt=paths=source_relative \
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
protos/hello.proto

What this does is generate the golang *and* the gRPC code to create a simple Hello service. Your directory should now look like:

/protos
hello.proto
hello.pb.go
hello_grpc.pb.go

gRPC Server and Client

Before bringing Envoy and docker into the picture, let’s just get a simple request to succeed.

First of all, you will need to make your go module so that imports can be found:

// in your project root
go mod init envoy_example

Next, create a file /client/main.go

And /server/main.go

Now you should have the following directory structure:

go.mod
go.sum
/protos
hello.proto
hello.pb.go
hello_grpc.pb.go
/server
main.go
/client
main.go

If you run the server and then the client, you should see something like:

Dockerize and Add Envoy

Let’s containerize this project, which gives us a good opportunity to add Envoy as well.

First, we’ll add the Dockerfile.server file to spin up a container for the gRPC backend:

Next, we can define our Dockerfile.envoy

Of course, a docker-compose.yaml to orchestrate:

And, finally, an envoy.yaml file:

Ok, that was a lot of files thrown at you! Let’s take a step back and make a few notes about what’s going on here.

First, though: remember to update the ./client/main.go file to dial the server at 1337 instead of the original port, because you are now no longer hitting the gRPC server directly—instead, we’re proxying our request via Envoy! A simple docker-compose up should confirm that everything is working: you’ll see the logs from envoy, and if you run your client code again, you should see the request logged by your Envoy container before seeing the response!

So, we have a client making a call to the dockerized Envoy sidecar. A few lines in the yaml defining our Envoy are worth noting:

listeners:
- name: listener_0
address:
socket_address: { address: 0.0.0.0, port_value: 1337 }

We can see here that Envoy is listening on port 1337. That’s why our docker-compose.yaml file exposes container port 1337 to outside processes — and why our client now hits that port instead.

- match:
prefix: "/"
grpc: {}
route:
cluster: backend

This simply matches gRPC traffic and tells Envoy to route it to the backend cluster, which is defined in the clusters section at the bottom of the yaml.

If you were to, say, change the match prefix to something that specifically does NOT match your request (which is likely /hello.HelloService/Hello ), you’d find that Envoy returns a gRPC error: UNIMPLEMENTED—essentially the gRPC equivalent of 404 Not Found!

Introducing ext_authz

Envoy is more than just a proxy server—you can introduce filters that enable some pretty cool features, from rate limiting to logging to authentication and authorization.

In our case, we’ll be doing the latter with an ext_authz filter. In fact, we’re going to set up two of them, so that you can see how filters work together, and how to set up both gRPC and HTTP ext_authz filter services.

First, we’ll add two new services to our project.

gRPC ext_authz Service

Let’s go ahead and add another gRPC server that implements an ext_authz method Check (overview here). Start by adding another directory called extauth. Then, add yet another main.go file:

Yep, just another go server with a gRPC service that exposes Check. Now, the interesting thing to note here is that the request to Check is not the original request! If you were to examine the metadata context of the request, it’s a bunch of Envoy-related data.

The original request is wrapped in the req.GetAttributes().GetRequest() object. That’s where we look to get the metadata keys we originally set on the client context:

ctx := metadata.AppendToOutgoingContext(
context.Background(),
"Authorization", "Bearer foo",
"Bar", "baz"
)

Authorization and Bar will be set as HTTP headers on that embedded request. Kinda weird, but it works!

Here, we’re just checking the Authorization header for validity (is it three characters long? Very secure). Then, we attach a credential header with “permissions” to the request and return an OK to Envoy, which sees that and spirits along the request. Otherwise, we return a 401, which Envoy knows to transform into the appropriate gRPC error: UNAUTHENTICATED.

Now, we’ll also need to add and update some other files:

In our envoy.yaml, add the following cluster definition:

- name: extauth
connect_timeout: 5s
type: STRICT_DNS
http2_protocol_options: {}
lb_policy: round_robin
hosts:
- socket_address:
address: extauth
port_value: 4040

Also, your http_filters section should now look like this:

http_filters:
- name: envoy.ext_authz
config:
grpc_service:
envoy_grpc:
cluster_name: extauth
- name: envoy.router

The docker-compose.yaml file needs a new section as well:

extauth:
build:
context: .
dockerfile: Dockerfile.extauth
networks:
- envoymesh
expose:
- "4040"
ports:
- "4040:4040"

And, as you may have guessed, a new Dockerfile.extauth:

That should do it! Go ahead and docker-compose up and give it a try. Update the Authorization header in your client call, and see how denials work. You’ll note plenty of useful logging in the envoy logs, as well!

So, now our little setup has grown a bit:

Now, what if you have multiple auth steps or more than one service that acts as part of your auth flow? One option would be consolidating your authentication and authorization logic in a single service. Another might be having the first service called by the ext_authz filter proxy any further calls itself.

Let’s go with an alternative, and simply introduce a final filter: one more ext_authz filter that will call a different service, so we can demo how filters are chained, as well as how to configure an HTTP service for ext_authz calls in addition to the RPC service we already implemented.

HTTP ext_authz Service

Alright, so we want to add a second server — let’s go ahead and add a new main.go file to a folder called extauth_http:

So here we have a simple HTTP server. The only handler performs another check: this time, rather than looking for an Authorization header, we can assume it’s already there and been authenticated correctly—otherwise, we wouldn’t have made it this far in the request flow!

Instead, we’ll look at the header added in the previous filter: we’ll look for my-credential-header and see if some arbitrarily designated “required” permission was included.

If so, congrats! This request will be forwarded on ahead to the upstream host. Otherwise, we’ll once again return a forbidden response, this time with a custom body that we defined to show how envoy will convert that to a gRPC message.

Now we’ll need to add the usual suspects again, including a new Dockerfile:

We’ll also add a new stanza to the docker-compose.yaml file:

httpauth:
build:
context: .
dockerfile: Dockerfile.extauth_http
networks:
- envoymesh
expose:
- "10003"
ports:
- "10003:10003"

And, finally, a couple new envoy.yaml sections. The first is the filter config, which should go between the envoy.ext_authz filter and the envoy.router filter:

- name: envoy.filters.http.ext_authz
typed_config:
"@type": type.googleapis.com/envoy.config.filter.http.ext_authz.v2.ExtAuthz
http_service:
authorization_request:
allowed_headers:
patterns:
- prefix: "my-credential-header"
server_uri:
uri: 127.0.0.1:10003
cluster: extauth_http
timeout: 0.25s
failure_mode_allow: false
include_peer_certificate: true

One thing to note here is that we have to explicitly allow the header we’re setting in the first filter to be passed to the second. If we didn’t, only the original headers from the gRPC request’s MetadataContext would be provided to our HTTP server’s handler.

And lastly a stanza for the cluster definition:

- name: extauth_http
connect_timeout: 0.25s
type: STRICT_DNS
lb_policy: round_robin
hosts:
- socket_address:
address: host.docker.internal
port_value: 10003

You can find the full envoy.yaml file here :)

Your final directory structure should look like:

go.mod
go.sum
envoy.yaml
docker-compose.yaml
Dockerfile.envoy
Dockerfile.extauth_http
Dockerfile.server
Dockerfile.extauth
/extauth
main.go
/extauth_http
main.go
/protos
hello.proto
hello.pb.go
hello_grpc.pb.go
/server
main.go
/client
main.go

Putting it all together

Let’s go ahead and docker-compose up --build to build fresh containers for all this good stuff.

Barring any terrifying logs and failures that are surely not the fault of this impeccable guide, you should have the following setup running, indicated by voluminous logs:

You can play around with the headers you set in the client file and see how things get denied.

You can also mess around with how the ext_authz filters interact. Try and set a different “permission” in the gRPC ext_authz server, for example. This is a silly exercise in some ways, but imagine that that server was instead taking the auth header and looking up the permissions for that identity before adding them to the request. Similarly, the second filter you can imagine would be checking for specific permissions that apply to the upstream host it’s proxying for.

There’s a lot of ways you can go with all this. Luckily, now you have a full on sandbox environment to play with and experiment.

Have fun!

Solver of problems. Brewer of beers. Befriender of dogs. Bike tourist and giant baseball nerd. Driven human being.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store