One Gateway Instead of Ten Logins: Authentik Forward Auth with Caddy

How to protect self-hosted services with one central login even when the applications themselves support neither SSO nor two-factor authentication.

6 min read
  • #Self Hosting
  • #Security

Over time, a homelab accumulates services. Almost every one of them brings its own user management, stores passwords in its own way and, in the worst case, does not even support two-factor authentication.

Creating a separate account for every service and putting everything into a password manager is possible, of course. It does not make the situation particularly pleasant, and more importantly it does not solve the actual problem: authentication remains distributed across many different applications.

An alternative is to move authentication in front of the applications. The reverse proxy checks every request to see whether the user is already authenticated. If not, the request is redirected to a central login page. The application itself only receives the request after successful authentication and does not need to understand SSO at all.

This pattern is called Forward Auth. In my homelab I use Caddy as the reverse proxy and Authentik as the identity provider.

The central Caddy snippet

Authentik’s Embedded Outpost already exposes the endpoint required for this. Caddy can ask it on every request whether valid authentication exists. A separate Outpost container is not required for this use case.

The Caddy configuration can be defined as a reusable snippet:

(authentik) {
	reverse_proxy /outpost.goauthentik.io/* authentik-server:9000

	forward_auth authentik-server:9000 {
		uri /outpost.goauthentik.io/auth/caddy
		copy_headers X-Authentik-Username X-Authentik-Groups X-Authentik-Email X-Authentik-Name X-Authentik-Uid
		trusted_proxies private_ranges
	}
}

A protected application then only needs to import that snippet:

dashboard.example.com {
	import authentik
	reverse_proxy dashboard:7575
}

Some parts of the snippet look optional at first. They are not.

The Outpost route

reverse_proxy /outpost.goauthentik.io/* authentik-server:9000

Authentik uses a path below the originally requested domain for the login and subsequent callback. The browser may therefore be redirected to an address such as:

https://dashboard.example.com/outpost.goauthentik.io/callback

Without the additional reverse-proxy route, this request lands at the application itself and normally ends in a 404. Because that only happens after a successful login, it is easy to investigate the problem in the wrong place.

Passing user information downstream

With copy_headers, Caddy takes the user information returned by Authentik and forwards it to the protected service:

copy_headers X-Authentik-Username X-Authentik-Groups X-Authentik-Email X-Authentik-Name X-Authentik-Uid

Applications that support header-based authentication can use this information directly. The user is then not only authenticated at Authentik but automatically logged into the application as well.

If an application does not understand these headers, it simply ignores them. Access is still protected, but the application may continue to require a second login of its own.

Trusted proxies

trusted_proxies private_ranges

This setting defines the network ranges from which Caddy accepts forwarded client information. Without a clean proxy configuration, headers such as X-Forwarded-For can be accepted from untrusted senders.

Applications and permissions in Authentik

The Caddy configuration handles only the technical check. Which application is protected and who may access it is configured in Authentik.

Three objects matter in practice:

  • The Provider defines how authentication is performed.
  • The Application represents the protected service.
  • The Policy Binding defines which users or groups are allowed to access it.

These objects can be created in the web interface. With only a few services that remains manageable. As the number grows, however, it quickly becomes unclear which setting was changed where.

Authentik supports Blueprints for exactly this reason. They allow the configuration to be managed as YAML:

version: 1

metadata:
  name: dashboard (proxy forward-auth)
  labels:
    blueprints.goauthentik.io/instantiate: "true"

entries:
  - model: authentik_providers_proxy.proxyprovider
    state: present
    identifiers:
      name: dashboard
    id: provider
    attrs:
      name: dashboard
      mode: forward_single
      external_host: https://dashboard.example.com
      cookie_domain: example.com
      authorization_flow: !Find
        - authentik_flows.flow
        - [slug, default-provider-authorization-implicit-consent]
      invalidation_flow: !Find
        - authentik_flows.flow
        - [slug, default-provider-invalidation-flow]

  - model: authentik_core.application
    state: present
    identifiers:
      slug: dashboard
    id: app
    attrs:
      name: Dashboard
      slug: dashboard
      provider: !KeyOf provider
      meta_launch_url: https://dashboard.example.com

  - model: authentik_policies.policybinding
    state: present
    identifiers:
      target: !KeyOf app
      group: !Find
        - authentik_core.group
        - [name, admin]
      order: 0
    attrs:
      enabled: true

The file is placed in Authentik’s Blueprint directory and loaded at startup. Authentik creates the described objects or reconciles them with the existing state.

That means the answer to the question of who may access a given application no longer exists only inside the Authentik database. The configuration can live in Git together with the rest of the homelab setup. Changes become traceable and can be reverted when necessary.

One important Provider setting is cookie_domain:

cookie_domain: example.com

When the parent domain is configured here, the Authentik session cookie can be used across all subdomains below it.

After logging in once at dashboard.example.com, the user is therefore already authenticated when opening grafana.example.com or paperless.example.com.

Without this setting, every application may require a separate login even though all of them use the same Authentik server.

Exceptions for APIs, apps and webhooks

Forward Auth works particularly well for applications opened through a normal browser. Not every client can handle a login page and browser session, though.

Typical problem cases include:

  • Mobile apps with their own authentication
  • API clients
  • Webhooks
  • Monitoring systems
  • Automated scripts

An API client normally expects a JSON response. If Authentik redirects it to an HTML login page instead, it cannot do anything useful with that response.

Such endpoints therefore need to be excluded from Forward Auth or exposed on a separate hostname.

One example is an application where only the administration interface should be protected while its API remains outside Forward Auth:

blog.example.com {
	@admin {
		path /admin /admin/*
		not path /admin/api/*
	}

	forward_auth @admin authentik-server:9000 {
		uri /outpost.goauthentik.io/auth/caddy
	}

	reverse_proxy blog:2368
}

The excluded path must not simply become unprotected. A webhook should validate a signature or secret, for example, and an API can use its own token.

A path without Forward Auth is not automatically insecure. What matters is that the service itself provides suitable authentication or signature validation.

Authentik becomes a central dependency

Central authentication turns Authentik into an important infrastructure component. If the service is unavailable, applications behind it can no longer be reached either.

That is the logical consequence of centralised access control. In a homelab the risk is usually acceptable, but it should be part of the design.

In particular, systems required for troubleshooting should not depend exclusively on Authentik. They need an alternative access path, for example:

  • Access through an internal network or VPN
  • A separate hostname protected with Basic Auth
  • Direct access through a management interface
  • A deliberately unprotected service outside Forward Auth

Such a path should not be needed during normal operation. When Authentik or the reverse proxy fails, however, it becomes very useful.

Result

After the migration, one login per session is enough. Applications without their own two-factor authentication still benefit from central protection through Authentik.

Access rights are no longer maintained separately in every service but managed through groups and policies. Blueprints also keep the configuration versioned in the repository.

For a new service, the Caddy side is reduced to two lines:

import authentik
reverse_proxy neuer-dienst:8080

The remaining work is a Blueprint containing the Provider, Application and desired group.

For me, the biggest advantage is not even Single Sign-on itself. New services no longer need to solve authentication independently before they can be used sensibly. The remaining question is simply which group should be allowed to access them.