Auth proxy with Authentik and Traefik

Many web applications these days support some form of centralized user management (LDAP, OAuth2/OIDC, SAML …). For those that don’t a special type of reverse proxy can be installed “in front of” the application to handle user authentication. This authentication proxy makes sure all HTTP requests coming from outside are authenticated and only authenticated requests are forwarded to the application. It completely removes the need for the application to integrate with native authorization and authentication protocols - which can be a daunting task and challenging to do correctly! Instead the application just needs to look at HTTP headers to determine the properties of the authenticated user: name, groups, roles etc.

1
2
3
4
5
6
7
┌──────┐             ┌────────────┐                             ┌─────────────┐
│ User ├─[request]──►│ Auth proxy ├──[authenticated request]───►│ Application │
└──────┘             └──────┬─────┘                             └─────────────┘
                             │
                   ┌─────────┴─────────┐
                   │ Identity provider │
                   └───────────────────┘

A popular open-source project that implements this concept is OAuth2 Proxy. Fortunately, Authentik already comes with an “embedded” authentication proxy, therefore it is not necessary to set up a separate instance of the reverse proxy - handy!

In this post I want to document how to set up such an authentication proxy with Authentik Identity Provider and Traefik Proxy, since I’m sure I’ll be using this more often in the future, thus it might be useful for you, too. The setup is also described in the Authentik documentation (referred to as forward authentication), although very terse and without much context.

For reference, at the time of writing I’m using Authentik 2023.10 and Traefik v2.9 (both installed via their official Helm charts).

#  Demo application

For testing purposes we’ll use the whoami server: it returns all received HTTP headers as the body of the response plus some additional metadata, like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
$ podman run --detach --publish 8080:80 docker.io/traefik/whoami:latest

$ curl --header "Foo: Bar Baz" "http://localhost:8080"
Hostname: 98a89edbc868
IP: 127.0.0.1
IP: ::1
IP: 10.0.2.100
IP: fe80::34a5:feff:feb7:7c8e
RemoteAddr: 10.0.2.100:35506
GET / HTTP/1.1

Host: localhost:8080
User-Agent: curl/8.4.0
Accept: */*
Foo: Bar Baz

Create Kubernetes Deployment, Service and Ingress resources in a new namespace:

1
2
3
4
5
$ kubectl create namespace test-auth-proxy
$ kubens test-auth-proxy # make this the default namespace; https://github.com/ahmetb/kubectx
$ kubectl create deployment whoami --image=docker.io/containous/whoami --port=80
$ kubectl create service clusterip whoami --tcp=80
$ kubectl create ingress whoami --rule=test-auth-proxy.example.com/=whoami:80

Assuming that the cluster has functional external DNS and an ingress controller, the application should now be available at test-auth-proxy.example.com:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
$ curl -k https://test-auth-proxy.example.com
Hostname: whoami-5f6b449846-7dxqk
IP: 127.0.0.1
IP: ::1
IP: 10.42.1.132
RemoteAddr: 10.42.2.10:48216
GET / HTTP/1.1

Host: test-auth-proxy.example.com
User-Agent: curl/8.4.0
Accept: */*
Accept-Encoding: gzip
X-Forwarded-For: 10.42.2.4
X-Forwarded-Host: test-auth-proxy.example.com
X-Forwarded-Port: 443
X-Forwarded-Proto: https
X-Forwarded-Server: traefik-85949dbd79-n2fp4
X-Real-Ip: 10.42.2.4

We can see that the HTTP request was routed through Traefik before reaching the whoami application, since Traefik injects various X-Forwarded-* headers.

#  Configure Authentik

At this point we need to log in as an administrator to Authentik and switch to the Admin interface.

Go to the Applications > Providers tab and Create a new Provider as follows:

  • Type: Proxy Provider
  • Name: test-auth-proxy
  • Authentication flow: default-authentication-flow
  • Authorization flow: default-provider-authorization-implicit-consent
  • Mode: Forward auth (single application)
  • External Host: https://test-auth-proxy.example.com/*

Next create a new Application as follows:

  • Name: Test Auth Proxy
  • Slug: test-auth-proxy
  • Provider: test-auth-proxy - this is the most important setting, make sure to use the provider created in the previous step

Finally, we must not forget to enable the Application on the Outpost: edit the authentik Embedded Outpost (Type: Proxy) and shift-click on the Test Auth Proxy Application created in the previous step.

#  Create Authentik Middleware

At this point we’re good to go from the Authentik point of view, so we can proceed with the Traefik configuration. Traefik Middlewares allow inspecting and manipulating requests before they are send to the application (the backend in Traefik’s terminology). These middlewares can be configured from a variety of providers. Since I’m deploying Traefik in a Kubernetes cluster, I’m opting for the Custom Resource Definition (CRD) approach (Traefik calls this “dynamic configuration”):

 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
