summaryrefslogtreecommitdiff
path: root/share/man/ENVOY.md
blob: 716aaaa51580b8e48e39e84d3438a8f2d6309576 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
# Envoy

Envoy Proxy is described as an edge and service proxy. What this means is that
Envoy can take care of managing inbound and outbound networks requests to and
from your application. This allows your application to not to have to worry
about managing key material like OAuth Client secrets, JSON Web Tokens (JWTs),
and other sensitive information.

Envoy provides a plugin system that allows application developers to use built
in plugins to handle things like:

* Redirecting to an Identity Provider
* Doing an OAuth handshake with an OAuth Authorization Server
  * Performing an Authorization Code Grant Exchange
  * Exchanging a refresh token for a new access token
* Validating incoming JSON Web Tokens
* Connecting to a policy decision point to authorize request before forwarding
  them to your application.

Envoy can be run in multiple ways and seems to work best when working as a
sidecar process to your application. The idea behind this is that you would
expose envoy to the externally and use it to reverse proxy requests to your
application that is only accessible via envoy. This is typically configured
using a loopback address for tcp connections. Envoy can speak gRPC and HTTP
quite fluently and the Envoy documentation is fairly extensive.

You can configure Envoy to receive its configuration from a static YAML file or
dynamically by giving it the location of a control plane for it to connect to
and receive its configuration from. Envoy Gateway and Istio are popular control
planes that allow you to manage a fleet of envoy proxies through a central
management point.

In this document I'm going to go over how to configure Envoy in a standalone
mode using static configuration. This configuration is written in YAML and is
provided to the Envoy program as a command line option during startup.

In order to adequately understand what Envoy is providing I will start with
going over the following primitives:

1. Authentication
  * Public Key Cryptography
  * Public Key Infrastructure
  * Digital Signing
1. Authorization
  * Access Control Models
    * DAC
    * RBAC
    * ABAC

After this brief overview I will dive into how to configure Envoy to provide
the bare necessities for booting up a new service with authentication
and authorization delegated to Envoy.

1. Authentication
  * OpenID Connect Provider using `envoy.filters.http.oauth2`
  * JSON Web Token Validation using `envoy.filters.http.jwt_authn`
1. Authorization
  * External policy decision point (PDP) using `envoy.filters.http.ext_authz`

## Pre-requisite Concepts

Authentication is the act of prooving you are who you claim to be.
Authorization is the act of prooving that you are allowed to do what
you're trying to do. The distinction between the two is important because the
context determines which elements are necessary.

An example of this is the difference between commuting via municipal transit
versus commuting via an airplane. The security context between the two modes of
transportation are different therefore the level or rigor applied to
authenticating versus authorizing access to the resource differ. To board a bus
you must present a bus token/ticket to the bus driver before you are able to
board the bus. The bus driver does not require you to verify who you are.
Instead, they are only interested in verifying that you have a valid bus ticket
that has not expired, is for the bus that they operate and is issued from a
legitimate authority (the transit authority). TO ride an airplane you must
provide both your passport and your boarding pass in order to board the plane.
The passport is used to verify that you are who you say you are and the boarding
pass is used to ensure that you have a valid seat on the plane. The passport is
used to authenticate the passenger and the bus ticket/boarding pass is used to
authorize the passenger. The bus and plane are protected resources like an API
and the operator of the API understand the security context the best. They
understand whether a rigorous authentication and authorization check is
warranted or not. The passenger is responsible for obtaining a passport,
boarding pass, bus ticket from trusted and reputable authorities.

```sequence
 +-----------+   +------------+             +-----+
 | Passenger |   | Bus Driver |             | Bus |
 +-----------+   +------------+             +-----+
   |                    |                      |
   |-- request access -->                      |
   |                    |                      |
   |<- request ticket --|                      |
   |                    |                      |
   |-- present ticket --> authorize (bus #, expiration, fake/legit?)
   |                    |                      |
   |<--- grant access --|                      |
   |                    |                      |
   |--- board bus ---------------------------->|

--------------------------------------------------------
   |<--- deny access --|
```

The Bus # indicates the canonical identifier for the resource and
this is similar to accessing a resource exposed via a REST/GraphQL
API. The expiration check ensures that the same token cannot be re-used
indefinitely and that the access granted by the ticket is limited in
scope to prevent abuse of the resource and this is similar to ensuring
that a JWT cannot be used indefinitely. The check to make sure that the
ticket is legitimate and issued from a trusted authority is similar to
a digital signature check. In this example, the bus driver does not need to
authenticate the passenger by verifying that they are who they say they are. The
bus driver does not care. The bus driver only cares about whether or not they
carry a token that awards them access to the resource. In this scenario the
passenger could give the token to someone else (for example a child) so that
they can access the resource. The security context of this resource does not
warrant the need for authentication and only requires authorization.

