Adding Forward Auth to a Gateway API HTTPRoute for Traefik

I am starting to toy around with Kubernetes in my homelab. One of the services I recently set up outside of Kubernetes was Authentik which has allowed me to configure authentication centrally. It's pretty minimal at this point, but one thing I've really enjoyed is just putting Caddy's forward_auth into some backends that don't support a great auth mechanism.

Because Traefik's Kubernetes support seems more mature and because Gateway API is the new way, I wanted to replicate this setup in my new cluster. However, I couldn't find any documentation for this specifically, so I wanted to share what was necessary to get this working.

Implementation

There are a few resources necessary to implement this setup. First it's important to understand what Traefik's ForwardAuth implementation looks like without Kubernetes: It's a middleware you add to your router, so we'll have to translate that across into Kubernetes. Next, we need to adapt our solution to Gateway API. Finally, we need to put it all together with a little more yaml glue. I am assuming you've already got your gateway configured. Here's what we'll need:

  • Authentik outpost running in Kubernetes
  • ReferenceGrant to allow cross-namespace connectivity.
  • Middleware to configure for forward auth
  • HTTPRoute for the service we want to protect

First we need a new Authentik outpost inside the cluster.

Authentik Outpost

Gateway API can only refer to cluster-internal backends, i.e. you can't have it point at a generic URL. This is relevant for the outpost prefix in the route. So instead of point at my (out-of-cluster) route, I deployed a new outpost for Authentik. Here's the full manifest:

apiVersion: v1
kind: Namespace
metadata:
  labels:
    app.kubernetes.io/instance: k8s-outpost
    app.kubernetes.io/managed-by: goauthentik.io
    app.kubernetes.io/name: authentik-proxy
    app.kubernetes.io/version: 2021.12.3
  name: authentik-outpost
---
apiVersion: v1
data:
  authentik_host: __HOST__
  authentik_host_insecure: ZmFsc2U=
  token: |
    __TOKEN__
kind: Secret
metadata:
  labels:
    app.kubernetes.io/instance: k8s-outpost
    app.kubernetes.io/managed-by: goauthentik.io
    app.kubernetes.io/name: authentik-proxy
    app.kubernetes.io/version: 2021.12.3
  name: authentik-outpost-api-58g5k79468
  namespace: authentik-outpost
type: Opaque
---
apiVersion: v1
kind: Service
metadata:
  labels:
    app.kubernetes.io/instance: k8s-outpost
    app.kubernetes.io/managed-by: goauthentik.io
    app.kubernetes.io/name: authentik-proxy
    app.kubernetes.io/version: 2021.12.3
  name: authentik-outpost
  namespace: authentik-outpost
spec:
  ports:
  - name: http
    port: 9000
    protocol: TCP
    targetPort: http
  - name: https
    port: 9443
    protocol: TCP
    targetPort: https
  selector:
    app.kubernetes.io/managed-by: goauthentik.io
    app.kubernetes.io/name: authentik-proxy
  type: ClusterIP
---
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app.kubernetes.io/instance: k8s-outpost
    app.kubernetes.io/managed-by: goauthentik.io
    app.kubernetes.io/name: authentik-proxy
    app.kubernetes.io/version: 2021.12.3
  name: authentik-outpost
  namespace: authentik-outpost
spec:
  selector:
    matchLabels:
      app.kubernetes.io/managed-by: goauthentik.io
      app.kubernetes.io/name: authentik-proxy
      app.kubernetes.io/version: 2021.12.3
  template:
    metadata:
      labels:
        app.kubernetes.io/managed-by: goauthentik.io
        app.kubernetes.io/name: authentik-proxy
        app.kubernetes.io/version: 2021.12.3
    spec:
      containers:
      - env:
        - name: AUTHENTIK_HOST
          valueFrom:
            secretKeyRef:
              key: authentik_host
              name: authentik-outpost-api-58g5k79468
        - name: AUTHENTIK_TOKEN
          valueFrom:
            secretKeyRef:
              key: token
              name: authentik-outpost-api-58g5k79468
        - name: AUTHENTIK_INSECURE
          valueFrom:
            secretKeyRef:
              key: authentik_host_insecure
              name: authentik-outpost-api-58g5k79468
        image: ghcr.io/goauthentik/proxy:2024.12.1
        name: proxy
        ports:
        - containerPort: 9000
          name: http
          protocol: TCP
        - containerPort: 9443
          name: https
          protocol: TCP

This is generated using Kustomize, but you should be able to adapt this to your own preferences. It will spin up a new Authentik outpost. Make sure to configure the host & token provided by Authentik.

ReferenceGrant

apiVersion: gateway.networking.k8s.io/v1beta1
kind: ReferenceGrant
metadata:
  name: traefik-grant
  namespace: authentik-outpost
spec:
  from:
  - group: gateway.networking.k8s.io
    kind: HTTPRoute
    namespace: monitoring
  to:
  - group: ""
    kind: Service

ReferenceGrant is a new mechanism for Gateway API which is necessary to allow the HTTPRoute to point at a backend in a different namespace. This grant belongs in the same namespace as our outpost, which is the target backend (and has to allow this reference).

Middleware

Traefik middleware configuration is unchanged between Ingress & Gateway API:

apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
  name: forward-auth
  namespace: monitoring
spec:
  forwardAuth:
    address: http://authentik-outpost.authentik-outpost.svc.cluster.local:9000/outpost.goauthentik.io/auth/traefik
    trustForwardHeader: true
    authResponseHeaders:
        - X-authentik-username
        - X-authentik-groups
        - X-authentik-entitlements
        - 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

This is copied straight from Authentik's example (though I renamed it). It's interesting to me that this seems to bypass the reference granting mechanism and I don't fully understand what the security model is here. Services can always refer to each other at the network level (by default). I didn't bother researching this further though.

HTTPRoute

Finally, we have all the pieces together to put a route behind forward auth. Here's my example for putting the Prometheus UI behind auth:

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: prometheus
  namespace: monitoring
spec:
  parentRefs:
    - name: traefik-gateway
      namespace: traefik
  hostnames:
    - prometheus.example.com
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /outpost.goauthentik.io/
      backendRefs:
        - name: authentik-outpost
          namespace: authentik-outpost
          port: 9000
    - matches:
        - path:
            type: PathPrefix
            value: /
      backendRefs:
        - name: prometheus-server
          port: 80
      filters:
        - type: ExtensionRef
          extensionRef:
            group: traefik.io
            kind: Middleware
            name: forward-auth

There are a few notable differences to a "normal" route:

  • The /outpost.goauthentik.io/ prefix: This is necessary for the client to reach the outpost through this route. Authentik identifies its "provider" from the Host header. Authentik provides the Ingress version in their docs, this is adapted from that.
  • The ExtensionRef filter: This is a special mechanism so Traefik can offer vendor-specific extensions that aren't standardised. In other words: This solution doesn't work with all gateways, only with Traefik. Gateway API hasn't standardised a filter for forward auth.

Summary

Once put together this is quite straightforward by Kubernetes standards, it's not that much YAML! However, because Gateway API is so new, these recipes just aren't well documented yet. I did find all the documentation to piece this together though, so here are the references I used:

Troubleshooting

I made some mistakes while setting this up so here are some tips in case it's not working right away:

  • Check the HTTPRoute status via kubectl describe: If there are issues, it will tell you. For example, I didn't know about the reference grant until I saw the error message there.
  • Run Traefik with log level DEBUG: If you don't, you'll get a generic 500 with no explanation what the issue is. I actually raised an issue for this to maybe raise the log level to help diagnose mistakes quicker.