blog

12021-09-25

SEAN K.H. LIAO

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:

package authd

import (
        "context"
        "net/http"

        envoy_config_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
        envoy_service_auth_v3 "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3"
        envoy_type_v3 "github.com/envoyproxy/go-control-plane/envoy/type/v3"
        "google.golang.org/genproto/googleapis/rpc/status"
        "google.golang.org/grpc/codes"
)

type sessionStore interface {
        GetUser(string) string
}

type GServer struct {
        authURL      string
        cookieName   string
        sessionStore sessionStore

        envoy_service_auth_v3.UnimplementedAuthorizationServer
}

func (s *GServer) Check(ctx context.Context, req *envoy_service_auth_v3.CheckRequest) (*envoy_service_auth_v3.CheckResponse, error) {
        sess := s.extractSession(req)
        if sess == "" {
                return s.unauthorized("no session"), nil
        }
        user := s.sessionStore.GetUser(sess)
        if user == "" {
                return s.unauthorized("no user"), nil
        }
        return s.ok(user), nil
}

func (s *GServer) ok(user string) *envoy_service_auth_v3.CheckResponse {
        return &envoy_service_auth_v3.CheckResponse{
                Status: &status.Status{
                        Code: int32(codes.OK),
                },
                HttpResponse: &envoy_service_auth_v3.CheckResponse_OkResponse{
                        OkResponse: &envoy_service_auth_v3.OkHttpResponse{
                                Headers: []*envoy_config_core_v3.HeaderValueOption{
                                        {
                                                Header: &envoy_config_core_v3.HeaderValue{
                                                        Key:   "authd_user",
                                                        Value: user,
                                                },
                                        },
                                },
                        },
                },
        }
}

func (s *GServer) unauthorized(msg string) *envoy_service_auth_v3.CheckResponse {
        return &envoy_service_auth_v3.CheckResponse{
                Status: &status.Status{
                        Code:    int32(codes.PermissionDenied),
                        Message: msg,
                },
                HttpResponse: &envoy_service_auth_v3.CheckResponse_DeniedResponse{
                        DeniedResponse: &envoy_service_auth_v3.DeniedHttpResponse{
                                Status: &envoy_type_v3.HttpStatus{
                                        Code: envoy_type_v3.StatusCode_Found,
                                },
                                Headers: []*envoy_config_core_v3.HeaderValueOption{
                                        {
                                                Header: &envoy_config_core_v3.HeaderValue{
                                                        Key:   "location",
                                                        Value: s.authURL,
                                                },
                                        },
                                },
                        },
                },
        }
}

func (s *GServer) extractSession(req *envoy_service_auth_v3.CheckRequest) string {
        switch {
        case req == nil:
                return ""
        case req.Attributes == nil:
                return ""
        case req.Attributes.Request == nil:
                return ""
        case req.Attributes.Request.Http == nil:
                return ""
        case req.Attributes.Request.Http.Headers == nil:
                return ""
        }
        rawCookies := req.Attributes.Request.Http.Headers["cookie"]
        httpReq := http.Request{
                Header: map[string][]string{
                        "Cookie": {rawCookies},
                },
        }
        cookie, err := httpReq.Cookie(s.cookieName)
        if err != nil {
                return ""
        }
        return cookie.Value
}

and using it in envoy:

static_resources:
  listeners:
    - name: https
      address:
        socket_address:
          address: 0.0.0.0
          port_value: 443
      listener_filters:
        - name: "envoy.filters.listener.tls_inspector"
      per_connection_buffer_limit_bytes: 32768 # 32 KiB
      filter_chains:
        - filter_chain_match:
            server_names:
              - auth-test.seankhliao.com
          transport_socket:
            name: envoy.transport_sockets.tls
            typed_config:
              "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext
              common_tls_context: *commontls
          filters:
            - name: envoy.filters.network.http_connection_manager
              typed_config:
                "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
                stat_prefix: ingress_http
                use_remote_address: true
                normalize_path: true
                merge_slashes: true
                path_with_escaped_slashes_action: REJECT_REQUEST
                common_http_protocol_options: *commonhttp
                http2_protocol_options:
                  max_concurrent_streams: 100
                  initial_stream_window_size: 65536 # 64 KiB
                  initial_connection_window_size: 1048576 # 1 MiB
                http_filters:
                  - 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: authd
                      transport_api_version: V3
                  - name: envoy.filters.http.router
                route_config:
                  virtual_hosts:
                    - name: auth-test
                      domains:
                        - auth-test.seankhliao.com
                      routes:
                        match:
                          prefix: /
                        route:
                          cluster: medea

  clusters:
    - name: authd
      typed_extension_protocol_options:
        envoy.extensions.upstreams.http.v3.HttpProtocolOptions:
          "@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions
          explicit_http_config:
            http2_protocol_options: {}
      load_assignment:
        cluster_name: authd
        endpoints:
          lb_endpoints:
            - endpoint:
                address:
                  socket_address:
                    address: 127.0.0.1
                    port_value: 28006