diff --git a/public/diagrams/authentik-sso.svg b/public/diagrams/authentik-sso.svg
new file mode 100644
index 0000000..9d2cd92
--- /dev/null
+++ b/public/diagrams/authentik-sso.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/diagrams/edge-ai.svg b/public/diagrams/edge-ai.svg
new file mode 100644
index 0000000..11b4bdb
--- /dev/null
+++ b/public/diagrams/edge-ai.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/diagrams/homelab.svg b/public/diagrams/homelab.svg
new file mode 100644
index 0000000..c987bef
--- /dev/null
+++ b/public/diagrams/homelab.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/diagrams/iac-fleet.svg b/public/diagrams/iac-fleet.svg
new file mode 100644
index 0000000..90eedac
--- /dev/null
+++ b/public/diagrams/iac-fleet.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/diagrams/outbound-exposure.svg b/public/diagrams/outbound-exposure.svg
new file mode 100644
index 0000000..5226dca
--- /dev/null
+++ b/public/diagrams/outbound-exposure.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/Diagram.astro b/src/components/Diagram.astro
index 400fd8b..94e6d77 100644
--- a/src/components/Diagram.astro
+++ b/src/components/Diagram.astro
@@ -1,175 +1,48 @@
---
-// Hand-authored, theme-aware architecture diagrams rendered as inline SVG at build.
-// No client JS, no headless browser, no inline
diff --git a/src/content/blog/authentik-sso-2fa.md b/src/content/blog/authentik-sso-2fa.md
new file mode 100644
index 0000000..2635af0
--- /dev/null
+++ b/src/content/blog/authentik-sso-2fa.md
@@ -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.
diff --git a/src/content/blog/outbound-only-exposure.md b/src/content/blog/outbound-only-exposure.md
new file mode 100644
index 0000000..6d12d7b
--- /dev/null
+++ b/src/content/blog/outbound-only-exposure.md
@@ -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.
diff --git a/src/pages/blog/[slug].astro b/src/pages/blog/[slug].astro
index 63f9856..2fc85c3 100644
--- a/src/pages/blog/[slug].astro
+++ b/src/pages/blog/[slug].astro
@@ -85,4 +85,16 @@ const { Content } = await render(entry);
.post__body {
margin-top: var(--space-7);
}
+ /* Diagram images (Markdown ``) sit on a constant light card so the
+ fixed-colour SVGs stay legible in dark mode — mirrors the Diagram.astro component. */
+ .post__body :global(img[src^="/diagrams/"]) {
+ display: block;
+ width: 100%;
+ height: auto;
+ margin: var(--space-6) auto;
+ padding: var(--space-5);
+ background: #f6f8fb;
+ border: 1px solid rgba(2, 6, 23, 0.1);
+ border-radius: var(--radius);
+ }