External Authorization Support

Starting in version 1.9, Contour supports routing client requests to an external authorization server. This feature can be used to centralize client authorization so that applications don’t have to implement their own authorization mechanisms.

Authorization Architecture

An external authorization server is a server that implements the Envoy external authorization GRPC protocol. Contour supports any server that implements this protocol.

You can bind an authorization server to Contour by creating a ExtensionService resource. This resource tells Contour the service exists, and that it should program Envoy with an upstream cluster directing traffic to it. Note that the ExtensionService resource just binds the server; at this point Contour doesn’t assume that the server is an authorization server.

Once you have created ExtensionService resource, you can bind it to a particular application by referencing it in a HTTPProxy resource. In the virtualhost field, a new authorization field specifies the name of an ExtensionService to bind for the virtual host. When you specify a resource name here, Contour will program Envoy to send authorization checks to the extension service cluster before routing the request to the upstream application.

Authorization Request Flow

It is helpful to have a mental model of how requests flow through the various servers involved in authorizing HTTP requests. The flow diagram below shows the actors that participate in the successful authorization of an HTTP request. Note that in some cases, these actors can be combined into a single application server. For example, there is no requirement for the external authorization server to be a separate application from the authorization provider.

+---------+                  +-------+                    +---------+             +---------------+ +---------+
| Client  |                  | Proxy |                    | ExtAuth |             | AuthProvider  | | Origin  |
+---------+                  +-------+                    +---------+             +---------------+ +---------+
     |                           |                             |                          |              |
     | Send HTTP request         |                             |                          |              |
     |-------------------------->|                             |                          |              |
     |                           |                             |                          |              |
     |                           | Authorize HTTP request      |                          |              |
     |                           |---------------------------->|                          |              |
     |                           |                             | -----------------\       |              |
     |                           |                             |-| Verify request |       |              |
     |                           |                             | |----------------|       |              |
     |                           |                             |                          |              |
     |                           |                      200 OK |                          |              |
     |                           |<----------------------------|                          |              |
     |         ----------------\ |                             |                          |              |
     |         | Inject        |-|                             |                          |              |
     |         | authorization | |                             |                          |              |
     |         | metadata      | |                             |                          |              |
     |         |---------------| | Forward HTTP request        |                          |              |
     |                           |---------------------------------------------------------------------->|
     |                           |                             |                          |              |
     |                           |                             |                 Respond to HTTP request |
     |                           |<----------------------------------------------------------------------|
     |                           |                             |                          |              |
     |     Forward HTTP response |                             |                          |              |
     |<--------------------------|                             |                          |              |
     |                           |                             |                          |              |

A HTTP Client generates an HTTP request and sends it to the Proxy. The Proxy is an Envoy instance that Contour has programmed with an external authorization configuration. The Proxy holds the HTTP request and sends an authorization check request to the ExtAuth server that Contour has bound to the virtual host. The ExtAuth server may be able to verify the request locally, but in many cases it will need to make additional requests to the AuthProvider server to verify or obtain an authorization token.

In this flow, the ExtAuth server is able to authorize the request, and sends an authorization response back to the Proxy. The response includes the authorization status, and a set of HTTP headers modifications to make to the HTTP request. Since this authorization was successful, the Proxy modifies the request and forwards it to the application. If the authorization was not successful, the Proxy would have immediately responded to the client with an HTTP error.

Using the Contour Authorization Server

The Contour project has built a simple authorization server named contour-authserver. contour-authserver supports an authorization testing server, and an HTTP basic authorization server that accesses credentials stored in htpasswd format.

To get started, ensure that Contour is deployed and that you have cert-manager installed in your cluster so that you can easily issue self-signed TLS certificates.

At this point, we should also create a cluster-wide self-signed certificate issuer, just to make it easier to provision TLS certificates later:

$ kubectl apply -f - <<EOF
apiVersion: cert-manager.io/v1alpha3
kind: ClusterIssuer
metadata:
  name: selfsigned
spec:
  selfSigned: {}
EOF
clusterissuer.cert-manager.io/selfsigned created

Deploying the Authorization Server

