site update: cover art for the learn-site case studies; publish untrusted-in-a-vm (SECURITY #7)
build-and-deploy / build (push) Failing after 11m8s

This commit is contained in:
2026-08-25 17:24:51 +10:00
parent c5b1044d7a
commit b5e0b25667
8 changed files with 137 additions and 0 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

+69
View File
@@ -0,0 +1,69 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1180" height="440" viewBox="0 0 1180 440" role="img" aria-label="Two isolation models compared. Left: three containers as processes sharing one host kernel, separated only by dotted namespace lines - an escape lands on the host. Right: each pod boots inside its own microVM with its own guest kernel behind the hardware hypervisor - an escape lands inside a disposable virtual machine, not on the host.">
<rect width="1180" height="440" fill="#ffffff"></rect>
<!-- titles -->
<text x="60" y="52" font-family="ui-monospace,Menlo,Consolas,monospace" font-size="17" font-weight="600" fill="#0f172a">namespaces: one shared kernel</text>
<text x="640" y="52" font-family="ui-monospace,Menlo,Consolas,monospace" font-size="17" font-weight="600" fill="#0f172a">Kata: a kernel per pod</text>
<!-- ===== LEFT: shared kernel ===== -->
<!-- three container processes, dotted separations -->
<g font-family="ui-monospace,Menlo,Consolas,monospace" font-size="14" text-anchor="middle">
<rect x="60" y="84" width="130" height="86" rx="8" fill="none" stroke="#0c8fce" stroke-width="2" stroke-dasharray="6 5"/>
<text x="125" y="122" fill="#0f172a">container</text><text x="125" y="142" fill="#64748b">process</text>
<rect x="215" y="84" width="130" height="86" rx="8" fill="none" stroke="#0c8fce" stroke-width="2" stroke-dasharray="6 5"/>
<text x="280" y="122" fill="#0f172a">container</text><text x="280" y="142" fill="#64748b">process</text>
<rect x="370" y="84" width="130" height="86" rx="8" fill="none" stroke="#c026d3" stroke-width="2" stroke-dasharray="6 5"/>
<text x="435" y="122" fill="#0f172a">untrusted</text><text x="435" y="142" fill="#64748b">process</text>
<!-- escape arrow: through the dotted floor into the kernel -->
<path d="M435 172 L435 208" stroke="#c026d3" stroke-width="2.5" marker-end="url(#am)"/>
<text x="522" y="196" fill="#c026d3" font-size="13">escape = host</text>
<!-- one kernel slab -->
<rect x="60" y="214" width="440" height="64" rx="8" fill="#eef2f7" stroke="#334155" stroke-width="2"/>
<text x="280" y="242" fill="#0f172a" font-weight="600">ONE host kernel</text>
<text x="280" y="262" fill="#64748b">shared by every container and the node</text>
<!-- host hardware -->
<rect x="60" y="292" width="440" height="46" rx="8" fill="none" stroke="#94a3b8" stroke-width="2"/>
<text x="280" y="321" fill="#64748b">host hardware</text>
<text x="280" y="376" fill="#64748b" font-size="13">dotted lines are namespaces:</text>
<text x="280" y="396" fill="#64748b" font-size="13">visibility control, not a security boundary</text>
</g>
<!-- ===== RIGHT: kata microVMs ===== -->
<g font-family="ui-monospace,Menlo,Consolas,monospace" font-size="14" text-anchor="middle">
<!-- two microVMs, solid walls, each with its own guest kernel -->
<g>
<rect x="640" y="84" width="220" height="150" rx="10" fill="none" stroke="#334155" stroke-width="3"/>
<text x="750" y="112" fill="#0f172a">pod</text>
<rect x="662" y="126" width="176" height="42" rx="6" fill="none" stroke="#0c8fce" stroke-width="2"/>
<text x="750" y="152" fill="#0f172a">container process</text>
<rect x="662" y="180" width="176" height="38" rx="6" fill="#eef2f7" stroke="#7c3aed" stroke-width="2"/>
<text x="750" y="204" fill="#7c3aed">own guest kernel</text>
</g>
<g>
<rect x="890" y="84" width="220" height="150" rx="10" fill="none" stroke="#334155" stroke-width="3"/>
<text x="1000" y="112" fill="#0f172a">untrusted pod</text>
<rect x="912" y="126" width="176" height="42" rx="6" fill="none" stroke="#c026d3" stroke-width="2"/>
<text x="1000" y="152" fill="#0f172a">agent process</text>
<rect x="912" y="180" width="176" height="38" rx="6" fill="#eef2f7" stroke="#7c3aed" stroke-width="2"/>
<text x="1000" y="204" fill="#7c3aed">own guest kernel</text>
</g>
<!-- escape arrow: contained inside the VM wall -->
<path d="M1000 160 Q 1052 168 1046 186" fill="none" stroke="#c026d3" stroke-width="2.5" marker-end="url(#am)"/>
<text x="1010" y="256" fill="#c026d3" font-size="13">escape = still inside a throwaway VM</text>
<!-- hypervisor + hardware -->
<rect x="640" y="272" width="470" height="46" rx="8" fill="#eef2f7" stroke="#334155" stroke-width="2"/>
<text x="875" y="301" fill="#0f172a" font-weight="600">hypervisor (KVM) - CPU-enforced boundary</text>
<rect x="640" y="332" width="470" height="42" rx="8" fill="none" stroke="#94a3b8" stroke-width="2"/>
<text x="875" y="359" fill="#64748b">host hardware (VT-x)</text>
<text x="875" y="406" fill="#64748b" font-size="13">runtimeClassName: kata - same kubectl, different boundary</text>
</g>
<!-- divider -->
<line x1="573" y1="70" x2="573" y2="400" stroke="#e2e8f0" stroke-width="2"/>
<defs>
<marker id="am" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">
<path d="M0 0L10 5L0 10z" fill="#c026d3"/>
</marker>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 5.0 KiB

+67
View File
@@ -0,0 +1,67 @@
---
title: "Untrusted code belongs in a VM, not a namespace"
date: 2026-08-25
summary: "A container isn't a security boundary - it's a process sharing the host kernel, wearing namespaces. For code you genuinely don't trust, that's not enough. Kata Containers give each pod its own microVM and guest kernel, with the kubectl ergonomics intact. Here's why, and what it costs."
tags: ["security", "kata", "kubernetes", "isolation", "ai-agents"]
hero: "/blog/untrusted-in-a-vm.webp"
heroAlt: "A sealed glowing microVM cube isolated above a darker host substrate, hard boundary between them"
draft: false
---
A container feels like a box. It isn't. It's a normal process on the host, wrapped in namespaces and cgroups so
it *can't see* most of the system - but it's still running on the **host's kernel**, sharing it with every other
container and with the node itself. Namespaces are an isolation *convenience*. They are not a security boundary
against code that is actively trying to get out.
For most workloads that's a fine trade. For code you genuinely don't trust - say, an AI agent executing
arbitrary tasks you didn't write - it isn't. One kernel bug, one container escape, and "isolated workload"
becomes "process on your node with your node's privileges." The boundary you were relying on was never really
there.
## Give it its own kernel
[Kata Containers](https://katacontainers.io) change the shape of the boundary. Instead of running the pod as a
process on the shared kernel, Kata boots each pod inside its own lightweight **virtual machine** - a microVM
with its *own guest kernel*, behind the hardware hypervisor (KVM). Now an escape doesn't land you on the host;
it lands you inside a throwaway VM, with a real CPU-enforced boundary (VT-x) between you and everything else.
The lovely part is that nothing about the *operator* experience changes. It's still a pod. You still
`kubectl apply` it, it still gets scheduled, mounts volumes, shows up in logs - you just set one field
(`runtimeClassName: kata`) and the container runtime quietly boots a VM instead of a namespace. The ergonomics
of Kubernetes, the isolation of a hypervisor.
![Shared-kernel namespaces versus per-pod microVMs](/diagrams/untrusted-in-a-vm.svg)
## How you know it's real
It's easy to *claim* hardware isolation; it's worth *proving*. The cleanest proof is the kernel itself: exec
into the pod and check the kernel version. If it's a different kernel from the host, it is - definitionally - a
different kernel, which means a real VM with its own guest, not a namespace dressed up as one. (On my setup the
guest reports one kernel and the Talos host another; that mismatch is the whole proof.) It needs hardware
virtualisation enabled (VT-x / KVM) - without it, Kata can't boot the guest and fails honestly rather than
silently downgrading.
## The cost is real, so design for it
A VM per pod isn't free. Each one carries a fixed memory overhead for the guest kernel and a cold-start measured
in seconds, not milliseconds. That changes how you plan:
- **It's a throughput tier, not a low-latency one.** Seconds-to-start means batch-shaped work, not
request-per-second serving. Fine for "run this task"; wrong for "answer this instantly."
- **Capacity is designed, not discovered.** Because the untrusted tier is, by definition, the part most likely
to misbehave, it gets a *hard* resource quota - a ceiling on memory and concurrent pods - so a runaway (or
hostile) sandbox can **never starve the control plane**. The scheduler also has to *know* about the per-VM
overhead, or it will quietly over-pack the node and OOM under load. You bound the blast radius with policy,
not hope.
## The honest caveat
Kata, plus a default-deny network around it, is a superb *isolation floor*: it contains an escape and it blocks
the pod from talking to things it shouldn't. But notice what it does **not** do. If the untrusted code is handed
a legitimate tool - a credential, an API it's *allowed* to call - hardware isolation won't stop it from misusing
that tool. The VM contains a break-out; it does nothing about exfiltration through a sanctioned door. That's a
different problem, and it needs a different answer -
[a broker that holds the keys the agent never sees](/blog/broker-pattern).
*Running on a single-node Talos cluster as the untrusted-execution tier of an AI-agent platform: Kata microVMs +
a default-deny floor + a designed capacity ceiling, verified guest-kernel-up.*
+1
View File
@@ -21,6 +21,7 @@ export const series: Record<string, { name: string; number: number }> = {
"authentik-sso-2fa": { name: "SECURITY SERIES", number: 4 }, "authentik-sso-2fa": { name: "SECURITY SERIES", number: 4 },
"one-doorman-many-doors": { name: "SECURITY SERIES", number: 5 }, "one-doorman-many-doors": { name: "SECURITY SERIES", number: 5 },
"clone-not-keys": { name: "SECURITY SERIES", number: 6 }, "clone-not-keys": { name: "SECURITY SERIES", number: 6 },
"untrusted-in-a-vm": { name: "SECURITY SERIES", number: 7 },
// NETWORKING SERIES - the eBPF datapath and the edge // NETWORKING SERIES - the eBPF datapath and the edge
"landing-on-cilium": { name: "NETWORKING SERIES", number: 1 }, "landing-on-cilium": { name: "NETWORKING SERIES", number: 1 },