<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: RuntimeStack</title>
    <description>The latest articles on DEV Community by RuntimeStack (@runtimestack).</description>
    <link>https://dev.to/runtimestack</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4200951%2F145bcabe-7a5c-43c7-9178-ed3f15344cab.png</url>
      <title>DEV Community: RuntimeStack</title>
      <link>https://dev.to/runtimestack</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/runtimestack"/>
    <language>en</language>
    <item>
      <title>Is a Docker Container a Sandbox? AI Agent Sandboxing Explained: seccomp, gVisor, Firecracker, Kata and Wasm</title>
      <dc:creator>RuntimeStack</dc:creator>
      <pubDate>Thu, 22 Oct 2026 19:08:26 +0000</pubDate>
      <link>https://dev.to/runtimestack/is-a-docker-container-a-sandbox-ai-agent-sandboxing-explained-seccomp-gvisor-firecracker-kata-5f4b</link>
      <guid>https://dev.to/runtimestack/is-a-docker-container-a-sandbox-ai-agent-sandboxing-explained-seccomp-gvisor-firecracker-kata-5f4b</guid>
      <description>&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/pvh4lbc0Ssc" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;When headlines say an AI agent "escaped its sandbox"... what wall did it pass?&lt;/p&gt;

&lt;p&gt;Some sandboxes are walls. Some are just curtains.&lt;/p&gt;

&lt;p&gt;Containers, seccomp, gVisor, Firecracker, Kata, WebAssembly, V8 isolates. They all sort into one ladder, with one question:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How much of the host kernel can the code reach?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And there's one door none of them close. We'll get there.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbo3up4jg8cz03y3f02xu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbo3up4jg8cz03y3f02xu.png" alt="An apartment building: the agent container behind a dashed curtain, the host and another app behind a wall, all on one shared kernel" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why sandbox at all?
&lt;/h2&gt;

&lt;p&gt;Your agent writes code, then runs it. But who really wrote it?&lt;/p&gt;

&lt;p&gt;Treat agent code like code from a stranger. Because it might be.&lt;/p&gt;

&lt;h2&gt;
  
  
  The building and its plumbing: the kernel
&lt;/h2&gt;

&lt;p&gt;Here's the building. Every program lives in its own apartment.&lt;/p&gt;

&lt;p&gt;Down in the basement is the &lt;strong&gt;kernel&lt;/strong&gt;: the plumbing every apartment shares.&lt;/p&gt;

&lt;p&gt;Open a file, send a packet? You ask the kernel. That request is a &lt;strong&gt;syscall&lt;/strong&gt;. Linux has hundreds of them, and every one is a pipe into the basement.&lt;/p&gt;

&lt;p&gt;One bug in that plumbing, and a tenant can flood the whole building.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwaqnvqnd1bj64vzaqxzu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwaqnvqnd1bj64vzaqxzu.png" alt="Hundreds of syscalls running from every apartment into one shared kernel; one bug floods the whole building" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Containers: a locked room
&lt;/h2&gt;

&lt;p&gt;So what's a container? A locked room in this same building.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Namespaces&lt;/strong&gt; control what you can &lt;em&gt;see&lt;/em&gt;: your own processes, files and network.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;cgroups&lt;/strong&gt; control how much you can &lt;em&gt;use&lt;/em&gt;: CPU, memory, how many processes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On a Mac or Windows, Docker runs a Linux VM, so the shared kernel is that VM's.&lt;/p&gt;

&lt;h2&gt;
  
  
  seccomp: the list of taps
&lt;/h2&gt;

&lt;p&gt;Next layer: &lt;strong&gt;seccomp&lt;/strong&gt;, a list of taps you're allowed to turn.&lt;/p&gt;

&lt;p&gt;It's a tiny filter program that checks each syscall's number and arguments, and answers allow, errno or kill:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;read       allow
write      allow
mount      blocked
ptrace     blocked
kexec_load blocked
openat     allow
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But the kernel's own docs say it plainly: syscall filtering isn't a sandbox.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is a Docker container a sandbox?
&lt;/h2&gt;

&lt;p&gt;Not on its own.&lt;/p&gt;

&lt;p&gt;In January 2024, a bug in &lt;strong&gt;runc&lt;/strong&gt;, the tool that starts containers, let them reach the host's files. That's CVE-2024-21626. A full container escape.&lt;/p&gt;

&lt;p&gt;And that wasn't even the kernel.&lt;/p&gt;

