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