```uml
+-----------+            +----------------+    +----------------+  +-------+
| Passenger |            | Security Agent |    | Boarding Agent |  | Plane |
+-----------+            +----------------+    +----------------+  +-------+
   |                              |                    |               |
   |-- request access to gate  -->|                    |               |
   |<--- request boarding pass ---|                    |               |
   |                              |                    |               |
   |-- present boarding pass ---->|                    |               |
   |                              |-> validate pass    |               |
   |<-- allow access to gate -----|                    |               |
   |                              |                    |               |
   |-- request access to board plane ----------------->|               |
   |<--- request passport -----------------------------|               |
   |-- present passport ------------------------------>|               |
   |<--- request boarding pass ------------------------|               |
   |-- present boarding pass ------------------------->|               |
   |<----- allow access to board plane                 |               |
   |                              |                    |               |
   |--- board plane  ------------------------------------------------->|
```

To board a plane you must pass through more security checks before you can
access the airplane. That is because flying in an airplane is a high security
context that requires additional checks to ensure the safety of everyone and the
risk of allowing access to a bad actor has more severe consequences. To board
the airplane you must pass through the security checkpoint by presenting a valid
boarding pass for a flight. This check ensures that we do not allow people into
the gate that do not have a valid pass. A valid pass is one that hasn't already
been used, is for a flight that is set to take off in the future and is for a
known and registered airline. Depending on whether the flight is a domestic or
international flight the gate may require other forms of proof of access. Once
the passenger has made it to the gate they are required to provide a passport
and boarding pass to an airline agent before they are allowed to board the
aircraft. This ensures that everyone who is aboard the airplane is known ahead
of time and that known bad actors are not allowed to board the aircraft. The
airline agent performs an authentication AND authorization check. The airplane
is a metaphor for a high security context that the operators of the airplane
understand. The credit card company and each intermediate authority that was
used to ensure entry do not determine the access controls for gaining entry into
the plane.

### Authentication

Authentication is the act of verifying that an entity is who they say they are.

How do we do this on the internet? To accomplish this we depend on public key
cryptography which is a form of asymmetic crypto. In this style of crypto each
party has a public and private key. Entities distribute their public keys while
keeping their private keys private. The interesting property of the
public/private key relationship is that messages that are encrypted by either
the public or private key can only be decrypted by the other corresponding key.

#### Confidentiality

So if I give you my public key then you can encrypt a message with my public key
and send that message to me. Only I can decrypt that message using my private
key. This ensures confidentiality so that the ciphertext produced can be snooped
by anyone but only the recipient can convert the ciphertext back into plaintext.

#### Authenticity

To ensure that a message originated from the entity that claims to have sent the
message an additional signature can be appended to the message. The signature
can contain any arbitrary text but is usually a hash (e.g. SHA256) of the
original plaintext message and encrypted using the private key of the sender.
I'll explain below why a hash is used below. If the recipient has the public key of the sender
then they can decrypt the signature using the public key of the sender. If
signature can be decrypted without an error then we can trust that the message
did in fact originate from the sender. This authenticates the message.

#### Integrity

When a recipient receives a message from a sender the recipient also needs to
verify that the message wasn't altered. If the signature of the message includes
an encrypted hash then the recipient can compute a hash of the plaintext message
and compare it with the hash in the encrypted signature. This ensures that the
message hasn't been tampered with.

In order for us to be able to trust JSON Web Tokens we need public/private key
pairs that we can use to validate the authenticity and integrity of the token.
It is also possible to encrypt the JWT body but this isn't necessary and this is
why storing sensitive information like personally identifiable information in a
JWT claim is not recommended.

The problem of sharing and distributing public keys is solved using Public Key
Infrastructure (PKI). PKI provides a mechanism for distributing X.509
certificates that include metadata and the public keys for different entities.
X.509 certificates can include a digital signature from other authorities
provides a chain of trust. Each X.509 certificate stores in the CA trust store
on your computer is a self signed certificate that is considered trusted.
Intermediate certificates can be traced back to a root certificate and provides
a web of trust. i.e. I trust this JWT because it was signed by a public key that
I found in this intermediate certificate that was signed by a public key found
in this root certificate that is in my operating systems root certificate
authority trust store. Typically, organizations will operate an internal
certificate authority that can sign intermediate certificates that can be
installed in the trust store for internal services. This makes it easier to
issue internal certificates without need direct access to private keys that can
be abused by bad actors. I apologize for the weak summary of this but a cursory
knowledge of how this works is important for understanding authentication.

When a service federates user authentication to an Identity Provider (.e.g. SAML
IdP, OIDC Provider) the transaction between the service (i.e. SAML Service
Provider, OIDC Relaying Party) depends on an exchange of public key information
ahead of time (AoT). Without this pre-prequisite, none of the downstream
assumptions about user authentication is valid.

