Authentik: Self-Hosted SSO for Everything in the Rack
One login, one MFA prompt, and a flow engine that will happily let you shoot yourself

Contents
At some point the rack acquires an identity problem. Mine crept up on me: fourteen services, each with its own user table, its own password reset flow that emails a link to a mail server hosted on the same machine, and its own idea of what a session is. Four had TOTP available and I’d enabled it on two. When a housemate moved out, offboarding meant finding fourteen admin pages and remembering which ones I’d forgotten. That’s the actual failure — the credentials weren’t weak, they were unmanageable, and an account you’ve forgotten exists is an account nobody will ever disable.
An identity provider fixes the shape of the problem. One user directory, one authentication policy, one MFA enrolment, one place to press “deactivate”. Authentik is the one I run, and this is what it took to get there and what I’d tell someone starting today.
Why an IdP rather than a better password manager
The obvious objection: you already have Vaultwarden, every service has a unique 32-character password, the browser fills them in, what’s actually wrong?
Three things. First, the passwords still exist. Every one of those services is storing a hash you’ve trusted them to store properly, and self-hosted software’s track record on password storage ranges from excellent to a sha1() call somebody wrote in 2014. With an IdP, most of those services stop having credentials at all — they receive an assertion from something you trust and take its word for it.
Second, MFA becomes uniform. Instead of enabling TOTP fourteen times in fourteen dialogs of varying quality, you enable a WebAuthn stage once and everything downstream inherits it — including the service that never implemented MFA and never will. That’s the piece that turns passkeys from a nice idea into something you can actually deploy across a whole estate.
Third, and this is the one that sold me: revocation becomes atomic. Disable the user in one place and every session everywhere is gone. Compare that to the current state of most homelabs, where “revoking access” is a research project.
The counter-argument deserves its due. You’re adding a component that everything depends on, and when it’s down, everything is down. That’s real, and I’ll come back to it in the verdict, because it’s the thing most people underestimate.
The concepts, in the order they’ll confuse you
Authentik’s model is more general than its competitors’ and this is simultaneously its best and worst feature. Four nouns:
Providers are how a specific application talks to Authentik. OAuth2/OIDC for anything modern. SAML for anything enterprise. Proxy for anything with no auth support at all. LDAP for the stubborn old thing that only speaks LDAP.
Applications are the user-facing object — a name, an icon, a tile on the dashboard — bound to exactly one provider. The split feels redundant until you realise the application is where authorisation policy attaches, and the provider is where protocol config lives.
Outposts are separate processes that do the work for proxy and LDAP providers. The embedded outpost ships inside the server container and handles most homelab cases. This is the concept people skip and then spend two hours confused about, because a proxy provider with no outpost assigned does precisely nothing and says nothing about it.
Flows are the interesting part and the dangerous part. A flow is an ordered list of stages — identification, password, MFA validation, consent, user write — executed to accomplish something. Authentik ships default flows that work. You can build your own, and the engine is expressive enough to implement “users in this group skip MFA on this network, users in that group must use WebAuthn, everyone else gets a captcha”, which is genuinely more than Authelia can express.
It’s also expressive enough to build a login flow with no way to log in. Authentik will let you save it. Ask me how I know.
That expressiveness is the whole axis of the choice against the alternative. If you want a config file that does one sensible thing, Authelia is 200MB of Go and a YAML file. If you want a policy engine with a UI, Authentik is a Python application, Postgres, Redis and about 1.5GB of RAM. I’ve laid the comparison out properly in Authelia vs Authentik; the short version is that Authentik earns its weight when you have heterogeneous applications, and looks like overkill when you have four containers behind one proxy.
A stack that works
| |
Pin the tag. 2024.6 rather than latest, because Authentik does schema migrations on startup and an unattended major bump on a component everything depends on is a bad evening. The general argument is in compose patterns that age well, and it applies double here.
The Docker socket mount on the worker is for managing outposts as containers automatically. It’s convenient and it hands the worker root on the host, so weigh it — if you’re only using the embedded outpost, drop it.
AUTHENTIK_SECRET_KEY encrypts session data and tokens. Generate it once with openssl rand -base64 50, back it up, and never regenerate it casually. Then browse to http://<host>:9000/if/flow/initial-setup/ to create the first admin.
Put it behind your reverse proxy on a real hostname with a real certificate, because OIDC redirect URIs and cookie domains both care. Anything in the Caddy or Traefik family is fine; the requirement is HTTPS and a stable name.
Wiring the two cases that matter
An app that speaks OIDC. Create an OAuth2/OIDC provider, note the client ID and secret, set the redirect URI to whatever the app documents, bind an application to it. In the app, point it at the three well-known endpoints. Authentik publishes a discovery document at /application/o/<slug>/.well-known/openid-configuration and any client worth using will consume that and configure itself.
An app that speaks nothing. This is the good bit. A Proxy Provider in forward-auth mode makes your reverse proxy ask Authentik whether a request is allowed before it ever reaches the app:
| |
Unauthenticated requests get bounced to the login flow. Authenticated ones arrive at the app with headers describing who the user is. Applications that support header-based auth read those and log the user straight in; ones that don’t at least sit behind a real front door with WebAuthn on it.
trusted_proxies is load-bearing. Those headers are trivially forgeable by anything that can reach the app directly, so the app must be reachable only through the proxy. A proxy-provider app on a port published to your LAN is an authentication system you can walk around.
Groups, policies, and the LDAP escape hatch
Once the plumbing works, the interesting question becomes authorisation rather than authentication — who gets to reach what. Authentik binds policies to applications, and a policy is either a group membership check, an expression, or a combination.
Group binding covers most of it. Create a group, bind it to an application, and only members see the tile and pass the check. This is the mechanism that makes shared services tolerable: household members get the media server and the photo library, and admin tooling stays in a group of one. The dashboard reflects it automatically, so people only see what they can actually use, which cuts support questions to roughly zero.
Expression policies are Python snippets evaluated at flow time, with access to the request context. They’re how you express the rules that don’t fit a checkbox:
| |
Bind that to an MFA stage as a policy and remote sessions get challenged while local ones don’t. That is either a sensible ergonomic compromise or a hole in your threat model, depending on how much you trust your own LAN, and the fact that Authentik makes it a six-line decision rather than a fork of somebody’s codebase is exactly what you’re paying the memory overhead for.
Then LDAP, which deserves a mention because it’s the escape hatch that makes the whole thing viable in a real rack. Some software will never speak OIDC. An LDAP provider turns Authentik into a directory those applications can bind against, using the same users, the same groups, and the same password policy. It runs on a dedicated outpost, it needs a service account to bind with, and it means the twelve-year-old Java application in the corner joins the estate instead of keeping its own user table forever. Authelia has no equivalent — it consumes LDAP rather than serving it — and for some racks that single feature decides the question.
What actually goes wrong
You locked yourself out of the admin flow. Editing default-authentication-flow and saving something impossible. There’s a recovery path: docker compose run --rm server create_recovery_key 10 akadmin prints a single-use URL valid for ten minutes. Test that command before you need it.
Redirect URI mismatch. The most common OIDC error and always literal — scheme, port, trailing slash. https://app.example.com/oauth/callback and https://app.example.com/oauth/callback/ are different URIs. Authentik’s event log shows the exact string it received.
The proxy provider does nothing. No outpost assigned, or an outpost that can’t reach the server. Check Applications → Outposts for a green health indicator. An outpost showing “not connected” while the container is happily running usually means it’s dialling AUTHENTIK_HOST at a URL it can’t resolve from inside its network.
Login loops. The proxy and Authentik disagree about the cookie domain. Both must be on the same parent domain — auth.mylab.local protecting app.mylab.local works; protecting something on an IP address does not.
Everything is slow. The worker is doing migrations, or Postgres has no cache. It wants around 1.5–2GB across the four containers. On a Pi this is a poor fit, and I’d say so plainly.
Group membership changes don’t take effect. Sessions are cached. A user removed from a group keeps their access until the session expires or you kill it explicitly from the admin interface. When revoking access in anger, deactivate the user rather than editing groups — that invalidates everything immediately, which is the property you actually wanted.
Users can’t enrol MFA. The enrolment stage needs binding to a flow that’s reachable. Authentik ships one; if you built a custom authentication flow and didn’t wire enrolment into it, users can authenticate and never register a factor, and you’ll discover this when you make MFA mandatory.
The honest verdict
Authentik is worth it above roughly eight services, or below that if any of them are shared with other people. The gain is the offboarding story and the uniform MFA — every service inheriting WebAuthn, including the ones that never heard of it, is a genuinely large security improvement for an evening’s work. Pair it with a hardware key, as in YubiKey for SSH, GPG and sudo, and the whole estate sits behind something phishing-resistant.
It’s the wrong tool for a four-container stack behind one proxy. You’d be running 2GB of identity infrastructure to protect 400MB of applications, and Authelia does that job in a fifth of the memory with a config file you can read in one sitting.
The honest cost is the dependency. When Authentik is down, everything is down, including — the first time it happens — your ability to reach the thing that would tell you why. Three mitigations, and I’d call all three mandatory: keep one break-glass local account on your reverse proxy that bypasses SSO entirely, keep the recovery key command in your runbook, and back up that Postgres database properly rather than by snapshotting a running container, which is the whole argument of dumps, WAL and PITR.
Do those and it’s a component you stop thinking about. Skip them and you’ve built a single point of failure with a lovely UI in front of it.