The first step is to deploy contour-authserver to the projectcontour-auth namespace. To do this, we will use kustomize to build a set of YAML object that we can deploy using kubectl. In a new directory, create the following kustomization.yaml file:

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

namespace: projectcontour-auth

resources:
- github.com/projectcontour/contour-authserver/config/htpasswd

patchesJson6902:
- target:
    group: cert-manager.io
    version: v1alpha3
    kind: Certificate
    name: htpasswd
    namespace: projectcontour-auth
  patch: |- 
    - op: add
      path: /spec/issuerRef/kind
      value: ClusterIssuer

images:
- name: contour-authserver:latest
  newName: docker.io/projectcontour/contour-authserver
  newTag: v2

Note that the kustomization patches the Certificate resource to use the “selfsigned” ClusterIssuer that we created earlier. This is required because the contour-authserver deployment includes a request for a self-signed TLS server certificate. In a real deployment, this certificate should be requested from a real trusted certificate issuer.

Now create the projectcontour-auth namespace, build the deployment YAML, and apply to your cluster:

$ kubectl create namespace projectcontour-auth
namespace/projectcontour-auth created
$ kubectl apply -f <(kustomize build .)
serviceaccount/htpasswd created
clusterrole.rbac.authorization.k8s.io/contour:authserver:htpasswd created
clusterrolebinding.rbac.authorization.k8s.io/contour:authserver:htpasswd created
service/htpasswd created
deployment.apps/htpasswd created
certificate.cert-manager.io/htpasswd created

At this point, contour-authserver is deployed and is exposed to the cluster as the Service projectcontour-auth/htpasswd. It has a self-signed TLS certificate and is accepting secure connections on port 9443.

In the default configuration, contour-authserver will accept htpasswd data from secrets with the projectcontour.io/auth-type: basic annotation. Most systems install the Apache htpasswd tool, which we can use to generate the password file:

$ touch auth
$ htpasswd -b auth user1 password1
Adding password for user user1
$ htpasswd -b auth user2 password2
Adding password for user user2
$ htpasswd -b auth user3 password3
Adding password for user user3

Once we have some password data, we can populate a Kubernetes secret with it. Note that the password data must be in the auth key in the secret, and that the secret must be annotated with the projectcontour.io/auth-type key.

$ kubectl create secret generic -n projectcontour-auth passwords --from-file=auth
secret/passwords created
$ kubectl annotate secret -n projectcontour-auth passwords projectcontour.io/auth-type=basic
secret/passwords annotated

Creating an Extension Service

Now that contour-authserver is deployed, the next step is to create a ExtensionService resource.

apiVersion: projectcontour.io/v1alpha1
kind: ExtensionService
metadata:
  name: htpasswd
  namespace: projectcontour-auth
spec:
  protocol: h2
  services:
  - name: htpasswd
    port: 9443

The ExtensionService resource must be created in the same namespace as the services that it binds. This policy ensures that the creator of the ExtensionService also has the authority over those services.

$ kubectl apply -f htpasswd.yaml
extensionservice.projectcontour.io/htpasswd created

Deploying a Sample Application

To demonstrate how to use the authorization server in a HTTPProxy resource, we first need to deploy a simple echo application.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: ingress-conformance-echo
spec:
  replicas: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: ingress-conformance-echo
  template:
    metadata:
      labels:
        app.kubernetes.io/name: ingress-conformance-echo
    spec:
      containers:
      - name: conformance-echo
        image: agervais/ingress-conformance-echo:latest
        ports:
        - name: http-api
          containerPort: 3000
        readinessProbe:
          httpGet:
            path: /health
            port: 3000
---
apiVersion: v1
kind: Service
metadata:
  name: ingress-conformance-echo
spec:
  ports:
  - name: http
    port: 80
    targetPort: http-api
  selector:
    app.kubernetes.io/name: ingress-conformance-echo

This echo server will respond with a JSON object that reports information about the HTTP request it received, including the request headers.

$ kubectl apply -f echo.yaml
deployment.apps/ingress-conformance-echo created
service/ingress-conformance-echo created

Once the application is running, we can expose it to Contour with a HTTPProxy resource.

apiVersion: cert-manager.io/v1alpha3
kind: Certificate
metadata:
  name: ingress-conformance-echo