The `id_token` in the OpenID Connect (OIDC) workflow represents the authentication context.
This _DOES NOT_ represent an authorization context.

TODO:: Describe the sections of a JWT and the schema of the `id_token`.

### Authorization

Authorization is the act of verifying that a party is allowed to perform a
specific action against a resource. This is separate from Authentication because
in many cases the Resource Server providing access to the Resource does not need
to know who the party is. This creates a decoupling that allows an API to
determine just how much information it actually needs to know about the party
making the request. See the Bus vis Airplane example above for an explanation.

OAuth was designed as a protocol for delegating authorization to an intermediate
entity so that this entity could access resources on behalf of a user without
needing full access to everything that the user has access too. By adhering to
the OAuth2 protocol flow we can ensure that requests made on behalf of end users
do not operate at the highest level of privilege available to them. We can
ensure that requests that are made on behalf of end users use the lowest level
of privilege necessary for the service (OAuth client) to perform their desired
function.

The OAuth2 `access_token` represents the authorization context and this is
distinct from the `id_token` which represents the authentication context. The
`id_token` tells us who the currently logged in user is but it _should not_ be
used to make authorization decisions. Authorization decisions should be made
using the `access_token` because this represents the delegated authorization
access granted to the service that the token was minted for. In general, most
API's that receive a request should make authorization decisions based on the
privileges granted to the `access_token`.

The authorization server that generates the `access_token` should only grant
just enough privileges to this token that is required by the service (OAuth
client) that this token is intended for.

The separation of the authentication context from the authorization context is
incredibly important. This ensures that services cannot access resources based
on the full scope of access that a user has but rather the delegated authorized
access that is granted to an access token. The access token represents the
low-privilege session for a specific service. A single `id_token` can be used
across services to allow the service to know who is logged in but each service
should have its own access token for each user based on the permissions that the
service declares that it needs and the permissions that the user agrees to give
it. I need to say this again because understanding this is crucial!

## Envoy Architecture

Given all the concerns listed above this is where Envoy shines. It can be used
to take care of Authentication via an OpenID Connect transaction by slightly
abusing the built-in `envoy.filters.http.oauth2` HTTP filter. It can also be
used to validate any incoming JWTs via the `envoy.filters.http.jwt_authn` HTTP
filter. Finally, we can use the `envoy.filters.http.ext_authz` HTTP filter to
delegate authorization decisions to an external policy decision point (PDP).

I wrote Sparkle as a proof-of-concept to model these ideas using Envoy. Before
we dive into the configuration I want to quickly go over the high level
architecture of how these pieces work together.

The proposed architecture ensures that authorization decisions are made consistently at the edge before requests reach the application.

### Authentication Flow

```mermaid
sequenceDiagram
    participant User
    box grey Docker Image
      participant Envoy
      participant authzd
      participant sparkled
    end
    participant OIDC Provider

    User->>Envoy: GET /dashboard (no auth)
    Envoy->>Envoy: OAuth2 filter detects no auth
    Envoy->>User: Redirect to OIDC Provider
    User->>OIDC Provider: Login
    OIDC Provider->>User: Redirect to /callback with code
    User->>Envoy: GET /callback?code=...
    Envoy->>OIDC Provider: Exchange code for tokens
    OIDC Provider->>Envoy: Return tokens (ID, access, refresh)
    Envoy->>User: Set cookies & redirect to /dashboard
```

