Project: Contour (Orchestration & Management / Service Proxy)
File: jwt-verification-design.md

This document describes a design for performing JSON Web Token (JWT) verification for requests to virtual hosts hosted by Contour. JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. (ref. https://jwt.io/introduction) Envoy Proxy has built-in support for verifying JWTs that are attached to incoming requests, via the . (Note, this document will use the term JWT verification to describe this process). Specifically, Envoy can verify the signature, audience, issuer and time restrictions of a JWT. If verification fails, the request will be rejected. It's important to note that this filter does not itself obtain a JWT for an incoming request; the request must already have one attached. Contour does not currently have support for configuring the JWT authentication filter. This document proposes a design for adding that support to Contour's custom resource, `HTTPProxy`. JWT verification for requests to TLS-enabled virtual hosts. Expose a subset of the Envoy filter configuration to cover the most common use cases. Be able to easily expose additional configuration settings if/when needed in the future. JWT verification for requests to non-TLS enabled virtual hosts. Exposing all possible Envoy configuration settings. Supporting end-to-end OAuth2/OIDC flows. Contour's `HTTPProxy` resource will get a new optional field, `spec.virtualhost.jwtProviders`, to define the details of how to verify JWTs for requests to a given virtual host. This field will only be supported for virtual hosts for which Envoy is terminating TLS. The structure of this field will be similar to the , with some simplifications. Specifically, a provider will define an issuer, 0+ audiences, and a JSON Web Key Set (JWKS) that can be used to verify a JWT (see for more information). Any number of providers can be defined for a virtual host, to allow different routes to be verified differently. At most one provider can be marked as the "default", meaning it will automatically be applied as a requirement to all routes unless they explicitly opt out (more below). `HTTPProxy` routes will also get a new optional field, `spec.routes.jwtVerificationPolicy`, to provide details on how to apply JWT verification. The JWT verification policy will allow a specific named provider to be required for the route if there is no default or if a provider other than the default should apply for the route. It will also allow explicitly opting out of using the proxy's default provider. Contour will validate the contents of `spec.virtualhost.jwtProviders` and `spec.routes.jwtVerificationPolicy` if present, and will configure the JWT authentication filter on the HTTP Connection Manager for the relevant virtual host. Contour will also add a CDS cluster for the remote JWKS, as required by

JWT verification will only be supported for TLS-enabled virtual hosts because (a) JWTs generally should not be transmitted in cleartext; and (b) all plain HTTP virtual hosts share a single HTTP Connection Manager & associated filter config, which creates challenges when configuring different JWT verification providers and rules for different virtual hosts. This constraint may be revisited at a later date if a compelling use case is identified. The detailed structure of the new fields is shown via YAML below: ```yaml apiVersion: projectcontour.io/v1 kind: HTTPProxy metadata: name: jwt-verification spec: virtualhost: fqdn: example.com tls: secretName: tls-cert jwtProviders: name: provider-1 default: true issuer: foo.com audiences: audience-1 audience-2 remoteJWKS: httpURI: uri: https://example.com/jwks.json timeout: 1s validation: caSecret: ca-crt subjectName: example.com cacheDuration: 5m localJWKS: secretName: my-jwks key: jwks.json routes: conditions: prefix: / jwtVerificationPolicy: requires: provider-1 disabled: false services: name: s1 port: 80 conditions: prefix: /js jwtVerificationPolicy: disabled: true services: name: s1 port: 80 ``` It is worth highlighting some of the configuration options that Envoy's filter has, that Contour will not expose, at least not initially: non-default extract locations for the JWT (the default locations are (1) the `Authorization` header using the Bearer schema; and (2) the `access_token` query parameter, in that order). complex rule requirements (e.g. `requiresany`, `requiresall`) rule route matches are limited to what Contour already supports for routing (path prefix and some header matching) Contour's API is structured such that these and other more complex/less common options may be added at a later date, if there is user demand for them. A complete/valid HTTPProxy using JWT verification is shown below: ```yaml apiVersion: projectcontour.io/v1 kind: HTTPProxy metadata: name: jwt-verification-proxy spec: virtualhost: fqdn: example.com tls: secretName: tls-cert jwtProviders: name: provider-1 default: true issuer: example.com audiences: audience-1 audience-2 remoteJWKS: httpURI: uri: https://example.com/jwks.json timeout: 1s cacheDuration: 5m routes: conditions: prefix: / services: name: s1 port: 80 conditions: prefix: /css jwtVerificationPolicy: disabled: true services: name: s1 port: 80 ``` Envoy also has an , which supports end-to-end OAuth2/OIDC flows. This provides related but separate functionality to the JWT authentication filter. This shows an example of how to use the OAuth2 and JWT filters together in Envoy. Contour , but it will be designed and implemented separately. An alternate API design is to define JWT verification rules separately from routing rules, as shown in the below YAML: ```yaml apiVersion: projectcontour.io/v1 kind: HTTPProxy metadata: name: jwt-verification-proxy spec: virtualhost: fqdn: example.com tls: secretName: tls-cert jwtVerificationPolicy: providers: name: provider-1 issuer: example.com audiences: audience-1 audience-2 remoteJWKS: httpURI: uri: https://example.com/jwks.json timeout: 1s cacheDuration: 5m rules: match: prefix: /js match: prefix: / providerName: provider-1 routes: conditions: prefix: / services: name: s1 port: 80 ``` This design has the potential benefit of reducing duplication in the routing rules, by not requiring an entire route to be duplicated in order to express an "except" condition for JWT verification. It also allows the user to have direct control over the order in which JWT rules are applied, versus being subject to Contour's rules for sorting routes. However, this design results in two separate places where route-related behavior are defined, and also creates cognitive overhead for users by having two separate ways of ordering match criteria. This option largely follows the design laid out above. JWT providers are defined on the root HTTPProxy. Routes on either the root HTTPProxy, or any included HTTPProxies, can then opt into JWT verification by specifying `jwtProvider:

This option puts the responsibility for opting into JWT verification on the owner of the child HTTPProxy. In this option, the `Include` type itself can opt into JWT verification, by specifying `jwtProvider: [name]`. All routes in the included HTTPProxy are then opted into JWT verification. An additional field is added to routes to explicitly opt out of JWT verification (`disableJWTVerification: true`). This option allows the owner of the root HTTPProxy to opt the child HTTPProxy's routes into JWT verification. The downside of this model is that it is somewhat inconsistent with the single HTTPProxy model, where routes have to explicitly opt into verification. In this option, each root HTTPProxy only allows a single JWT provider. All routes in the root HTTPProxy, as well as its included HTTPProxies, have JWT verification enabled by default. An additional field is added to routes to explicitly opt out of JWT verification (`disableJWTVerification: true`). This option allows the owner of the root HTTPProxy to opt all routes (including any child HTTPProxies' routes) into JWT verification. The owner of the child HTTPProxies can still opt out specific routes if needed. The downside of this option is that it does limit each root HTTPProxy to a single provider. JWT verification will be an optional feature that is disabled by default. Existing users should not be affected by its addition. HTTPProxy with a single provider, marked as the default: ```yaml apiVersion: projectcontour.io/v1 kind: HTTPProxy metadata: name: jwt-verification-proxy spec: virtualhost: fqdn: example.com tls: secretName: tls-cert jwtProviders: name: provider-1 default: true issuer: example.com audiences: audience-1 audience-2 remoteJWKS: httpURI: uri: https://example.com/jwks.json timeout: 1s cacheDuration: 5m routes: conditions: prefix: / services: name: s1 port: 80 conditions: prefix: /css jwtVerificationPolicy: disabled: true services: name: s1 port: 80 ``` HTTPProxy with a single provider, NOT marked as the default: ```yaml apiVersion: projectcontour.io/v1 kind: HTTPProxy metadata: name: jwt-verification-proxy spec: virtualhost: fqdn: example.com tls: secretName: tls-cert jwtProviders: name: provider-1 issuer: example.com audiences: audience-1 audience-2 remoteJWKS: httpURI: uri: https://example.com/jwks.json timeout: 1s cacheDuration: 5m routes: conditions: prefix: / jwtVerificationPolicy: require: provider-1 services: name: s1 port: 80 conditions: prefix: /css services: name: s1 port: 80 ``` HTTPProxy with multiple providers, with one marked as the default: ```yaml apiVersion: projectcontour.io/v1 kind: HTTPProxy metadata: name: jwt-verification-proxy spec: virtualhost: fqdn: example.com tls: secretName: tls-cert jwtProviders: name: provider-1 default: true issuer: example.com audiences: audience-1 audience-2 remoteJWKS: httpURI: uri: https://example.com/jwks.json timeout: 1s cacheDuration: 5m name: provider-2 issuer: foo.com remoteJWKS: httpURI: uri: https://foo.com/jwks.json timeout: 1s cacheDuration: 5m routes: conditions: prefix: / services: name: s1 port: 80 conditions: prefix: /foo jwtVerificationPolicy: require: provider-2 services: name: s2 port: 80 conditions: prefix: /css jwtVerificationPolicy: disabled: true services: name: s1 port: 80 ``` HTTPProxy with a single provider, marked as the default, using inclusion: ```yaml apiVersion: projectcontour.io/v1 kind: HTTPProxy metadata: name: jwt-verification-root-proxy spec: virtualhost: fqdn: example.com tls: secretName: tls-cert jwtProviders: name: provider-1 default: true issuer: example.com audiences: audience-1 audience-2 remoteJWKS: httpURI: uri: https://example.com/jwks.json timeout: 1s cacheDuration: 5m includes: name: jwt-verification-child-proxy conditions: prefix: /service-1 apiVersion: projectcontour.io/v1 kind: HTTPProxy metadata: name: jwt-verification-child-proxy routes: conditions: prefix: / services: name: s1 port: 80 conditions: prefix: /css jwtVerificationPolicy: disabled: true services: name: s1 port: 80 ```