WSL Ubuntu Sandbox Guide
What is a WSL sandbox?
Container-grade isolation with VM-grade convenience — and you have to know which one you actually got.
Microsoft's own architecture note is the line to internalise: WSL 2 distros share the network namespace, device tree, CPU/kernel/memory/swap and the /init binary, and only get their own PID, mount, user and cgroup namespaces. So a second distro is a container inside one shared utility VM, not a second VM — a kernel exploit crosses the distro boundary, port :3000 collides with your dev distro, and /mnt/wsl is a tmpfs every distro can see. The hard wall is Hyper-V, between the VM and Windows. .wslconfig is VM-global, so there is no per-distro RAM cap; cap the workload with systemd-run --scope -p MemoryMax=2G, or drop to docker run --rm --network none --memory 2g, which is the only tier that actually gives you a private network namespace. The default that bites hardest is automount: a Linux binary touching /mnt/c does so with the permissions of the Windows user who ran wsl.exe, so an unhardened sandbox can read every .env.local and SSH key in your profile. For codeAmani this is where the security guide's isolated-lab requirement lands, where an unfamiliar dependency gets its install script watched with the network off before it enters the libraries catalog, and where a customer repro gets built from a known-clean base instead of from whatever your laptop happens to be today.
Six moves that make a sandbox
One wsl.exe binary, no SDK — the discipline is in which verb you reach for and in what order.
███████╗ █████╗ ███╗ ██╗██████╗ ██████╗ ██████╗ ██╗ ██╗
██╔════╝██╔══██╗████╗ ██║██╔══██╗██╔══██╗██╔═══██╗╚██╗██╔╝
███████╗███████║██╔██╗ ██║██║ ██║██████╔╝██║ ██║ ╚███╔╝
╚════██║██╔══██║██║╚██╗██║██║ ██║██╔══██╗██║ ██║ ██╔██╗
███████║██║ ██║██║ ╚████║██████╔╝██████╔╝╚██████╔╝██╔╝ ██╗
╚══════╝╚═╝ ╚═╝╚═╝ ╚═══╝╚═════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝WSL Ubuntu Sandbox Guide
Focus: Standing up a disposable, isolated Ubuntu on Windows for experiments — spin-up, snapshot, blast-radius containment, and a one-command nuke — so a risky install or a customer repro never lands in your real dev distro.
Overview
The wsl/ guide covers WSL as your primary dev environment. This one covers the opposite posture: a distro you expect to break. The whole workflow is four verbs on the same wsl.exe binary you already have — no extra tooling, no VM images to download.
| Verb | Command | Cost |
|---|---|---|
| Spin up | wsl --install Ubuntu-24.04 --name lab | ~1 min, one download |
| Snapshot | wsl --export lab lab-clean.tar.gz --format tar.gz | seconds–minutes, one file |
| Restore | wsl --import lab-2 D:\wsl\lab-2 lab-clean.tar.gz | seconds |
| Nuke | wsl --unregister lab | instant, unrecoverable |
The important thing to internalise before you trust it with anything dangerous: all WSL 2 distros run inside one shared lightweight utility VM. Per Microsoft's own architecture note, distros "share the same network namespace, device tree (other than /dev/pts), CPU/Kernel/Memory/Swap, /init binary, but have their own PID namespace, Mount namespace, User namespace, Cgroup namespace, and init process." The hard wall is the Hyper-V boundary between the VM and Windows — not the boundary between two distros.
Which tier do you actually need?
| Tier | What you get | Reset cost | Reach for it when |
|---|---|---|---|
| Second WSL distro | Own ext4 VHDX + mount/PID/user/cgroup namespaces | wsl --unregister — instant | A risky curl | bash, a whole toolchain, a repro that needs systemd |
| Snapshot + import | The above, plus a known-good starting point | Re-import a tarball — under a minute | You need to run the same experiment five times from a clean base |
| Docker container | Everything a distro gets plus its own network namespace and hard cgroup caps | Automatic on --rm | One process, one command, no persistent state |
| Full Hyper-V VM | Separate kernel, separate network stack, checkpoints | Restore checkpoint — minutes | Detonating actual malware, or testing kernel modules |
Official Documentation
| Resource | URL |
|---|---|
| Basic WSL commands (export / import / unregister / manage) | https://learn.microsoft.com/windows/wsl/basic-commands |
Advanced settings — .wslconfig and wsl.conf | https://learn.microsoft.com/windows/wsl/wsl-config |
| WSL 2 architecture (what's shared, what isn't) | https://learn.microsoft.com/windows/wsl/about |
| Enterprise setup — images, firewall, file-access model | https://learn.microsoft.com/windows/wsl/enterprise |
| Import a custom distro from a tarball | https://learn.microsoft.com/windows/wsl/use-custom-distro |
| Manage disk space / sparse VHDX | https://learn.microsoft.com/windows/wsl/disk-space |
| Hyper-V Firewall (filtering WSL traffic) | https://learn.microsoft.com/windows/security/operating-system-security/network-security/windows-firewall/hyper-v-firewall |
| WSL releases (check what your build supports) | https://github.com/microsoft/WSL/releases |
docker run reference | https://docs.docker.com/reference/cli/docker/container/run/ |
Docker none network driver | https://docs.docker.com/engine/network/drivers/none/ |
Quickstart — a throwaway Ubuntu in 60 seconds
# Stand up a SECOND Ubuntu named "lab" — --name is what makes a second
# copy of an already-installed distro possible.
wsl.exe --install Ubuntu-24.04 --name lab --no-launch
# Enter it
wsl.exe -d lab
# ...break things...
# Nuke it. Instant. Unrecoverable. That's the point.
wsl.exe --unregister labFrom PowerShell, drop the .exe. Everything below uses wsl (PowerShell form) except where a Linux-side command is being run.
Confirm what's installed and which VM version each distro is on:
wsl.exe --list --verbose
wsl.exe --version # verify your WSL build supports the flags below
wsl.exe --list --online # real, current distro names for --install--list --online today includes Ubuntu-26.04, Ubuntu-24.04, Ubuntu-22.04, Debian, kali-linux, archlinux, FedoraLinux-44, and the AlmaLinux / openSUSE / SUSE / Oracle families. Use the NAME column verbatim.
Placing and sizing the sandbox
Put the sandbox VHDX somewhere you don't mind filling up, and cap it:
wsl --install Ubuntu-24.04 `
--name lab `
--location D:\wsl\lab `
--vhd-size 40GB `
--no-launch| Install flag | Effect |
|---|---|
--name <Name> | Registers under a custom name — the key to running N copies of one distro |
--location <Path> | Where the ext4.vhdx lives (default is under %LocalAppData%\wsl) |
--vhd-size <Size> | Caps the virtual disk (default defaultVhdSize is 1 TB) |
--no-launch, -n | Register without launching — you configure wsl.conf before first boot |
--fixed-vhd | Fixed-size rather than dynamically expanding disk |
--from-file <Path> | Install from a local distro file instead of the store |
--version <1|2> | Force WSL 1 or WSL 2 for this distro |
Snapshot and restore
This is the part that turns a second distro into a sandbox. Get the environment to a known-good state, export it, and from then on every experiment starts from that file.
Take the golden snapshot
# Terminate first so the filesystem is quiescent
wsl --terminate lab
# tar is the portable format; tar.gz / tar.xz trade CPU for size
wsl --export lab D:\wsl\snapshots\lab-clean.tar.gz --format tar.gz--format accepts tar, tar.gz, tar.xz, and vhd. The older --vhd switch is still accepted and is equivalent to --format vhd. A .vhdx export restores fastest (no untar) but is far larger and only meaningful for WSL 2.
<FileName> can be - for stdout (and - for stdin on import), so an export can be streamed into another tool. Do this from cmd or a POSIX shell, not PowerShell — PowerShell's object pipeline mangles binary streams:
# Git Bash / WSL — stream a snapshot straight to a compressor or a remote host
WSL_UTF8=1 wsl.exe --export lab - | zstd -T0 -19 -o /d/wsl/snapshots/lab-clean.tar.zstRestore into a fresh distro
wsl --import lab-2 D:\wsl\lab-2 D:\wsl\snapshots\lab-clean.tar.gz --version 2
wsl -d lab-2| Import flag | Effect |
|---|---|
--version <1|2> | WSL version for the new distro |
--vhd | The source file is a .vhdx, not a tar — WSL copies it to <InstallLocation> |
For a .vhdx you want to attach without copying (fast, but the file becomes the live disk):
wsl --import-in-place lab-3 D:\wsl\images\lab.vhdxThe VHD must be formatted ext4.
The reset loop
# One experiment, start to finish
wsl --unregister lab
wsl --import lab D:\wsl\lab D:\wsl\snapshots\lab-clean.tar.gz --version 2
wsl -d lab -- bash -lc './experiment.sh'Because --unregister deletes the root filesystem outright, this loop is genuinely a few seconds. Treat lab-clean.tar.gz as immutable and never export over it from a dirty distro.
Post-import housekeeping
An imported distro has no launcher executable, which breaks the usual ubuntu config --default-user trick. Use wsl --manage instead:
wsl --manage lab --set-default-user amani # imported distros boot as root otherwise
wsl --manage lab --set-sparse true # auto-reclaim freed disk space
wsl --manage lab --resize 60GB # grow (or shrink) the VHDX
wsl --manage lab --move E:\wsl\lab # relocate the distro's diskOr set it inside the distro before you snapshot, in /etc/wsl.conf:
[user]
default=amaniContaining the blast radius
A fresh distro is not contained by default: it automounts your Windows drives and can launch Windows binaries as you. Harden it in /etc/wsl.conf before you take the golden snapshot.
# Inside the sandbox distro
sudo tee /etc/wsl.conf >/dev/null <<'EOF'
# Do not mount C:\ (and friends) into the sandbox at all.
[automount]
enabled = false
mountFsTab = false
# Do not let Linux processes launch Windows binaries, and keep the
# Windows PATH out of $PATH.
[interop]
enabled = false
appendWindowsPath = false
[network]
hostname = lab
generateResolvConf = true
[user]
default = amani
[boot]
systemd = true
EOFThen, from Windows:
wsl --terminate lab # settings only apply after the distro fully stopsWhat each switch buys you:
| Setting | Without it | With it |
|---|---|---|
[automount] enabled=false | /mnt/c exposes your entire Windows profile — SSH keys, .env.local, browser profiles — writable as your Windows user | The sandbox cannot see Windows files at all |
[interop] enabled=false | A script in the sandbox can run powershell.exe, explorer.exe, or any Windows binary as you | Windows process launch is blocked |
[interop] appendWindowsPath=false | Windows PATH entries leak into $PATH; a typo can silently invoke a Windows tool | Clean Linux-only PATH |
[boot] systemd=true | No service manager — many repros won't reproduce | Real systemctl, matching production |
Microsoft's enterprise guidance is explicit about why
automountmatters: when a Linux binary in WSL touches a Windows file, it does so with the permissions of the Windows user who ranwsl.exe. Root inside the sandbox is not root on Windows — but it is you on Windows, for every file you can reach.
What you still don't get
Be honest with yourself about the boundary, especially before running anything you'd call malware:
- Shared kernel. Every distro runs on the one
microsoft-standard-WSL2kernel. A kernel-level exploit escapes the distro boundary into the VM. It does not escape the Hyper-V boundary into Windows — that's the wall that counts. - Shared network namespace. Distros share it. A listener bound in the sandbox collides with your dev distro's
:3000, and a process in the sandbox can reach services your dev distro is serving on localhost. - Shared
/mnt/wsl. A tmpfs visible from every running distro. Anything written there is cross-distro. .wslconfigis VM-global. There is no per-distro memory or CPU cap.memory=8GBcaps the entire VM shared by dev and sandbox.wsl --debug-shellopens a root shell in the utility VM itself. It's a diagnostic tool; it's also a reminder of where the real boundary sits.- Docker Desktop registers its own
docker-desktopdistro in the same VM.
For anything genuinely hostile: a full Hyper-V VM (or Windows Sandbox, or a disposable cloud box) — not a WSL distro.
Resource limits with .wslconfig
.wslconfig lives on the Windows side at %UserProfile%\.wslconfig and governs the whole WSL 2 VM. Keeping a runaway make -j$(nproc) in the sandbox from freezing Windows is exactly what it's for.
# %UserProfile%\.wslconfig — applies to the VM shared by ALL WSL 2 distros
[wsl2]
memory=8GB # default: 50% of host RAM
processors=4 # default: all logical processors
swap=4GB # default: 25% of memory, rounded up to the nearest GB
swapFile=D:\\wsl\\swap.vhdx
defaultVhdSize=64GB # cap new distro disks (default 1 TB)
vmIdleTimeout=60000 # ms of idle before the VM shuts down (Windows 11)
nestedVirtualization=true
localhostForwarding=true
[experimental]
autoMemoryReclaim=gradual # reclaim cache slowly instead of dropCache
sparseVhd=true # every new VHDX is sparse — disk comes back on deleteNotes that will save you an hour:
- Paths need escaped backslashes (
D:\\wsl\\swap.vhdx). Sizes takeGB/MBsuffixes; bare numbers are bytes. - Nothing applies until the VM restarts. Run
wsl --shutdown— a--terminateof one distro is not enough. Give it ~8 seconds before relaunching. - A malformed file is silently ignored. WSL boots with defaults and tells you nothing. Verify with
free -handnprocinside a distro after the shutdown. - Windows 11 ships a WSL Settings GUI app that edits this file; it's the recommended path over hand-editing.
Per-experiment limits
Since .wslconfig can't cap a single distro, cap the workload instead — systemd-run inside the sandbox:
# Needs [boot] systemd=true in the sandbox's wsl.conf
sudo systemd-run --scope \
-p MemoryMax=2G -p CPUQuota=200% -p TasksMax=512 \
./experiment.sh(--user scopes work too, but only where systemd has delegated the memory and
pids controllers to the user slice — sudo is the version that always applies.)
...or run the experiment in a container, which is the next section.
Networking isolation
WSL's network knobs are all in [wsl2], and all global:
[wsl2]
networkingMode=mirrored # nat (default) | mirrored | virtioproxy | none
firewall=true # Windows Firewall + Hyper-V rules filter WSL traffic (default)
dnsTunneling=true # DNS via virtualization rather than packets — VPN-friendly
autoProxy=true # inherit the Windows HTTP proxynetworkingMode=nonedisconnects WSL networking entirely — a genuine air-gap, but for every distro at once. Practical as a deliberate "offline experiment" mode: set it,wsl --shutdown, run the experiment, revert.firewall=true(the default since WSL 2.0.9 on Windows 11 22H2+) means your Windows Firewall rules already apply to WSL. Per-distro rules are possible via Hyper-V Firewall.- For per-sandbox network isolation that doesn't disturb your dev distro, don't fight
.wslconfig— use a container with--network none, oriptables/nftablesinside the sandbox distro itself.
Docker containers — the lighter sandbox
When the experiment is one process rather than a whole environment, a container is faster, tighter, and self-cleaning. Run this from inside your normal distro (or Docker Desktop's WSL integration):
# Ephemeral, network-isolated Ubuntu shell. Gone the moment you exit.
docker run --rm -it --network none ubuntu:24.04 bashA hardened version for running something you actively distrust:
docker run --rm -it \
--network none \
--memory 2g --cpus 2 --pids-limit 512 \
--cap-drop ALL \
--security-opt no-new-privileges \
--read-only --tmpfs /tmp:rw,noexec,nosuid,size=256m \
--user 1000:1000 \
-v "$PWD/artifacts:/out:rw" \
-w /out \
ubuntu:24.04 bash| Flag | Why it's there |
|---|---|
--rm | Container and its writable layer are removed on exit — no cleanup discipline required |
--network none | The container gets a loopback-only namespace. This is the isolation a second WSL distro can't give you. |
--memory / --cpus / --pids-limit | Real cgroup caps — a fork bomb hits the limit, not your laptop |
--cap-drop ALL + --security-opt no-new-privileges | No CAP_*, no setuid escalation |
--read-only + --tmpfs /tmp | Immutable root; scratch space that can't execute |
--user 1000:1000 | Not root. Without user-namespace remapping, container root maps to host root on the shared kernel |
-v ...:/out | One narrow, explicit path for results to escape through |
Container vs. distro, decided in one line: if you need systemctl, a persistent home directory, or a multi-day environment, use a distro. If you need "run this and forget it", use a container.
The nuke-and-rebuild script
Keep this next to the golden snapshot. It's the whole workflow in one file.
# D:\wsl\reset-lab.ps1
param(
# No default for $Name: --unregister has no undo, so the caller must say it out loud.
[Parameter(Mandatory)][string]$Name,
[string]$Root = "D:\wsl\lab",
[string]$Snapshot = "D:\wsl\snapshots\lab-clean.tar.gz"
)
$env:WSL_UTF8 = 1 # otherwise wsl.exe output is UTF-16 and every match fails
if (-not (Test-Path $Snapshot)) { throw "No snapshot at $Snapshot" }
if ($Name -eq "Ubuntu") { throw "Refusing to nuke the default distro" }
# Only tear down if it is actually registered.
if ((wsl --list --quiet) -contains $Name) {
wsl --terminate $Name
wsl --unregister $Name
if ($LASTEXITCODE -ne 0) { throw "unregister failed for $Name" }
}
New-Item -ItemType Directory -Force -Path $Root | Out-Null
wsl --import $Name $Root $Snapshot --version 2
if ($LASTEXITCODE -ne 0) { throw "import failed from $Snapshot" }
wsl --manage $Name --set-sparse true
Write-Host "Sandbox '$Name' rebuilt from $Snapshot"
wsl --list --verboseAnd the one-way door, spelled out because it deserves it:
wsl --unregister lab
--unregisterdeletes the distro's root filesystem. There is no recycle bin, no undo, and no prompt. Type the distro name carefully —wsl --unregister Ubuntuandwsl --unregister labare one character apart in muscle memory.
Scripting wsl.exe from agents and CI
wsl.exe emits UTF-16LE with no BOM by default, which turns into interleaved NUL bytes in any pipeline that assumes UTF-8 — the classic "why is my output U b u n t u". Set WSL_UTF8=1:
WSL_UTF8=1 wsl.exe --list --verbose$env:WSL_UTF8 = 1
wsl --list --verboseTwo more scripting notes:
- From Git Bash / MSYS, Windows paths in
wsl.exearguments get path-mangled. Prefix withMSYS_NO_PATHCONV=1, or run those commands from PowerShell. wsl.exereturns-1(255) on failure with anWsl/Service/...error code on stderr. Check the exit code; don't parse the message.
codeAmani notes
A sandbox is where risky things go, not where secrets go.
- Never seed a sandbox distro with real credentials. Hazina bindings,
.env.local,~/.ssh,~/.aws, and cloud CLI tokens stay in the dev distro. With[automount] enabled=falseand[interop] enabled=falsein the sandbox'swsl.conf, they're not reachable from it — which is the entire point. If an experiment needs a credential, mint a scoped test key for it and revoke it when the distro is unregistered. - Pairs with the
securityguide's isolated-lab requirement. Scanners, exploit PoCs, dependency-confusion checks, andgitleaksruns against untrusted repos belong here, one snapshot-restore per run so results are never contaminated by the previous one. - Customer-bug repros. Import the golden snapshot, install exactly the reported versions, reproduce, capture the diff, unregister. No "it works on my machine" because the machine was clean an hour ago.
- Vetting a new dependency before it enters the catalog.
libraries/build-vs-buy calls often hinge on running an unfamiliar package's install script. Run it inlabwith the network off (--network nonecontainer, ornetworkingMode=nonefor the run) and watch what it tries to do. - Supply-chain posture. The sandbox is where you observe; it doesn't substitute for the real controls. SLSA provenance,
npm audit signatures, dependency review, and secret scanning still gate anything codeAmani ships (seesupply-chain/CLAUDE_CODE_INTEGRATION.md). - Low-bandwidth reality for the Kenya-targeted builds. Snapshot-and-restore is the bandwidth win here: one
Ubuntu-24.04download, then every rebuild is a local tarball. Export astar.xzif a teammate on a metered connection has to pull the golden image; keep--format tarfor local speed. Testing a duka-order-bot or boda-dispatch webhook path against a throttled interface is also easiest in a sandbox —tc qdiscthere can't wreck your working network stack. - Don't run production-adjacent work in a sandbox you plan to nuke. Anything worth keeping goes back to git before
--unregister. The sandbox has no backup, by design.
Related guides
wsl/CLAUDE_CODE_INTEGRATION.md— WSL as your primary dev environment: filesystem performance,/mnt/crules, Claude Code inside WSL, networking modes, systemd. Read that one first; this guide assumes it.security/CLAUDE_CODE_INTEGRATION.md— the isolated-lab requirement this guide satisfies.supply-chain/CLAUDE_CODE_INTEGRATION.md— SLSA provenance for anything that leaves the lab.