Add SSO+2FA and outbound-exposure posts with Excalidraw architecture diagrams; Diagram component serves outlined SVGs
build-and-deploy / build (push) Failing after 12m1s

This commit is contained in:
2026-06-22 16:26:15 +10:00
parent 1c87af560d
commit cbb82eb97b
9 changed files with 148 additions and 192 deletions
+58
View File
@@ -0,0 +1,58 @@
---
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
---
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)
## 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.
## The principle
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.
@@ -0,0 +1,53 @@
---
title: "The most secure inbound port is the one you never open"
date: 2026-06-23
summary: "Exposing self-hosted services to the internet with zero open ports, zero port-forwarding, and the origin's IP never leaving the building."
tags: ["cloudflare", "networking", "security", "zero-trust", "kubernetes"]
draft: false
---
The default way to put a home service on the internet is to forward a port: poke a hole in the router, map
it to the box, and hope the thing behind it never has a bad day. Every open port is a standing invitation
that's online whether or not anyone's knocking.
There's a better shape: don't open anything. Let the origin dial *out*.
![Outbound-only exposure — public traffic hits a Cloudflare edge with WAF and geo rules; the origin holds a single outbound tunnel; nothing is port-forwarded](/diagrams/outbound-exposure.svg)
## The tunnel dials out
A lightweight connector runs next to the service and opens a persistent **outbound** connection to the edge
network. Public traffic arrives at the edge, and the edge hands it back down that already-open tunnel. The
router has no inbound rule. The origin's public IP is never advertised. Port-scan the home connection and
there's nothing listening — because there isn't.
You've inverted the trust direction: instead of the internet reaching *in*, the box reaches *out*.
## Same name, two answers
The wrinkle is DNS. I run **split-horizon**: the internal domain resolves to in-cluster ingress for anyone
on the LAN, and the public domain resolves through the edge for the outside world. Same services, two names,
two answers depending on where you're standing — and the internal estate is never reachable through the
public path.
This bites in a non-obvious way too: a pod *inside* the cluster can't resolve the public hostname (it gets
the internal view), so any server-side callout has to target the internal service address directly.
Browser-facing stays public; machine-to-machine stays internal.
## The edge is the perimeter
Because every request now funnels through one edge before it ever touches home, that edge *is* the firewall.
So the controls live there, not on the origin:
- geo-fence to the regions you actually serve,
- rate-limit the authentication endpoints,
- put access control in front of anything administrative.
The origin's job shrinks to one thing: hold the tunnel open and serve. Everything hostile is filtered a
continent away.
## The principle
Exposure isn't binary, and it isn't a synonym for port-forwarding. Push the perimeter out to an edge you
don't host, make the origin speak only outbound, and the attack surface at home collapses to zero open
ports. The safest port is the one that was never listening.