Skip to content

UmutComlekci/envoy_external_authorization

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

6 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Envoy External Authorization server (envoy.ext_authz) HelloWorld

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:

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 of course 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

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: The tutorial runs envoy directly here but you can use the docker image as well.

Start with docker-compose

You can simple use docker-compose file the testing.

docker-compose up --remove-orphans --build --force-recreate

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).

$ go run backend_server/http_server.go 

Start External Authorization server

$ go run authz_server/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
          - name: envoy.lua
            config:
              inline_code: |
                function envoy_on_request(request_handle)
                  request_handle:logInfo('>>> LUA envoy_on_request Called')
                  --buf = request_handle:body()
                  --bufbytes = buf:getBytes(0, buf:length())
                  --request_handle:logInfo(bufbytes)
                end
                
                function envoy_on_response(response_handle)
                  response_handle:logInfo('>>> LUA envoy_on_response Called')
                  response_handle:headers():add("X-Custom-Header-From-LUA", "bar")
                end

  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