&lt;h3&gt;
  
  
  The classic own-goal: docker.sock
&lt;/h3&gt;

&lt;p&gt;Mounting the Docker socket into the container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-v&lt;/span&gt; /var/run/docker.sock:/var/run/docker.sock agent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Whoever controls that socket controls the Docker daemon, and the daemon runs as root.&lt;/p&gt;

&lt;p&gt;Volumes only, a separate network? Better. But it's still the same kernel.&lt;/p&gt;

&lt;p&gt;So the people saying "if it escaped, it wasn't a sandbox"? They're right. If it shares a kernel, it's a curtain, not a wall.&lt;/p&gt;

&lt;h2&gt;
  
  
  gVisor: hire a concierge
&lt;/h2&gt;

&lt;p&gt;So how do you keep code away from the pipes? Hire a concierge.&lt;/p&gt;

&lt;p&gt;That's &lt;strong&gt;gVisor&lt;/strong&gt;: its &lt;strong&gt;Sentry&lt;/strong&gt; is a kernel, written in Go, running in user space. Your code makes a syscall, and the Sentry answers it itself.&lt;/p&gt;

&lt;p&gt;It only makes a small, filtered set of host calls. Files go through a helper, the &lt;strong&gt;Gofer&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa9tndhgt5653194tooe1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa9tndhgt5653194tooe1.png" alt="gVisor: your code's open() goes to the Sentry in user space instead of the host kernel; files go through the Gofer" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What does the concierge cost? Every syscall takes a detour, so syscall-heavy work slows down. And some programs expect plumbing the Sentry doesn't provide, so compatibility drops.&lt;/p&gt;

&lt;h2&gt;
  
  
  MicroVMs: your own tiny house
&lt;/h2&gt;

&lt;p&gt;Still nervous about sharing a building? Then move out. Get your own tiny house.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;microVM&lt;/strong&gt; runs your code on its own guest kernel, with its own plumbing. &lt;strong&gt;Firecracker&lt;/strong&gt; is the VMM, the virtual machine monitor, built on &lt;strong&gt;KVM&lt;/strong&gt;, the hypervisor inside Linux.&lt;/p&gt;

&lt;p&gt;The house still has one metered service line into the building's basement: KVM. To escape, code has to get past its own kernel, and then break the VMM or KVM too.&lt;/p&gt;

&lt;p&gt;Share a folder in, though, and you've cut a door in that wall.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcxskjwj6e3m0kneifa8f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcxskjwj6e3m0kneifa8f.png" alt="Firecracker microVM: your code on its own guest kernel, with only the VMM and KVM between it and the host kernel" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Isn't a VM slow?
&lt;/h3&gt;

&lt;p&gt;Firecracker's spec says the guest reaches init within 125 ms:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[0.000] InstanceStart
[0.003] guest kernel boots...
[0.125] /sbin/init
/dev/kvm: required
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the VMM itself adds at most about 5 MB of memory. Low-powered machine? The overhead is tiny, if it has KVM. The guest's own memory is the real bill.&lt;/p&gt;

&lt;p&gt;Many sandboxes at once? Fine. They only talk to each other if you wire up a network.&lt;/p&gt;

&lt;p&gt;That's why platforms like E2B describe a sandbox as a Linux VM made on demand. Even Docker's own agent product, Docker Sandboxes, runs each agent in its own microVM.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kata Containers: a container's front door
&lt;/h2&gt;

&lt;p&gt;But your tools speak containers, not VMs. So &lt;strong&gt;Kata&lt;/strong&gt; puts a lightweight VM behind the container interface.&lt;/p&gt;

&lt;p&gt;Same front door as an apartment, but a tiny house behind it. Your container tools never know they're running a VM.&lt;/p&gt;

&lt;h2&gt;
  
  
  WebAssembly and V8 isolates: the glovebox
&lt;/h2&gt;

&lt;p&gt;Now flip it. Picture a sealed glovebox in the lobby. You work through the gloves you're handed, and nothing else.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;WebAssembly&lt;/strong&gt; module's memory is bounds-checked. Step outside it, and the module traps: no pipes to the basement at all.&lt;/p&gt;

&lt;p&gt;Same idea for JavaScript. &lt;strong&gt;V8&lt;/strong&gt; is Chrome's JavaScript engine, and an &lt;strong&gt;isolate&lt;/strong&gt; is one sealed instance of it. It gets its own memory and only the APIs it's handed. Cloudflare Workers run thousands per process. Cloudflare says they start about a hundred times faster than a containerized Node process, using a tenth of the memory.&lt;/p&gt;