# kubectl create -n test-auth-proxy -f - <<EOF
apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
  name: authentik-auth-proxy
spec:
  forwardAuth:
    # address of the identity provider (IdP)
    address: http://ak-outpost-authentik-embedded-outpost.authentik.svc.cluster.local:9000/outpost.goauthentik.io/auth/traefik
    trustForwardHeader: true
    # headers that are copied from the IdP response and set on forwarded request to the backend application
    authResponseHeaders:
    - X-authentik-username
    - X-authentik-groups
    - X-authentik-email
    - X-authentik-name
    - X-authentik-uid
    - X-authentik-jwt
    - X-authentik-meta-jwks
    - X-authentik-meta-outpost
    - X-authentik-meta-provider
    - X-authentik-meta-app
    - X-authentik-meta-version

EOF

The middleware is named authentik-auth-proxy. It must be deployed in the same namespace as the Ingress (which will reference the Middleware in the next step). It is critical that the spec:forwardAuth:address setting has the correct address of the embedded-outpost-service Service: http://<SERVICE_NAME>.<NAMESPACE_NAME>.svc.cluster.local:9000 is the most common variant - double check it by looking up the service name, namespace and port number of your Authentik deployment. If you use Network Policies in your Kubernetes cluster, these must allow connections from the Traefik pods to the Authentik (server) pods.

For all available configuration options of the ForwardAuth Middleware, refer to the upstream documentation or kubectl explain middleware.spec.forwardAuth.

#  Reconfigure Ingress for Middleware

By adding the Middleware declared in the previous step into the original Ingress created in the first step, we instruct Traefik that all requests coming to this endpoint must be authenticated. If a request is missing the necessary authentication, then Traefik will redirect it to the authentication provider.

1
2
kubectl annotate ingress whoami \
    traefik.ingress.kubernetes.io/router.middlewares=test-auth-proxy-authentik-auth-proxy@kubernetescrd

The value of the annotation is in the format <NAMESPACE_NAME>-<MIDDLEWARE_NAME>@<PROVIDER>.

Test it:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
$ curl -i http://test-auth-proxy.example.com
HTTP/2 302
content-type: text/html; charset=utf-8
date: Thu, 21 Dec 2023 16:19:46 GMT
location: https://auth.example.com/application/o/authorize/?client_id=vn7nqyCMlxnMRJoRWujFmQUVTFc8Bau9XbTqcRpA&redirect_uri=https%3A%2F%2Ftest-auth-proxy.example.com%2Foutpost.goauthentik.io%2Fcallback%3FX-authentik-auth-callback%3Dtrue&response_type=code&scope=profile+ak_proxy+openid+email&state=ydo1AuJNRRsvZZHwoVKcTO-yT9-ts11QLlKAk6tkr6Q
set-cookie: authentik_proxy_vn7nqyCM=3SXRUTM4X2EROTXEAIIQIPHHOBO4RTXMOH6OA3SEX4DEJG5EBYRKJDJXPDSC4YJDDQYFNNDX3ICCNAKT3NSUS6VKB37FFY6Q5XDG6ZY; Path=/; Expires=Fri, 22 Dec 2023 16:19:47 GMT; Max-Age=86401; HttpOnly; Secure; SameSite=Lax
vary: Accept-Encoding
content-length: 372

<a href="https://auth.example.com/application/o/authorize/?client_id=vn7nqyCMlxnMRJoRWujFmQUVTFc8Bau9XbTqcRpA&amp;redirect_uri=https%3A%2F%2Ftest-auth-proxy.example.com%2Foutpost.goauthentik.io%2Fcallback%3FX-authentik-auth-callback%3Dtrue&amp;response_type=code&amp;scope=profile+ak_proxy+openid+email&amp;state=ydo1AuJNRRsvZZHwoVKcTO-yT9-ts11QLlKAk6tkr6Q">Found</a>.

#  Troubleshooting

As you can guess by the fact that I’m writing all of this down, I didn’t get it working on the first try.

The first issue was that my authentik-worker had to lost connection to the authentik-server and it was marked as “unhealthy” in the admin interface (apparently this can happen when Redis restarts). This caused the error message no app for hostname to appear when visiting https://test-auth-proxy.example.com - meaning Authentik didn’t fully process the newly created Application (see also authentik issue #3745). I restarted all Authentik pods, then the worker was “healthy” again and I could authenticate.

Next I had an a redirection loop between the Traefik Middleware and Authentik: a request to https://test-auth-proxy.example.com was redirected to https://auth.example.com/..., after the login I was redirected back to https://test-auth-proxy.example.com and the whole process would repeat again. I managed to fix / workaround this by enabling the Compatibility mode in the default-authentication-flow (can be found under Behavior settings). At the time of writing, this actually seems to be a bug in Authentik: see authentik issue #7374.

For more troubleshooting steps, see also the dedicated Authentik documentation page.

Happy proxying!