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
build-and-deploy / build (push) Failing after 11m8s
This commit is contained in:
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 |
@@ -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 |
@@ -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.
|
||||||
|
|
||||||
|

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