The `envoy.filters.http.oauth2` HTTP filter can be configured to detect an
unauthenticated request and intercept all inbound requests by redirecting the
user-agent to the hard-coded OAuth Authorization Server endpoints. This filter
does not support the OIDC Discovery endpoint but an Envoy Gateway
[plugin](https://gateway.envoyproxy.io/docs/tasks/security/oidc/) does.
Envoy Gateway is a control plane that is outside the scope of this document.

```yaml
static_resources:
  listeners:
    - filter_chains:
        - filters:
            - name: envoy.filters.network.http_connection_manager
              typed_config:
                "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
                http_filters:
                  - name: envoy.filters.http.oauth2
                    typed_config:
                      "@type": type.googleapis.com/envoy.extensions.filters.http.oauth2.v3.OAuth2
                      config:
                        auth_scopes:
                          - email
                          - openid
                          - profile
                        authorization_endpoint: "https://gitlab.com/oauth/authorize"
                        credentials:
                          client_id: "OAUTH_CLIENT_ID"
                          cookie_names:
                            id_token: id_token
                        redirect_path_matcher:
                          path:
                            exact: /callback
                        redirect_uri: "%REQ(x-forwarded-proto)%://%REQ(:authority)%/callback"
                        signout_path:
                          path:
                            exact: /signout
                        token_endpoint:
                          uri: "https://gitlab.com/oauth/token"
                        use_refresh_token: true
```

### Authorization Flow

```mermaid
sequenceDiagram
    participant User
    box grey Docker Image
      participant Envoy
      participant authzd
      participant sparkled
    end
    participant OIDC Provider

    User->>Envoy: GET /dashboard (with cookies)
    Envoy->>Envoy: Extract ID token from cookie
    Envoy->>Envoy: JWT filter validates & extracts claims
    Note right of Envoy: Sets headers:<br/>x-jwt-payload<br/>x-jwt-claim-sub

    Envoy->>authzd: Check authorization (gRPC)
    Note right of authzd: Request includes:<br/>- Method & Path<br/>- Headers (inc. cookies)<br/>- JWT claims
    authzd->>authzd: Evaluate authorization rules
    authzd->>Envoy: Return OK/Denied decision

    alt Authorization OK
        Envoy->>sparkled: Forward request with JWT headers
        sparkled->>sparkled: Extract user from x-jwt-claim-sub
        sparkled->>User: Return dashboard content
    else Authorization Denied
        Envoy->>User: Return 401 Unauthorized
    end
```

The ID token can be validated using the `envoy.filters.http.jwt_authn` HTTP
filter. The following configuration will look for an `id_token` cookie and then
parse the value, validate it against the list of keys specified at the
`remote_jwks` uri and then it will inject a header called `x-jwt-payload` with
the valid JWT as well as the `x-jwt-claim-sub` with the body section of the JWT.
This filter ensures ensures the integrity and authenticity of the detected JWT
and will immediately reject tokens that are invalid.

```yaml
static_resources:
  listeners:
    - filter_chains:
        - filters:
            - name: envoy.filters.network.http_connection_manager
              typed_config:
                "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
                http_filters:
                  - name: envoy.filters.http.oauth2
                    # ...
                  - name: envoy.filters.http.jwt_authn
                    typed_config:
                      "@type": type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.JwtAuthentication
                      providers:
                        gitlab_provider:
                          audiences:
                            - OAUTH_CLIENT_ID
                          claim_to_headers:
                            - header_name: x-jwt-claim-sub
                              claim_name: sub
                          forward: true
                          forward_payload_header: x-jwt-payload
                          from_cookies:
                            - id_token
                          issuer: https://gitlab.com
                          remote_jwks:
                            http_uri:
                              uri: https://gitlab.com/oauth/discovery/keys
                      rules:
                        - match:
                            path: /
                          requires:
                            provider_name: gitlab_provider
```

The `envoy.filters.http.ext_authz` filter can be used to forward the incoming HTTP request to an external
policy decision point that can be used to make the authorization decision. For
Sparkle the PDP is hosted as a sidecar process called `authzd` that makes the
authorization decision specifically on the contents of the HTTP request.

```yaml
                  # ...
                  - name: envoy.filters.http.oauth2
                  # ...
                  - name: envoy.filters.http.jwt_authn
                  # ...
                  - 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: authzd
                      failure_mode_allow: false
```

The external authorization service must implement the [`CheckRequest` protobuf](https://github.com/envoyproxy/envoy/blob/04378898516847d1107c5b15c22ac602ff06372c/api/envoy/service/auth/v3/external_auth.proto#L35) service definition.
An example of this can be found in the Sparkle repo. Below is an example
snippet:

```golang
package authz

import (
	"context"

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

type CheckService struct {
	auth.UnimplementedAuthorizationServer
}

func (svc *CheckService) Check(ctx context.Context, request *auth.CheckRequest) (*auth.CheckResponse, error) {
	if svc.isAllowed(ctx, request) {
		return svc.OK(ctx), nil
	}
	return svc.Denied(ctx), nil
}

// ...

func (svc *CheckService) OK(ctx context.Context) *auth.CheckResponse {
	return &auth.CheckResponse{
		Status: &status.Status{
			Code: int32(codes.OK),
		},
		HttpResponse: &auth.CheckResponse_OkResponse{
			OkResponse: &auth.OkHttpResponse{
				Headers:              []*core.HeaderValueOption{},
				HeadersToRemove:      []string{},
				ResponseHeadersToAdd: []*core.HeaderValueOption{},
			},
		},
	}
}

func (svc *CheckService) Denied(ctx context.Context) *auth.CheckResponse {
	return &auth.CheckResponse{
		Status: &status.Status{
			Code: int32(codes.PermissionDenied),
		},
		HttpResponse: &auth.CheckResponse_DeniedResponse{
			DeniedResponse: &auth.DeniedHttpResponse{
				Status: &types.HttpStatus{
					Code: types.StatusCode_Unauthorized,
				},
				Headers: []*core.HeaderValueOption{},
			},
		},
	}
}
```

## Envoy Configuration

Let's dive into the envoy configuration.