Envoy as an API Gateway: Part IV


Glossary

  • Authentication (authn) is a verification of the user’s identity.
  • Authorization (authz) is a verification of the user access permissions.

Outcome

This part will show you how to authenticate and/or authorize incoming HTTP requests before them to upstream service-one.

ext_authz is an external authorization module, but we will use it for both authentication and authorization. For authentication, Envoy proxy has a dedicated JSON Web Token (JWT) Authentication module.

Changed plan

Our schema from the beginning of the story should be slightly modified. We add here a new authz service. Envoy will call it and check, should the request be forwarded to the upstream service or not. If authz returns no errors, incoming requests reach the destination. Otherwise, the client will receive an HTTP error code based on the Envoy configuration. This commit adds authorization to the existing service.

Plan

External authorization service (ext_authz)

Envoy allows you to use HTTP or gRPC service for external authorization, whereas gRPC service can be either Envoy’s service or Google’s one. We will use Envoy’s one.

What is the “authz” service in a nutshell?

authz, in our case, is a gRPC service that implements Envoy Authorization service. with one method, Check.

service Authorization {
  // Performs authorization check based on the attributes associated with the
  // incoming request, and returns status `OK` or not `OK`.
  rpc Check(CheckRequest) returns (CheckResponse) {
  }
}

Inside the implementation we have an access to HTTP headers. Depending on with_request_body setting HTTP body can also be presented. Let’s look at our implementation:

func (a *Server) Check(ctx context.Context, req *auth.CheckRequest) (*auth.CheckResponse, error) {
	headers := req.Attributes.Request.Http.Headers

	fmt.Println("=== Request headers ===")
	for h, v := range headers {
		fmt.Printf("%s: %s\n", h, v)
	}
	fmt.Println("=======================")

	if headers["token"] != "abc" {
		return denided(401, "unauthenticated"), nil
	}

	return allowed(), nil
}

Here we grab HTTP headers from the request and check if header token has a valid value abc. If so, request is authenticated and will be forwarded to upstream service-one. Otherwise, client receive a response with HTTP 401 Unuathorized. For debug purposes we print all of the HTTP header. Among them you can find header set by Envoy.

