SSO is for people, not for machines

Sooner or later, a machine-facing service grows a human face. The container registry every node in the fleet pulls from ships a web UI. The development environment becomes a browser tab with a live terminal in it. Now there are people arriving at hostnames that machines also depend on, and the tempting move is one auth layer over the whole thing. Put the entire host behind single sign-on. One rule, no exceptions, done.
Do that to a registry and the cluster stops pulling images the same afternoon, because a container runtime cannot answer an interactive login page. The failure after that one is quieter and worse: you see the machines breaking, so you loosen the layer to let them through - an exception here, an anonymous path there - until the human door is weaker because the machines needed in.
The answer isn't a cleverer single layer. It's a split: route by audience, not by app. People get one doorman. Machines get keyed doors of their own, nowhere near the desk.
A doorman is for doors that can't answer for themselves
The house rule comes before anything else: when an app speaks OIDC natively, wire it natively. The app authenticates its own users against the identity provider and stays directly reachable, which matters, because its existing machine clients (a credential vault's mobile app, a git CLI pushing over HTTPS) keep working untouched. Native wiring routes by audience for free.
A proxy outpost (an authenticating reverse proxy operated by the identity provider) is for the remainder: apps with no serious auth story of their own, or surfaces you'd never trust to defend themselves. Two tenants in this fleet fit that bill. A registry web UI, whose built-in auth is a static htpasswd file: a fine key for a machine, a poor front door for a person. And a browser IDE: an editor in a tab with a real shell behind it, which is about the most consequential thing you can put behind a URL.
One desk signs in every visitor
Recent versions of the identity provider dropped their embedded proxy, so the outpost is now a deployment you run yourself. That sounds like a tax. It's the opposite, because you only ever run one. A single outpost serves many providers: each new application binds a new provider to the same instance. When the browser IDE arrived - the second tenant - the marginal cost was a provider object, a route pointing its hostname at the outpost, and a matching pair of network policies. No new deployment. App N+1 is a nameplate at the desk, not a second desk.
Standing up the desk the first time is where the hours went. Three traps, each with a price paid in real time:
- A provider created by automation isn't a provider created by the UI. Scripting the provider into existence skipped the OAuth defaults the admin UI sets silently - the redirect URIs sat empty, and every successful login dumped the user onto the identity provider's own homepage with a shrug of "no provider url". The login worked; it just went nowhere. Set the defaults explicitly, every time.
- The outpost has to be told which host the browser sees. Left alone it knows only its internal service name, and it will happily redirect your browser to an address that exists nowhere outside the cluster. One environment variable carrying the browser-facing hostname closes the loop, and it wins over anything set through the API.
- A token with an invisible trailing newline fails like everything else fails. The outpost's token, pasted into a secret, carried a newline you cannot see. The authorisation header it produces is invalid, and the symptoms are indistinguishable from both traps above. Strip the newline before the secret ever exists.
Couriers don't queue at reception
The registry is the sharper case, because its real customers were never people. Every node's container runtime pulls images through its API - the /v2 path - dozens of times a day. Put the doorman in front of that and every pull in the fleet dies waiting on a login page no runtime can answer.
So the route splits at the gateway. /v2 goes straight to the registry. Everything else on the same hostname - the UI, the search - goes to the outpost and gets the full sign-in. And the direct path isn't left swinging open as the price of working: it's keyed with its own scoped, read-only identity, carried in each node's registry configuration. The couriers don't queue at reception, and reception doesn't prop the dock open for them either.
One hostname, two audiences, two doors, and neither is weakened to accommodate the other.
Rattle every door before you trust the lock
The proof discipline matters as much as the pattern. The easy check is to load the homepage, watch it bounce to the sign-in page, and call the thing secured. But the homepage is the door nobody breaks in through.
The browser IDE made this concrete. Its whole value is a terminal speaking over a WebSocket, and a WebSocket doesn't render a login page. It either connects or it doesn't. Had the route carved that path out for convenience, the front door would be locked while a side corridor ran straight past the desk, exactly where no identity is ever checked. The same goes for static assets, and for the health endpoint.
So the route sends every path on the host to the outpost, and the check was empirical: an unauthenticated request to the root, to the terminal WebSocket, to a static asset and to the health endpoint each came back as a redirect to the identity provider. All of them. On a human door, a single carve-out is the hole; carve-outs belong only on machine doors, where they get keys of their own. And behind the doorman, the IDE still keeps a lock of its own - the desk adds a layer; it doesn't replace the one on the office door.
Slice auth by who arrives
Slice authentication by who arrives, not by what serves the request. Humans are one audience - interactive, browser-borne, phishable, and they get a doorman: one strong flow, every human door in the estate behind the same desk, so door N+1 inherits the whole policy for the cost of a nameplate. Machines are the other audience - headless, scoped, incapable of answering a challenge, and they get narrow keys to their own doors, which the doorman never learns about.
The one-layer instinct fails because it treats the app as the unit of security. The unit is the audience. Sign the people in at one desk, key the machines at the dock, and neither door ever has to weaken to let the other's traffic through.
Live in the homelab: one Authentik proxy outpost on the cave cluster fronting the Zot registry UI and a code-server IDE; the registry's /v2 pull path keyed separately with a read-only identity across six Talos nodes.

