SEANK.H.LIAO

Envoy ExtAuth gRPC service with Go

pushing auth to the network

envoy

envoy can do external authorization (checking each request if it should be allowed to continue) over http or grpc.

So naturally, i wanted to try it. The raw proto files are in envoyproxy/envoy, there's a read only mirror of just the protos in envoyproxy/data-plane-api and there's envoyproxy/go-control-plane which has other stuff, but more importantly, pregenerated protobuf/grpc Go code that we can import.

So a very simple partial implementation would look like:

  1package authd
  2
  3import (
  4        "context"
  5        "net/http"
  6
  7        envoy_config_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
  8        envoy_service_auth_v3 "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3"
  9        envoy_type_v3 "github.com/envoyproxy/go-control-plane/envoy/type/v3"
 10        "google.golang.org/genproto/googleapis/rpc/status"
 11        "google.golang.org/grpc/codes"
 12)
 13
 14type sessionStore interface {
 15        GetUser(string) string
 16}
 17
 18type GServer struct {
 19        authURL      string
 20        cookieName   string
 21        sessionStore sessionStore
 22
 23        envoy_service_auth_v3.UnimplementedAuthorizationServer
 24}
 25
 26func (s *GServer) Check(ctx context.Context, req *envoy_service_auth_v3.CheckRequest) (*envoy_service_auth_v3.CheckResponse, error) {
 27        sess := s.extractSession(req)
 28        if sess == "" {
 29                return s.unauthorized("no session"), nil
 30        }
 31        user := s.sessionStore.GetUser(sess)
 32        if user == "" {
 33                return s.unauthorized("no user"), nil
 34        }
 35        return s.ok(user), nil
 36}
 37
 38func (s *GServer) ok(user string) *envoy_service_auth_v3.CheckResponse {
 39        return &envoy_service_auth_v3.CheckResponse{
 40                Status: &status.Status{
 41                        Code: int32(codes.OK),
 42                },
 43                HttpResponse: &envoy_service_auth_v3.CheckResponse_OkResponse{
 44                        OkResponse: &envoy_service_auth_v3.OkHttpResponse{
 45                                Headers: []*envoy_config_core_v3.HeaderValueOption{
 46                                        {
 47                                                Header: &envoy_config_core_v3.HeaderValue{
 48                                                        Key:   "authd_user",
 49                                                        Value: user,
 50                                                },
 51                                        },
 52                                },
 53                        },
 54                },
 55        }
 56}
 57
 58func (s *GServer) unauthorized(msg string) *envoy_service_auth_v3.CheckResponse {
 59        return &envoy_service_auth_v3.CheckResponse{
 60                Status: &status.Status{
 61                        Code:    int32(codes.PermissionDenied),
 62                        Message: msg,
 63                },
 64                HttpResponse: &envoy_service_auth_v3.CheckResponse_DeniedResponse{
 65                        DeniedResponse: &envoy_service_auth_v3.DeniedHttpResponse{
 66                                Status: &envoy_type_v3.HttpStatus{
 67                                        Code: envoy_type_v3.StatusCode_Found,
 68                                },
 69                                Headers: []*envoy_config_core_v3.HeaderValueOption{
 70                                        {
 71                                                Header: &envoy_config_core_v3.HeaderValue{
 72                                                        Key:   "location",
 73                                                        Value: s.authURL,
 74                                                },
 75                                        },
 76                                },
 77                        },
 78                },
 79        }
 80}
 81
 82func (s *GServer) extractSession(req *envoy_service_auth_v3.CheckRequest) string {
 83        switch {
 84        case req == nil:
 85                return ""
 86        case req.Attributes == nil:
 87                return ""
 88        case req.Attributes.Request == nil:
 89                return ""
 90        case req.Attributes.Request.Http == nil:
 91                return ""
 92        case req.Attributes.Request.Http.Headers == nil:
 93                return ""
 94        }
 95        rawCookies := req.Attributes.Request.Http.Headers["cookie"]
 96        httpReq := http.Request{
 97                Header: map[string][]string{
 98                        "Cookie": {rawCookies},
 99                },
100        }
101        cookie, err := httpReq.Cookie(s.cookieName)
102        if err != nil {
103                return ""
104        }
105        return cookie.Value
106}

and using it in envoy:

 1static_resources:
 2  listeners:
 3    - name: https
 4      address:
 5        socket_address:
 6          address: 0.0.0.0
 7          port_value: 443
 8      listener_filters:
 9        - name: "envoy.filters.listener.tls_inspector"
10      per_connection_buffer_limit_bytes: 32768 # 32 KiB
11      filter_chains:
12        - filter_chain_match:
13            server_names:
14              - auth-test.seankhliao.com
15          transport_socket:
16            name: envoy.transport_sockets.tls
17            typed_config:
18              "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext
19              common_tls_context: *commontls
20          filters:
21            - name: envoy.filters.network.http_connection_manager
22              typed_config:
23                "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
24                stat_prefix: ingress_http
25                use_remote_address: true
26                normalize_path: true
27                merge_slashes: true
28                path_with_escaped_slashes_action: REJECT_REQUEST
29                common_http_protocol_options: *commonhttp
30                http2_protocol_options:
31                  max_concurrent_streams: 100
32                  initial_stream_window_size: 65536 # 64 KiB
33                  initial_connection_window_size: 1048576 # 1 MiB
34                http_filters:
35                  - name: envoy.filters.http.ext_authz
36                    typed_config:
37                      "@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz
38                      grpc_service:
39                        envoy_grpc:
40                          cluster_name: authd
41                      transport_api_version: V3
42                  - name: envoy.filters.http.router
43                route_config:
44                  virtual_hosts:
45                    - name: auth-test
46                      domains:
47                        - auth-test.seankhliao.com
48                      routes:
49                        match:
50                          prefix: /
51                        route:
52                          cluster: medea
53
54  clusters:
55    - name: authd
56      typed_extension_protocol_options:
57        envoy.extensions.upstreams.http.v3.HttpProtocolOptions:
58          "@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions
59          explicit_http_config:
60            http2_protocol_options: {}
61      load_assignment:
62        cluster_name: authd
63        endpoints:
64          lb_endpoints:
65            - endpoint:
66                address:
67                  socket_address:
68                    address: 127.0.0.1
69                    port_value: 28006