&lt;h3&gt;
  
  
  The glovebox catch
&lt;/h3&gt;

&lt;p&gt;So the glovebox is the strongest rung, right? Not so fast.&lt;/p&gt;

&lt;p&gt;Now the wall is the runtime itself, and runtimes have bugs. Break one, and you're a normal host process. In 2024, V8's team said even its newest sandbox wasn't yet a strong boundary.&lt;/p&gt;

&lt;p&gt;Side channels, timing the shared CPU, still work too. Even between tiny houses.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ladder
&lt;/h2&gt;

&lt;p&gt;Sorted by how much host kernel the code can reach:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rung&lt;/th&gt;
&lt;th&gt;Analogy&lt;/th&gt;
&lt;th&gt;What the code reaches&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Container + seccomp&lt;/td&gt;
&lt;td&gt;locked room, list of taps&lt;/td&gt;
&lt;td&gt;the shared kernel, filtered by seccomp&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gVisor&lt;/td&gt;
&lt;td&gt;the concierge&lt;/td&gt;
&lt;td&gt;the Sentry, which makes a small, filtered set of host calls&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Firecracker microVM / Kata&lt;/td&gt;
&lt;td&gt;your own tiny house&lt;/td&gt;
&lt;td&gt;only its VMM and the KVM under it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wasm / V8 isolates&lt;/td&gt;
&lt;td&gt;the glovebox&lt;/td&gt;
&lt;td&gt;nothing but their imports&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8u6wshn8ibs16uqzbrt8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8u6wshn8ibs16uqzbrt8.png" alt="The sandbox ladder: rungs from container to V8 isolate, with what each reaches of the host kernel and what it costs" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And every wall here is software, so any rung can have bugs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which rung should you pick?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Your own agent on your own repo? A container-level sandbox is the floor. An injected command's easy win is your network, not a kernel exploit, and the next rule shuts that.&lt;/li&gt;
&lt;li&gt;Running strangers' code, or anything multi-tenant? Give it its own kernel: gVisor or a microVM.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The door no rung closes: the network
&lt;/h2&gt;

&lt;p&gt;So what if your agent sat in a perfect microVM, holding real API keys, with open internet?&lt;/p&gt;

&lt;p&gt;Without network isolation, one injected instruction and one outbound request, and your keys are gone.&lt;/p&gt;

&lt;p&gt;Every wall held. The code just walked out the front door.&lt;/p&gt;

&lt;h3&gt;
  
  
  Egress: the mailroom
&lt;/h3&gt;

&lt;p&gt;Allowlist destinations: a mailroom that only delivers to addresses on the list.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ALLOWLIST
  pypi.org
  github.com
everything else: DENIED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Can it still leak? If it reads your data and can reach an allowed host, yes. So keep that list as short as the job allows, and lock down DNS too.&lt;/p&gt;

&lt;h3&gt;
  
  
  Secrets: keep the master keys out of the house
&lt;/h3&gt;

&lt;p&gt;The sandbox gets a placeholder, and the mailroom attaches the real credential on the way out:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;inside the sandbox:   Authorization: Bearer PLACEHOLDER
after the mailroom:   Authorization: Bearer sk-live-...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Injected code can read that placeholder all day. Off the box, it's worthless. It can still &lt;em&gt;use&lt;/em&gt; the key through the mailroom, so keep the allowlist short.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F00llnq3hxwhjvw4g39io.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F00llnq3hxwhjvw4g39io.png" alt="Keys stay out: the agent only holds a placeholder; the mailroom injects the real key at egress" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Agent code is untrusted on your laptop too: review it, or run it sandboxed.&lt;/p&gt;

&lt;h2&gt;
  
  
  A real example: Claude Code's sandbox
&lt;/h2&gt;

&lt;p&gt;Does anyone build it this way? Here's one example: Claude Code's sandbox.&lt;/p&gt;

&lt;p&gt;It's off by default. You turn it on with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/sandbox
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On Linux it uses &lt;strong&gt;bubblewrap&lt;/strong&gt;, the same namespaces trick a container uses. On macOS, &lt;strong&gt;Seatbelt&lt;/strong&gt;, Apple's built-in sandbox.&lt;/p&gt;

