Add SSO+2FA and outbound-exposure posts with Excalidraw architecture diagrams; Diagram component serves outlined SVGs
build-and-deploy / build (push) Failing after 12m1s
build-and-deploy / build (push) Failing after 12m1s
This commit is contained in:
@@ -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.
|
||||
|
||||

|
||||
|
||||
## 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*.
|
||||
|
||||

|
||||
|
||||
## 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.
|
||||
Reference in New Issue
Block a user