=== Request headers ===
te: trailers
user-agent: curl/7.79.1
token: abc
x-forwarded-proto: http
x-envoy-original-path: /v1/hello?name=Bazel
accept: */*
x-envoy-original-method: GET
:authority: localhost:8080
x-request-id: d7fce8bf-b90a-4d5f-87b8-988e9bea6f88
:method: POST
content-type: application/grpc
x-envoy-auth-partial-body: false
:path: /svc.ServiceOne/Hello
=======================

denied/allowed functions

It’s helper functions that make code a bit clear. The first function is denied. It accepts two params:

  • code is an HTTP response code.
  • body is an HTTP body.

and returns *auth.CheckResponse with HttpResponse field initialized as &auth.CheckResponse_DeniedResponse.

func denided(code int32, body string) *auth.CheckResponse {
	return &auth.CheckResponse{
		Status: &status.Status{Code: code},
		HttpResponse: &auth.CheckResponse_DeniedResponse{
			DeniedResponse: &auth.DeniedHttpResponse{
				Status: &envoy_type.HttpStatus{
					Code: envoy_type.StatusCode(code),
				},
				Body: body,
			},
		},
	}
}

The response for requests with invalid token will look like:

% curl -i \
  -H 'token: invalid' \
  http://localhost:8080/v1/hello\?name\=Bazel

HTTP/1.1 401 Unauthorized
content-length: 15
content-type: text/plain
date: Wed, 13 Oct 2021 20:59:32 GMT
server: envoy

unauthenticated

The second function allowed is similar to denied.

func allowed() *auth.CheckResponse {
	return &auth.CheckResponse{
		Status: &status.Status{Code: int32(codes.OK)},
		HttpResponse: &auth.CheckResponse_OkResponse{
			OkResponse: &auth.OkHttpResponse{
				Headers: []*core.HeaderValueOption{
					{
						Header: &core.HeaderValue{
							Key:   "x-custom-header-propagated-to-upstream-service",
							Value: "bla-bla-bla",
						},
					},
				},
				HeadersToRemove: []string{"token"},
			},
		},
	}
}

But it has three main differences:

  • HttpResponse field is initialized as &auth.CheckResponse_OkResponse.
  • OkHttpResponse.Header allows you to specify additional headers which will be propagated to upstream service as gRPC metadata. See service-one for details below.
  • HeadersToRemove allows you to remove a header from propagation, i.e., headers listed here will not be sent to the upstream service. In our case, we don’t want to expose security tokens anywhere outside tne authz service.

Envoy configuration

Now, we have to somehow to say Envoy to use this new service. For this we change Envoy config next way:

We add new cluster, which point to authz service. It’s the same as for service-one:

- name: authz
  connect_timeout: 1.25s
  type: logical_dns
  lb_policy: round_robin
  dns_lookup_family: V4_ONLY
  http2_protocol_options: {}
  load_assignment:
    cluster_name: authz
    endpoints:
      - lb_endpoints:
          - endpoint:
              address:
                socket_address:
                  address: authz.%{namespace}.svc.cluster.local
                  port_value: 5000

And add one more filter to the filter_chains of our grpc-listener:

- name: envoy.filters.http.ext_authz
  typed_config:
    "@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz
    grpc_service:
      envoy_grpc:
        cluster_name: authz
      timeout: 0.5s
    transport_api_version: V3
    failure_mode_allow: false
    with_request_body:
      max_request_bytes: 8192
      allow_partial_message: true
      pack_as_bytes: true
    status_on_error:
      code: 503

Here:

  • With

    grpc_service:
      envoy_grpc:
        cluster_name: authz
    

    we specify the type of authz service (envoy_grpc) and cluster name.

  • failure_mode_allow: false means do not forward requests if authz service is unavailable.

  • With

      with_request_body:
        max_request_bytes: 8192
        allow_partial_message: true
        pack_as_bytes: true
    

    we allow to forward HTTP body to authz service.

  • With

      status_on_error:
        code: 503
    

    we define which HTTP status to return in case when authz service is unavailable. HTTP 503 here is used for distinguishing actual service unavailability and unauthorized access HTTP 403.

service-one

We also modified a Hello method implementation to grab the metadata.

func (s *Server) Hello(ctx context.Context, req *pb.HelloRequest) (*pb.HelloResponse, error) {
	md, ok := metadata.FromIncomingContext(ctx)
	if ok {
		fmt.Println("=== metadata ===")
		for k, v := range md {
			fmt.Printf("%s: %#v\n", k, v)
		}
		fmt.Println("================")
	}

	header := metadata.Pairs("set-cookie", "service-one")
	grpc.SendHeader(ctx, header)

	return &pb.HelloResponse{
		Body: "Hello, " + req.Name,
	}, nil
}

This is how metadata looks like:

=== metadata ===
user-agent: []string{"curl/7.79.1"}
x-forwarded-proto: []string{"http"}
content-type: []string{"application/grpc"}
:authority: []string{"localhost:8080"}
x-request-id: []string{"d7fce8bf-b90a-4d5f-87b8-988e9bea6f88"}
x-envoy-original-method: []string{"GET"}
x-custom-header-propagated-to-upstream-service: []string{"bla-bla-bla"}
accept: []string{"*/*"}
x-envoy-original-path: []string{"/v1/hello?name=Bazel"}
x-envoy-expected-rq-timeout-ms: []string{"60000"}
================

With two lines:

	header := metadata.Pairs("set-cookie", "service-one")
	grpc.SendHeader(ctx, header)

we can set HTTP headers and send it back to the client.

curl -i \
  -H 'token: abc' \
  http://localhost:8080/v1/hello\?name\=Bazel

HTTP/1.1 200 OK
content-type: application/json
set-cookie: service-one
x-envoy-upstream-service-time: 1
grpc-status: 0
grpc-message:
content-length: 28
date: Wed, 13 Oct 2021 21:46:13 GMT
server: envoy

{
 "body": "Hello, Bazel"
}
% curl -i \
  -H 'token: invalid' \
  http://localhost:8080/v1/hello\?name\=Bazel

HTTP/1.1 401 Unauthorized
content-length: 15
content-type: text/plain
date: Wed, 13 Oct 2021 20:59:32 GMT
server: envoy

unauthenticated

Now service-one is guarded by authz service.