blog: republish 2 edited posts, dossier thumbnails 4-6, new post broker-pattern
Content: shipping-this-site + init-gating-gpu-readiness get their edited-workshop bodies (from the
NAS Published/ pass) - site frontmatter (hero/heroAlt) kept, only prose replaced.
Dossiers 004-006 (gpu-as-code, global-infra-modernisation, network-automation-fleet) were all
rendering the same generic pipeline thumbnail - added 3 tailored DossierThumb motifs (gpu die +
passthrough lanes, region globe, switch-fleet grid) via a new diagram: frontmatter field. Also
closed the dead-space gap above the tag row on short-argument cards with a faint on-theme grid
fill (::before, masked fade) instead of a flat void.
New post: broker-pattern ('An agent should never hold the key it's using') - SECURITY SERIES 02,
between secret-zero and workload-least-privilege. Hero de-watermarked from the NAS source (resized
to the calibrated 1600x1073 coordinate space, Jacobi-diffused out the Nano Banana sparkle at its
known center) via scripts/dewatermark-broker-pattern.mjs (reusable for the next 3 drafts). New
diagram/broker-pattern.svg in the house light-card palette. Inline top-of-body hero stripped per
the astro-static-site skill guard (frontmatter hero: is now the only render path).
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 237 KiB |
@@ -0,0 +1,60 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="1180" height="430" viewBox="0 0 1180 430" role="img" aria-label="The broker-pattern flow: an untrusted agent asks a credential broker, which mints a scoped short-lived capability without exposing any credential; the agent hands that capability to a tool broker, which verifies it and performs the action using a credential it holds; any write is staged behind a human approval gate on a phone before it executes; the credential never leaves the trusted brokers.">
|
||||||
|
<rect width="1180" height="430" fill="#ffffff"></rect>
|
||||||
|
|
||||||
|
<!-- title -->
|
||||||
|
<text x="60" y="46" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="23" font-weight="700" fill="#0e1726">The broker pattern: ask, never hold</text>
|
||||||
|
<text x="61" y="70" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="13.5" fill="#64748b">the agent can only ask — every credential stays behind the brokers, every write waits on a human</text>
|
||||||
|
|
||||||
|
<!-- trust boundary: untrusted (left) vs trusted (right) -->
|
||||||
|
<rect x="36" y="196" width="230" height="164" rx="16" fill="#c026d3" fill-opacity="0.04" stroke="#c026d3" stroke-width="2.2" stroke-dasharray="7 6"></rect>
|
||||||
|
<text x="54" y="218" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="13" font-weight="700" fill="#c026d3">untrusted executor</text>
|
||||||
|
|
||||||
|
<rect x="298" y="196" width="846" height="164" rx="16" fill="#0c8fce" fill-opacity="0.04" stroke="#0c8fce" stroke-width="2.2" stroke-dasharray="7 6"></rect>
|
||||||
|
<text x="316" y="218" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="13" font-weight="700" fill="#0c8fce">trusted tier — holds every credential</text>
|
||||||
|
|
||||||
|
<!-- agent box -->
|
||||||
|
<rect x="56" y="236" width="188" height="100" rx="14" fill="#ffffff" stroke="#c026d3" stroke-width="1.8"></rect>
|
||||||
|
<text x="150" y="272" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="15.5" font-weight="700" fill="#c026d3">agent</text>
|
||||||
|
<text x="150" y="298" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#64748b">sandboxed · assume compromised</text>
|
||||||
|
<text x="150" y="319" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#0e1726">holds a capability, not a key</text>
|
||||||
|
|
||||||
|
<!-- credential broker -->
|
||||||
|
<rect x="320" y="236" width="196" height="100" rx="14" fill="#ffffff" stroke="#0e1726" stroke-width="1.6"></rect>
|
||||||
|
<text x="418" y="266" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="14" font-weight="700" fill="#0e1726">credential broker</text>
|
||||||
|
<text x="418" y="290" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#64748b">verifies the sandbox identity</text>
|
||||||
|
<text x="418" y="308" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#0e1726">mints a scoped, one-time,</text>
|
||||||
|
<text x="418" y="324" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#0e1726">expiring capability</text>
|
||||||
|
|
||||||
|
<!-- tool broker -->
|
||||||
|
<rect x="598" y="236" width="196" height="100" rx="14" fill="#ffffff" stroke="#0e1726" stroke-width="1.6"></rect>
|
||||||
|
<text x="696" y="266" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="14" font-weight="700" fill="#0e1726">tool broker</text>
|
||||||
|
<text x="696" y="290" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#64748b">checks signature · scope · nonce</text>
|
||||||
|
<text x="696" y="308" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#0e1726">runs the tool with a credential</text>
|
||||||
|
<text x="696" y="324" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#0e1726">it holds — never the agent</text>
|
||||||
|
|
||||||
|
<!-- human approval gate -->
|
||||||
|
<rect x="876" y="236" width="196" height="100" rx="14" fill="#ffffff" stroke="#7c3aed" stroke-width="1.8"></rect>
|
||||||
|
<text x="974" y="266" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="14.5" font-weight="700" fill="#0e1726">human approval</text>
|
||||||
|
<text x="974" y="290" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#64748b">every write is staged, not run</text>
|
||||||
|
<text x="974" y="308" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#0e1726">one-time token · fail-closed</text>
|
||||||
|
<text x="974" y="324" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#0e1726">timeout = denied</text>
|
||||||
|
|
||||||
|
<!-- arrows -->
|
||||||
|
<path d="M244 286 H314" fill="none" stroke="#64748b" stroke-width="2.2" stroke-linecap="round"></path>
|
||||||
|
<path d="M304 280 L314 286 M304 292 L314 286" fill="none" stroke="#64748b" stroke-width="2.2" stroke-linecap="round"></path>
|
||||||
|
<path d="M516 286 H592" fill="none" stroke="#64748b" stroke-width="2.2" stroke-linecap="round"></path>
|
||||||
|
<path d="M582 280 L592 286 M582 292 L592 286" fill="none" stroke="#64748b" stroke-width="2.2" stroke-linecap="round"></path>
|
||||||
|
<path d="M794 286 H870" fill="none" stroke="#64748b" stroke-width="2.2" stroke-linecap="round"></path>
|
||||||
|
<path d="M860 280 L870 286 M860 292 L870 286" fill="none" stroke="#64748b" stroke-width="2.2" stroke-linecap="round"></path>
|
||||||
|
|
||||||
|
<!-- arrow labels -->
|
||||||
|
<rect x="253" y="255" width="52" height="19" rx="9.5" fill="#ffffff"></rect>
|
||||||
|
<text x="279" y="268" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#64748b">asks</text>
|
||||||
|
<rect x="509" y="255" width="90" height="19" rx="9.5" fill="#ffffff"></rect>
|
||||||
|
<text x="554" y="268" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#64748b">capability</text>
|
||||||
|
<rect x="786" y="255" width="78" height="19" rx="9.5" fill="#ffffff"></rect>
|
||||||
|
<text x="825" y="268" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#64748b">stages write</text>
|
||||||
|
|
||||||
|
<!-- footer takeaway -->
|
||||||
|
<text x="60" y="398" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="11.5" fill="#64748b">The agent can only ask. Every credential, every write-authority, every scope decision lives at the broker — a compromised agent's worst case is a denied request.</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 6.6 KiB |
@@ -37,6 +37,58 @@ const { kind = "generic" } = Astro.props;
|
|||||||
<circle cx="310" cy="45" r="12" class="nd nd--m" />
|
<circle cx="310" cy="45" r="12" class="nd nd--m" />
|
||||||
<circle cx="310" cy="135" r="12" class="nd nd--m" />
|
<circle cx="310" cy="135" r="12" class="nd nd--m" />
|
||||||
</>
|
</>
|
||||||
|
) : kind === "gpu" ? (
|
||||||
|
<>
|
||||||
|
{/* GPU die with passthrough lanes feeding two edge nodes */}
|
||||||
|
<rect x="150" y="55" width="100" height="70" rx="6" class="nd nd--c" />
|
||||||
|
<line x1="164" y1="55" x2="164" y2="40" class="ln ln--c" />
|
||||||
|
<line x1="186" y1="55" x2="186" y2="40" class="ln ln--c" />
|
||||||
|
<line x1="208" y1="55" x2="208" y2="40" class="ln ln--m" />
|
||||||
|
<line x1="230" y1="55" x2="230" y2="40" class="ln ln--m" />
|
||||||
|
<rect x="172" y="72" width="56" height="36" rx="3" fill="none" stroke="rgba(63,186,245,0.4)" stroke-width="1" />
|
||||||
|
<text x="200" y="94" text-anchor="middle" class="tx">GPU</text>
|
||||||
|
<line x1="250" y1="70" x2="330" y2="55" class="ln ln--c" />
|
||||||
|
<line x1="250" y1="110" x2="330" y2="125" class="ln ln--m" />
|
||||||
|
<rect x="330" y="40" width="30" height="26" rx="4" class="nd nd--dim" />
|
||||||
|
<rect x="330" y="112" width="30" height="26" rx="4" class="nd nd--m" />
|
||||||
|
<circle cx="290" cy="62" r="2.5" class="dot dot--c pulse" />
|
||||||
|
<circle cx="290" cy="117" r="2.5" class="dot dot--m pulse-slow" />
|
||||||
|
</>
|
||||||
|
) : kind === "globe" ? (
|
||||||
|
<>
|
||||||
|
{/* globe of regions — a modernised multi-region estate */}
|
||||||
|
<circle cx="200" cy="90" r="52" fill="none" stroke="rgba(63,186,245,0.35)" stroke-width="1" />
|
||||||
|
<ellipse cx="200" cy="90" rx="52" ry="20" fill="none" stroke="rgba(63,186,245,0.25)" stroke-width="1" />
|
||||||
|
<ellipse cx="200" cy="90" rx="20" ry="52" fill="none" stroke="rgba(63,186,245,0.25)" stroke-width="1" />
|
||||||
|
<line x1="148" y1="90" x2="252" y2="90" stroke="rgba(63,186,245,0.25)" stroke-width="1" />
|
||||||
|
<circle cx="176" cy="62" r="4" class="nd nd--c" />
|
||||||
|
<circle cx="228" cy="66" r="4" class="nd nd--m" />
|
||||||
|
<circle cx="182" cy="118" r="4" class="nd nd--dim" />
|
||||||
|
<circle cx="222" cy="114" r="4" class="nd nd--c" />
|
||||||
|
<circle cx="200" cy="90" r="3" class="dot dot--c pulse" />
|
||||||
|
<line x1="176" y1="62" x2="200" y2="90" class="ln ln--c" />
|
||||||
|
<line x1="228" y1="66" x2="200" y2="90" class="ln ln--m" />
|
||||||
|
<line x1="182" y1="118" x2="200" y2="90" class="ln ln--c" />
|
||||||
|
<line x1="222" y1="114" x2="200" y2="90" class="ln ln--m" />
|
||||||
|
</>
|
||||||
|
) : kind === "network-fleet" ? (
|
||||||
|
<>
|
||||||
|
{/* fleet of switches, single-pane managed */}
|
||||||
|
<rect x="170" y="30" width="60" height="24" rx="4" class="nd nd--c" />
|
||||||
|
<text x="200" y="47" text-anchor="middle" class="tx">NCM</text>
|
||||||
|
<line x1="200" y1="54" x2="200" y2="70" class="ln ln--c" />
|
||||||
|
<line x1="80" y1="70" x2="320" y2="70" class="ln ln--c" />
|
||||||
|
<line x1="100" y1="70" x2="100" y2="120" class="ln ln--dim" />
|
||||||
|
<line x1="160" y1="70" x2="160" y2="120" class="ln ln--dim" />
|
||||||
|
<line x1="240" y1="70" x2="240" y2="120" class="ln ln--m" />
|
||||||
|
<line x1="300" y1="70" x2="300" y2="120" class="ln ln--m" />
|
||||||
|
<rect x="82" y="120" width="36" height="18" rx="3" class="nd nd--dim" />
|
||||||
|
<rect x="142" y="120" width="36" height="18" rx="3" class="nd nd--dim" />
|
||||||
|
<rect x="222" y="120" width="36" height="18" rx="3" class="nd nd--m" />
|
||||||
|
<rect x="282" y="120" width="36" height="18" rx="3" class="nd nd--m" />
|
||||||
|
<circle cx="100" cy="70" r="2.5" class="dot dot--c pulse" />
|
||||||
|
<circle cx="300" cy="70" r="2.5" class="dot dot--m pulse-slow" />
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{/* pipeline — commits flowing through gates to a fleet */}
|
{/* pipeline — commits flowing through gates to a fleet */}
|
||||||
|
|||||||
@@ -79,12 +79,26 @@ const dossierNo = String(index).padStart(3, "0");
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dossier__body {
|
.dossier__body {
|
||||||
|
position: relative;
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
/* a faint on-theme grid fills the idle space above the chips/meta (the
|
||||||
|
margin-top:auto gap on shorter "argument" text) instead of a flat void */
|
||||||
|
.dossier__body::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 68px 24px 64px;
|
||||||
|
background-image:
|
||||||
|
linear-gradient(var(--grid-line) 1px, transparent 1px),
|
||||||
|
linear-gradient(90deg, var(--grid-line) 1px, transparent 1px);
|
||||||
|
background-size: 22px 22px;
|
||||||
|
mask-image: linear-gradient(180deg, black, transparent 85%);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
.dossier__title {
|
.dossier__title {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: var(--font-display);
|
font-family: var(--font-display);
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
---
|
||||||
|
title: "An agent should never hold the key it's using"
|
||||||
|
date: 2026-07-03
|
||||||
|
summary: "You want an AI agent that can actually do things — call APIs, touch real data. You also don't fully trust it. The resolution isn't a better sandbox; it's making sure the agent never possesses a credential at all. A broker holds the keys, mints short-lived capabilities, and gates every write behind a human. Here's the pattern."
|
||||||
|
tags: ["security", "ai-agents", "architecture", "zero-trust", "homelab"]
|
||||||
|
draft: false
|
||||||
|
hero: "/blog/broker-pattern.webp"
|
||||||
|
heroAlt: "An untrusted agent reaches through a sealed gate to ask; on the far side, a guardian holds a ring of keys the agent can never touch."
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
The previous post put untrusted code in a hardware-isolated VM, and ended on a caveat: isolation contains an
|
||||||
|
*escape*, but it does nothing about an agent **misusing a tool it was legitimately given**. If you hand an AI
|
||||||
|
agent a database credential so it can be useful, a single bad decision — a prompt injection, a confused chain
|
||||||
|
of reasoning — spends that credential. The sandbox did its job perfectly and you still got robbed, through the
|
||||||
|
front door you built.
|
||||||
|
|
||||||
|
So the real question isn't "how do I isolate the agent?" It's "how does the agent get work done *without ever
|
||||||
|
holding a key*?"
|
||||||
|
|
||||||
|
## The agent holds a capability, not a credential
|
||||||
|
|
||||||
|
The pattern is to put every credential, every tool, and every model endpoint **behind a broker**, and give the
|
||||||
|
agent only a *capability to ask*. The agent never sees a token. It calls the broker; the broker holds the real
|
||||||
|
credential, decides whether the request is allowed, and — if it is — performs the action itself and returns the
|
||||||
|
result. The key never leaves the broker.
|
||||||
|
|
||||||
|
That one inversion changes the threat model completely. A fully compromised agent can now do exactly one thing:
|
||||||
|
**ask**. And asking is answered by something it can't reach, can't impersonate, and can't bypass.
|
||||||
|
|
||||||
|
<!-- DIAGRAM: untrusted sandbox → cred-broker (mint capability) → mcp-broker (verify + run tool with held cred) → human-approval gate → downstream. Agent never touches the credential. -->
|
||||||
|

|
||||||
|
|
||||||
|
## Two clusters, two brokers
|
||||||
|
|
||||||
|
The trust boundary is physical, not just logical. The **untrusted executor** (the Kata sandboxes) lives on one
|
||||||
|
cluster; the **trusted tier** (the brokers, the model gateway, the real credentials) lives on a *separate*
|
||||||
|
cluster. A total compromise of the executor still can't reach the brokers' secrets except across a policed
|
||||||
|
network link — there's no shared kernel, no shared API server, nothing to pivot through.
|
||||||
|
|
||||||
|
On the trusted side there are two brokers, deliberately split:
|
||||||
|
|
||||||
|
- A **credential broker** validates the sandbox's identity (a short-lived, signed token unique to the task) and
|
||||||
|
mints a **capability** — a cryptographically signed, scoped, single-use, expiring grant. Not a credential. A
|
||||||
|
*permission to ask for one specific thing*.
|
||||||
|
- A **tool broker** takes that capability, verifies the signature, the scope, and the one-time nonce, and only
|
||||||
|
then runs the requested tool — using a credential *it* holds. The result comes back; the credential doesn't.
|
||||||
|
|
||||||
|
## A human gates every write
|
||||||
|
|
||||||
|
Reads are one thing. For anything that *changes the world* — creating, deleting, sending — the broker doesn't
|
||||||
|
just decide on policy. It **stages** the action and pings a human: an Approve/Deny prompt on my phone, carrying
|
||||||
|
a one-time token bound to the exact task, method, and arguments. Tap approve and it executes; tap deny, or
|
||||||
|
ignore it, and it doesn't. The gate is **fail-closed**: a timeout is a denial, a replayed token is rejected, an
|
||||||
|
unknown method is rejected. The default, always, is *no*.
|
||||||
|
|
||||||
|
The load-bearing principle underneath all of it: **enforcement that has to survive a compromised agent lives at
|
||||||
|
the broker, never at the agent or the orchestrator.** Approval, scope, budgets, write-authority — none of it
|
||||||
|
lives anywhere the agent's reasoning can touch. The agent can be wrong, jailbroken, or outright hostile, and the
|
||||||
|
worst-case is still just *a request that gets refused*.
|
||||||
|
|
||||||
|
## Useful and safe at the same time
|
||||||
|
|
||||||
|
It's tempting to think you have to choose: give the agent real power and accept real risk, or lock it down so
|
||||||
|
hard it can't do anything. The broker pattern is how you get both. The agent is *useful* — it can call real
|
||||||
|
tools against real systems. It's *safe* — it never holds a key, every write waits on a human, and the moment
|
||||||
|
something goes wrong, the blast radius is a denied request, not a spent credential.
|
||||||
|
|
||||||
|
Run agents like you'd run any other untrusted input: assume it's compromised, and make sure that assumption is
|
||||||
|
*boring*.
|
||||||
|
|
||||||
|
*Live as the trusted tier of a two-cluster AI-agent platform: a credential broker and a tool broker holding the
|
||||||
|
keys, a phone-based human-approval gate on every write, and an agent that — by construction — never sees a
|
||||||
|
secret.*
|
||||||
@@ -8,18 +8,20 @@ heroAlt: "A GPU glows behind a sealed checkpoint gate while a waiting pod-orb is
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
The most common way a GPU workload fails at the edge isn't the model, the driver, or the
|
The most common way a GPU workload fails at the edge isn't the model, the driver, or the network.
|
||||||
network. It's timing. Kubernetes is eager — it will happily schedule your inference pod the
|
It's timing. Kubernetes is eager — it will happily schedule your inference pod the moment a node
|
||||||
moment a node is `Ready`, which is often *before* the NVIDIA device plugin has advertised
|
reports `Ready`, which is often *before* the NVIDIA device plugin has advertised `nvidia.com/gpu`.
|
||||||
`nvidia.com/gpu`. The pod starts, can't see a GPU, crash-loops, and now your rollout is
|
The pod starts, can't see a GPU, crash-loops — and now your rollout is poisoned across the fleet,
|
||||||
poisoned across the fleet.
|
on boxes nobody is standing next to.
|
||||||
|
|
||||||
The fix is to make readiness explicit. Don't trust node-`Ready`; gate on the GPU.
|
Node-`Ready` answers the wrong question. It says the kubelet is up. It says nothing about whether
|
||||||
|
the one piece of hardware your workload exists to use is actually there yet. So stop trusting it:
|
||||||
|
make GPU readiness explicit, and gate on that.
|
||||||
|
|
||||||
## Gate the schedule, not just the start
|
## Gate the schedule, then gate the start
|
||||||
|
|
||||||
A resource request is the first line — a pod that *requests* a GPU won't schedule until the
|
The first gate is free — a resource request. A pod that *requests* a GPU won't schedule until the
|
||||||
plugin advertises capacity:
|
device plugin advertises capacity:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
resources:
|
resources:
|
||||||
@@ -27,9 +29,10 @@ resources:
|
|||||||
nvidia.com/gpu: 1
|
nvidia.com/gpu: 1
|
||||||
```
|
```
|
||||||
|
|
||||||
But on a single-GPU edge node that's recovering from a reboot, you still want a hard check
|
That handles the common case. But on a single-GPU edge node recovering from a power cut, there's a
|
||||||
before the workload does anything expensive. An init container that blocks until the device
|
window where the plugin has advertised the device and the driver is still finding its feet — and
|
||||||
is real keeps the main container honest:
|
you don't want an expensive model load to be the thing that discovers it. So the second gate is an
|
||||||
|
init container that blocks until the device is demonstrably real, and fails loudly if it never is:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
@@ -45,11 +48,21 @@ echo "GPU never became ready" >&2
|
|||||||
exit 1
|
exit 1
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Two gates, two failure modes closed: the scheduler can't place the pod before capacity exists, and
|
||||||
|
the workload can't start before the hardware answers. Note the bound — an init gate that waits
|
||||||
|
forever isn't a gate, it's a hang. Two and a half minutes, then fail loud and let the platform
|
||||||
|
retry. Fail-closed, never fail-quiet.
|
||||||
|
|
||||||
## Why this is the win
|
## Why this is the win
|
||||||
|
|
||||||
Once readiness is gated, the whole class of "pod started before the GPU" failures disappears
|
Once readiness is gated, the entire class of "pod started before the GPU" failures disappears —
|
||||||
— and it disappears *the same way on every node*. That consistency is the real prize at the
|
and it disappears *the same way on every node*. That consistency is the real prize at the edge.
|
||||||
edge, where no one is standing next to the box to nurse a bad rollout.
|
A fix that requires a human to notice, shell in, and nurse a bad rollout doesn't scale past the
|
||||||
|
first dozen sites; a gate that makes every node converge identically after every reboot does.
|
||||||
|
|
||||||
The principle generalises: at the edge, **design the dependency, don't hope for it**. The GPU
|
## The principle
|
||||||
is just the first dependency worth gating; egress paths and model artifacts are next.
|
|
||||||
|
At the edge, **design the dependency — don't hope for it**. Anything your workload cannot run
|
||||||
|
without deserves an explicit, bounded, fail-loud gate between it and the scheduler's optimism.
|
||||||
|
The GPU is just the first dependency worth naming; egress paths and model artifacts are next,
|
||||||
|
and they want the same treatment.
|
||||||
|
|||||||
@@ -8,25 +8,28 @@ heroAlt: "A glowing data container travels a luminous rail from a small server r
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
This site is a static Astro build, but how it gets to you is the interesting part. It's
|
This site is a static Astro build, and that's the least interesting thing about it. What matters
|
||||||
served from my homelab Kubernetes cluster over a Cloudflare Tunnel, deployed the same way I'd
|
is how it reaches you: served from my homelab Kubernetes cluster over a Cloudflare Tunnel, shipped
|
||||||
ship anything else: as an immutable image, pinned by digest, reconciled by GitOps.
|
the way I'd ship anything I actually cared about — an immutable image, pinned by digest, reconciled
|
||||||
|
by GitOps. No special case for "it's just a website."
|
||||||
|
|
||||||
## The pipeline
|
## The pipeline
|
||||||
|
|
||||||
1. The site is built and baked into a hardened `nginx-unprivileged` image.
|
The build is baked into a hardened `nginx-unprivileged` image and pushed to a **self-hosted public
|
||||||
2. The image is pushed to a **self-hosted public Gitea registry** — deliberately separate
|
Gitea registry** — deliberately a separate instance from the private one holding my infrastructure
|
||||||
from the private instance that holds my infrastructure code.
|
code, so the public artifact and the private estate never share a trust boundary. The image digest
|
||||||
3. The image digest is pinned in a private `home-ops` repo.
|
is then pinned in a private `home-ops` repo, **ArgoCD** reconciles that repo onto the cluster, and
|
||||||
4. **ArgoCD** reconciles that repo onto the cluster.
|
a **Cloudflare Tunnel** exposes exactly one service — this site — outbound-only.
|
||||||
5. A **Cloudflare Tunnel** exposes exactly one service — this site — outbound-only.
|
|
||||||
|
|
||||||
No open ports. No server runtime. No registry credential on the cluster, because the public
|
Follow the chain and notice what's missing. No open ports: the tunnel dials out. No server runtime:
|
||||||
package is anonymous-pull and the image holds nothing secret.
|
the output is static files behind nginx. No registry credential on the cluster: the public package
|
||||||
|
is anonymous-pull, and the image holds nothing secret to protect. Every link in the pipeline is
|
||||||
|
either immutable, declarative, or absent.
|
||||||
|
|
||||||
## Security as acceptance criteria
|
## Security as acceptance criteria
|
||||||
|
|
||||||
The interesting constraint was treating security as a checklist to *pass*, not a vibe:
|
The discipline that made it work was treating security as a checklist to *pass*, not a vibe to
|
||||||
|
gesture at. The site didn't ship until every box was ticked:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
[x] Static output — no server runtime to attack
|
[x] Static output — no server runtime to attack
|
||||||
@@ -36,8 +39,14 @@ The interesting constraint was treating security as a checklist to *pass*, not a
|
|||||||
[x] Outbound-only tunnel, single hostname, no catch-all
|
[x] Outbound-only tunnel, single hostname, no catch-all
|
||||||
```
|
```
|
||||||
|
|
||||||
## Why bother
|
A checklist sounds bureaucratic until you notice what it changes: each item is a claim you can
|
||||||
|
verify, and a failing item blocks the ship. "Pretty secure" isn't a state you can test for.
|
||||||
|
`grep` finding zero secrets in the bundle is.
|
||||||
|
|
||||||
Because the site *is* the argument. A platform engineer's portfolio should demonstrate the
|
## The principle
|
||||||
discipline it's advertising — and "it's a static page" is no excuse to skip the rigour. The
|
|
||||||
deployment story is part of the work.
|
The site *is* the argument. A platform engineer's portfolio should demonstrate the discipline it
|
||||||
|
advertises, and "it's a static page" is no excuse to skip the rigour — it's the cheapest possible
|
||||||
|
place to practise it. If the pipeline behind a brochure site is immutable, verified, and
|
||||||
|
zero-trust, that's not overkill. That's the standard, rehearsed where the stakes are low so it
|
||||||
|
holds where they aren't.
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ period: "2019 – 2025"
|
|||||||
stack: ["VMware / vSphere", "Azure (Blob, AVS)", "Microsoft 365", "SD-WAN", "Aruba ClearPass", "Palo Alto / FortiGate", "Veeam"]
|
stack: ["VMware / vSphere", "Azure (Blob, AVS)", "Microsoft 365", "SD-WAN", "Aruba ClearPass", "Palo Alto / FortiGate", "Veeam"]
|
||||||
featured: false
|
featured: false
|
||||||
order: 40
|
order: 40
|
||||||
|
diagram: "globe"
|
||||||
---
|
---
|
||||||
|
|
||||||
## Problem
|
## Problem
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ period: "2025 – Present"
|
|||||||
stack: ["GPU passthrough", "ESXi", "DCGM Exporter", "Prometheus", "Bash", "Watchdogs"]
|
stack: ["GPU passthrough", "ESXi", "DCGM Exporter", "Prometheus", "Bash", "Watchdogs"]
|
||||||
featured: false
|
featured: false
|
||||||
order: 30
|
order: 30
|
||||||
|
diagram: "gpu"
|
||||||
---
|
---
|
||||||
|
|
||||||
## Problem
|
## Problem
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ period: "2019 – 2022"
|
|||||||
stack: ["Unimus (NCM)", "NetBox (IPAM / SoT)", "Config backup & DR", "Bulk config push", "Credential vaulting + rotation", "Multi-vendor switching"]
|
stack: ["Unimus (NCM)", "NetBox (IPAM / SoT)", "Config backup & DR", "Bulk config push", "Credential vaulting + rotation", "Multi-vendor switching"]
|
||||||
featured: false
|
featured: false
|
||||||
order: 45
|
order: 45
|
||||||
|
diagram: "network-fleet"
|
||||||
links:
|
links:
|
||||||
- label: "Unimus"
|
- label: "Unimus"
|
||||||
href: "https://unimus.net"
|
href: "https://unimus.net"
|
||||||
|
|||||||
+1
-1
@@ -2,6 +2,6 @@
|
|||||||
// The security series threads the zero-trust arc across posts as they publish.
|
// The security series threads the zero-trust arc across posts as they publish.
|
||||||
export const series: Record<string, { name: string; number: number }> = {
|
export const series: Record<string, { name: string; number: number }> = {
|
||||||
"secret-zero": { name: "SECURITY SERIES", number: 1 },
|
"secret-zero": { name: "SECURITY SERIES", number: 1 },
|
||||||
|
"broker-pattern": { name: "SECURITY SERIES", number: 2 },
|
||||||
"workload-least-privilege": { name: "SECURITY SERIES", number: 3 },
|
"workload-least-privilege": { name: "SECURITY SERIES", number: 3 },
|
||||||
// broker-pattern → 02 when it publishes (currently a draft in the workshop)
|
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user