&lt;p&gt;Commands get no direct route out. A host proxy checks every domain against an allowlist that starts empty. Set up masking, and the proxy swaps a placeholder key for the real one.&lt;/p&gt;

&lt;p&gt;Its own docs say it reduces risk, but isn't a complete isolation boundary. For strangers' code, climb.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recap
&lt;/h2&gt;

&lt;p&gt;One building. The kernel is the shared plumbing. A container is a locked room. seccomp is the list of taps. gVisor is the concierge. A microVM is your own tiny house, and Kata gives it a container's front door. Wasm and isolates are the glovebox. And the mailroom guards the one door no wall closes.&lt;/p&gt;

&lt;p&gt;So did that agent escape its sandbox? Ask two things: was it a wall, or a curtain? And which door was open?&lt;/p&gt;

&lt;p&gt;Which rung is your agent on right now? Tell me in the comments.&lt;/p&gt;

&lt;p&gt;More in this series: &lt;a href="https://youtu.be/Q3wdpWt09UM" rel="noopener noreferrer"&gt;AI agent identity with SPIFFE and SPIRE&lt;/a&gt;, &lt;a href="https://youtu.be/rIzbixCoSEs" rel="noopener noreferrer"&gt;Pi 1.0 and Pi Durable&lt;/a&gt;, &lt;a href="https://youtu.be/IF_1lFy7YM8" rel="noopener noreferrer"&gt;AI reverse engineering&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This is the written version of a 10-minute animated explainer on &lt;a href="https://www.youtube.com/@runtimestack" rel="noopener noreferrer"&gt;RuntimeStack&lt;/a&gt;. Sources: &lt;a href="https://man7.org/linux/man-pages/man2/syscalls.2.html" rel="noopener noreferrer"&gt;syscalls(2)&lt;/a&gt;, &lt;a href="https://man7.org/linux/man-pages/man7/namespaces.7.html" rel="noopener noreferrer"&gt;namespaces(7)&lt;/a&gt;, &lt;a href="https://man7.org/linux/man-pages/man7/cgroups.7.html" rel="noopener noreferrer"&gt;cgroups(7)&lt;/a&gt;, &lt;a href="https://docs.kernel.org/userspace-api/seccomp_filter.html" rel="noopener noreferrer"&gt;seccomp BPF&lt;/a&gt;, &lt;a href="https://access.redhat.com/security/vulnerabilities/RHSB-2024-001" rel="noopener noreferrer"&gt;Red Hat RHSB-2024-001&lt;/a&gt;, &lt;a href="https://nvd.nist.gov/vuln/detail/CVE-2024-21626" rel="noopener noreferrer"&gt;CVE-2024-21626&lt;/a&gt;, &lt;a href="https://dirtypipe.cm4all.com/" rel="noopener noreferrer"&gt;Dirty Pipe&lt;/a&gt;, &lt;a href="https://docs.docker.com/engine/security/" rel="noopener noreferrer"&gt;Docker Engine security&lt;/a&gt;, &lt;a href="https://docs.docker.com/ai/sandboxes/architecture/" rel="noopener noreferrer"&gt;Docker Sandboxes architecture&lt;/a&gt;, &lt;a href="https://docs.docker.com/desktop/features/vmm/" rel="noopener noreferrer"&gt;Docker Desktop VMM&lt;/a&gt;, &lt;a href="https://gvisor.dev/docs/" rel="noopener noreferrer"&gt;gVisor docs&lt;/a&gt;, &lt;a href="https://gvisor.dev/docs/architecture_guide/platforms/" rel="noopener noreferrer"&gt;gVisor platforms&lt;/a&gt;, &lt;a href="https://github.com/firecracker-microvm/firecracker/blob/main/README.md" rel="noopener noreferrer"&gt;Firecracker README&lt;/a&gt;, &lt;a href="https://github.com/firecracker-microvm/firecracker/blob/main/SPECIFICATION.md" rel="noopener noreferrer"&gt;Firecracker specification&lt;/a&gt;, &lt;a href="https://github.com/firecracker-microvm/firecracker/blob/main/docs/getting-started.md" rel="noopener noreferrer"&gt;Firecracker getting started&lt;/a&gt;, &lt;a href="https://www.usenix.org/conference/nsdi20/presentation/agache" rel="noopener noreferrer"&gt;Firecracker (NSDI '20)&lt;/a&gt;, &lt;a href="https://github.com/kata-containers/kata-containers/blob/main/README.md" rel="noopener noreferrer"&gt;Kata Containers&lt;/a&gt;, &lt;a href="https://github.com/kata-containers/kata-containers/blob/main/docs/hypervisors.md" rel="noopener noreferrer"&gt;Kata hypervisors&lt;/a&gt;, &lt;a href="https://webassembly.org/docs/security/" rel="noopener noreferrer"&gt;WebAssembly security&lt;/a&gt;, &lt;a href="https://developers.cloudflare.com/workers/reference/how-workers-works/" rel="noopener noreferrer"&gt;How Workers works&lt;/a&gt;, &lt;a href="https://developers.cloudflare.com/workers/reference/security-model/" rel="noopener noreferrer"&gt;Workers security model&lt;/a&gt;, &lt;a href="https://v8.dev/blog/sandbox" rel="noopener noreferrer"&gt;The V8 Sandbox&lt;/a&gt;, &lt;a href="https://code.claude.com/docs/en/sandboxing" rel="noopener noreferrer"&gt;Claude Code sandboxing&lt;/a&gt;, &lt;a href="https://docs.e2b.dev/" rel="noopener noreferrer"&gt;E2B docs&lt;/a&gt;, &lt;a href="https://genai.owasp.org/llmrisk/llm01-prompt-injection/" rel="noopener noreferrer"&gt;OWASP LLM01&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>docker</category>
      <category>devops</category>
    </item>
    <item>
      <title>You've Never Actually Sent a JWT (JOSE, JWS, JWE, JWA and JWK Explained)</title>
      <dc:creator>RuntimeStack</dc:creator>
      <pubDate>Thu, 22 Oct 2026 17:51:39 +0000</pubDate>
      <link>https://dev.to/runtimestack/youve-never-actually-sent-a-jwt-jose-jws-jwe-jwa-and-jwk-explained-enn</link>
      <guid>https://dev.to/runtimestack/youve-never-actually-sent-a-jwt-jose-jws-jwe-jwa-and-jwk-explained-enn</guid>
      <description>&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/Kl3LZRcJsbM" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;You've used JWTs a hundred times. But here's the thing: you've probably never actually sent one.&lt;/p&gt;

&lt;p&gt;That token in your &lt;code&gt;Authorization&lt;/code&gt; header? Technically, it's a &lt;strong&gt;JWS&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;JOSE, JWS, JWE, JWA, JWK, JWT. It's alphabet soup. Here's one analogy that makes all of it click.&lt;/p&gt;

&lt;h2&gt;
  
  
  JOSE: the shipping company
&lt;/h2&gt;

&lt;p&gt;Start with &lt;strong&gt;JOSE&lt;/strong&gt;: JSON Object Signing and Encryption. It's not a token. It's a family of IETF standards.&lt;/p&gt;

&lt;p&gt;Think of it as a shipping company, with rules for how packages get &lt;strong&gt;sealed&lt;/strong&gt;, &lt;strong&gt;locked&lt;/strong&gt; and &lt;strong&gt;delivered&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  JWT: the letter
&lt;/h2&gt;

&lt;p&gt;A JWT isn't a container at all. It's the letter inside: a set of claims.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"iss"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"auth.example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sub"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user_42"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"aud"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"orders-api"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"exp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1767225600&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Who issued it. Who it's about. Who it's for. And when it expires.&lt;/p&gt;

&lt;p&gt;But a letter on its own isn't secure. It needs packaging.&lt;/p&gt;

&lt;h2&gt;
  
  
  JWS: the sealed envelope
&lt;/h2&gt;

&lt;p&gt;Option one: &lt;strong&gt;JWS&lt;/strong&gt;, JSON Web Signature. Picture a clear envelope with a tamper-proof wax seal.&lt;/p&gt;

&lt;p&gt;It has three parts, separated by dots: a header that names the algorithm, the payload, and the signature.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;eyJhbGciOiJ...  .  eyJzdWIiOiI...  .  SflKxwRJSMe...
    header            payload           signature
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The seal proves who sent it, and that nobody changed a single byte.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2hq0duqhiesh8xx130z5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2hq0duqhiesh8xx130z5.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The gotcha: the envelope is clear
&lt;/h3&gt;

&lt;p&gt;The payload is just Base64URL encoded. Anyone can decode it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ=='&lt;/span&gt; | &lt;span class="nb"&gt;base64&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;   &lt;span class="c"&gt;# Base64URL drops the = padding; add it back for base64&lt;/span&gt;
&lt;span class="c"&gt;# {"sub":"1234567890","name":"John Doe","iat":1516239022}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So &lt;strong&gt;never put secrets inside a signed JWT&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fngmam1jwa4zb0ppqou35.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fngmam1jwa4zb0ppqou35.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  JWE: the locked box
&lt;/h2&gt;

&lt;p&gt;Need privacy? Option two: &lt;strong&gt;JWE&lt;/strong&gt;, JSON Web Encryption. This is a locked metal box, with five parts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A header&lt;/li&gt;
&lt;li&gt;An encrypted key&lt;/li&gt;
&lt;li&gt;An initialization vector&lt;/li&gt;
&lt;li&gt;The ciphertext&lt;/li&gt;
&lt;li&gt;An authentication tag&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Only the recipient, holding the right key, can open it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpgxrn2dh7mn1nvoj32z9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpgxrn2dh7mn1nvoj32z9.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  JWA: the toolbox
&lt;/h2&gt;

&lt;p&gt;So how do you seal or lock it? That's &lt;strong&gt;JWA&lt;/strong&gt;, JSON Web Algorithms. The toolbox.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Signing:&lt;/strong&gt; HS256, RS256 and ES256&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Encryption:&lt;/strong&gt; RSA-OAEP and A256GCM&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;alg&lt;/code&gt; field in the header tells the receiver which tool was used.&lt;/p&gt;

&lt;h2&gt;
  
  
  JWK and JWKS: the keys
&lt;/h2&gt;

&lt;p&gt;And the keys themselves? That's &lt;strong&gt;JWK&lt;/strong&gt;, JSON Web Key. Instead of PEM files, a key is just JSON: a key type (&lt;code&gt;kty&lt;/code&gt;), what it's used for (&lt;code&gt;use&lt;/code&gt;), a key ID called &lt;code&gt;kid&lt;/code&gt;, and the key material.&lt;/p&gt;

&lt;p&gt;Put several keys in an array, and you get a &lt;strong&gt;JWKS&lt;/strong&gt;. Auth servers publish it at &lt;code&gt;/.well-known/jwks.json&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Your API reads the &lt;code&gt;kid&lt;/code&gt; from the token header, fetches the matching public key, and verifies the signature. That's how keys rotate without redeploying anything.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4q8o6y0dh99l0o44965k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4q8o6y0dh99l0o44965k.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Nested JWT: both at once
&lt;/h2&gt;

&lt;p&gt;And yes, you can combine them. Sign a JWT, then encrypt the result. That's a nested JWT: a sealed envelope, inside a locked box.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recap
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Analogy&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;JOSE&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;the shipping company&lt;/td&gt;
&lt;td&gt;the family of standards&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;JWT&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;the letter&lt;/td&gt;
&lt;td&gt;the claims&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;JWS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;the sealed envelope&lt;/td&gt;
&lt;td&gt;signed, readable by anyone&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;JWE&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;the locked box&lt;/td&gt;
&lt;td&gt;encrypted, private&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;JWA&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;the toolbox&lt;/td&gt;
&lt;td&gt;the algorithms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;JWK&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;the keys&lt;/td&gt;
&lt;td&gt;keys as JSON&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzdzz23wb6wt0ohziqzbe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzdzz23wb6wt0ohziqzbe.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Next time someone says JWT, you can say: well, actually... it's a JWS.&lt;/p&gt;

&lt;p&gt;Which of these acronyms confused you the most? Tell me in the comments.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This is the written version of a 3-minute animated explainer on &lt;a href="https://www.youtube.com/@runtimestack" rel="noopener noreferrer"&gt;RuntimeStack&lt;/a&gt;. Sources: &lt;a href="https://www.rfc-editor.org/rfc/rfc7519" rel="noopener noreferrer"&gt;RFC 7519 (JWT)&lt;/a&gt;, &lt;a href="https://www.rfc-editor.org/rfc/rfc7515" rel="noopener noreferrer"&gt;RFC 7515 (JWS)&lt;/a&gt;, &lt;a href="https://www.rfc-editor.org/rfc/rfc7516" rel="noopener noreferrer"&gt;RFC 7516 (JWE)&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>jwt</category>
      <category>security</category>
      <category>authentication</category>
    </item>
  </channel>
</rss>