spec:
  dnsNames:
  - local.projectcontour.io
  secretName: ingress-conformance-echo
  issuerRef:
    name: selfsigned
    kind: ClusterIssuer
---
apiVersion: projectcontour.io/v1
kind: HTTPProxy
metadata:
  name: echo
spec:
  virtualhost:
    fqdn: local.projectcontour.io
    tls:
      secretName: ingress-conformance-echo
  routes:
  - services:
    - name: ingress-conformance-echo
      port: 80

Note that we created a TLS secret and exposed the application over HTTPS.

$ kubectl apply -f echo-proxy.yaml
certificate.cert-manager.io/ingress-conformance-echo created
httpproxy.projectcontour.io/echo created
$ kubectl get proxies echo
NAME   FQDN                      TLS SECRET                 STATUS   STATUS DESCRIPTION
echo   local.projectcontour.io   ingress-conformance-echo   valid    valid HTTPProxy

We can verify that the application is working by requesting any path:

$ curl -k https://local.projectcontour.io/test/$((RANDOM))
{"TestId":"","Path":"/test/12707","Host":"local.projectcontour.io","Method":"GET","Proto":"HTTP/1.1","Headers":{"Accept":["*/*"],"Content-Length":["0"],"User-Agent":["curl/7.64.1"],"X-Envoy-Expected-Rq-Timeout-Ms":["15000"],"X-Envoy-Internal":["true"],"X-Forwarded-For":["172.18.0.1"],"X-Forwarded-Proto":["https"],"X-Request-Id":["7b87d5d1-8ee8-40e3-81ac-7d74dfd4d50b"],"X-Request-Start":["t=1601596511.489"]}}

Using the Authorization Server

Now that we have a working application exposed by a HTTPProxy resource, we can add HTTP basic authorization by binding to the ExtensionService that we created earlier. The simplest configuration is to add an authorization field that names the authorization server ExtensionService resource that we created earlier.

apiVersion: projectcontour.io/v1
kind: HTTPProxy
metadata:
  name: echo
spec:
  virtualhost:
    fqdn: local.projectcontour.io
    tls:
      secretName: ingress-conformance-echo
    authorization:
      extensionRef:
        name: htpasswd
        namespace: projectcontour-auth
  routes:
  - services:
    - name: ingress-conformance-echo
      port: 80
$ kubectl apply -f echo-auth.yaml
httpproxy.projectcontour.io/echo configured

Now, when we make the same HTTP request, we find the response requests authorization:

$ curl -k -I https://local.projectcontour.io/test/$((RANDOM))
HTTP/2 401
www-authenticate: Basic realm="default", charset="UTF-8"
date: Fri, 02 Oct 2020 00:27:49 GMT
server: envoy

Providing a user credential from the password file that we created earlier allows the request to succeed. Note that contour-authserver has injected a number of headers (prefixed with Auth-) to let the application know how the request has been authorized.

$ curl -k --user user1:password1 https://local.projectcontour.io/test/$((RANDOM))
{"TestId":"","Path":"/test/27132","Host":"local.projectcontour.io","Method":"GET","Proto":"HTTP/1.1","Headers":{"Accept":["*/*"],"Auth-Handler":["htpasswd"],"Auth-Realm":["default"],"Auth-Username":["user1"],"Authorization":["Basic dXNlcjE6cGFzc3dvcmQx"],"Content-Length":["0"],"User-Agent":["curl/7.64.1"],"X-Envoy-Expected-Rq-Timeout-Ms":["15000"],"X-Envoy-Internal":["true"],"X-Forwarded-For":["172.18.0.1"],"X-Forwarded-Proto":["https"],"X-Request-Id":["2c0ae102-4cf6-400e-a38f-5f0b844364cc"],"X-Request-Start":["t=1601601826.102"]}}

Caveats

There are a few caveats to consider when deploying external authorization:

  1. Only one external authorization server can be configured on a virtual host
  2. Only HTTPS virtual hosts are supported
  3. External authorization cannot be used with the TLS fallback certificate (i.e. client SNI support is required)
Ready to try Contour?

Read our getting started documentation.