---
title: "SSO is a perimeter decision, not a login box"
date: 2026-06-23
summary: "Putting single sign-on in front of a fleet of self-hosted apps isn't about a nicer login screen - it's about where your trust boundary lives, and how it fails."
tags: ["authentik", "oidc", "sso", "security", "kubernetes"]
draft: false
hero: "/blog/authentik-sso-2fa.webp"
heroAlt: "A single central gate ringed by a second concentric seal radiates beams of light to a circle of app-doors inside a glowing perimeter."

---

Most write-ups treat single sign-on as a feature you bolt on: stand up an identity provider, point the
apps at it, enjoy one login. That framing misses the actual decision. SSO *moves your trust boundary* - every app now trusts one issuer, and every account is one credential away from all of them. Get it right
and you've hardened the whole estate at once. Get it wrong and you've built a single, central thing to lose.

Here's how I think about it after wiring an OIDC provider in front of a self-hosted fleet.

![Authentik SSO flow - the browser authenticates against the identity provider over the front channel; each app validates tokens over an internal back channel](/diagrams/authentik-sso.svg?v=2)

## Enforce the second factor at the flow, not per app

The leverage is putting 2FA on the *authentication flow itself*, not on each application. Every app that
federates to the provider inherits it for free - you can't forget to enable 2FA on app number nine, because
app number nine never sees a password. Enrolment (TOTP / WebAuthn) is mandatory at the identity layer, and
the apps just receive an already-verified identity.

That's the whole point of centralising: you set the policy once, at the boundary, and everything downstream
gets it.

## Keep a key under the mat

The flip side of "one issuer for everything" is that when the issuer is down, or you fat-finger the auth
flow - you can lock yourself out of *everything*, including the tools you'd use to fix it. So every critical
app keeps a **local break-glass admin** that bypasses SSO, and the recovery codes for 2FA enrolment live
offline.

SSO is the front door. Break-glass is the fire exit. You don't ship a building with only one.

## The gotchas that actually cost time

Two traps ate real hours, and they're the same lesson wearing two hats:

- **A baked-in config file silently overrode environment variables.** I set the OIDC scopes via env, login
  kept failing with *"missing fields: email"*, and the cause was a config file winning over the env. The
  provider needs `openid email profile` explicitly - set where the app actually reads it, not where you
  assume it does.
- **In-cluster back-channels can't use the public hostname.** The browser hits the public login URL fine,
  but the app's *server-side* token exchange runs inside the cluster, where split-horizon DNS doesn't
  resolve the public name. The fix: point the back channel at the internal service address while the
  browser-facing URL stays public.

Both say the same thing: SSO has two channels - the one the user sees and the one the server uses, and they
don't live on the same network.

## Design the concentration you just created

Centralising identity is a force multiplier, but it concentrates risk *by design*. So design the
concentration: enforce the strong factor at the boundary, plan the failure mode before you need it, and
remember that OIDC authenticates a *user* - it doesn't, on its own, make a sensitive service safe to expose.
The login box is the easy part. The perimeter is the decision.
