WSL Ubuntu Sandbox Guide

Technology: sandbox · Category: tooling · Last reviewed: 2026-08-23

Source: https://tech-stack.codeamanilabs.org/guide/sandbox

Insight:

A WSL sandbox is three commands used as a discipline: wsl --install --name to stand up a second Ubuntu, wsl --export/--import as snapshot-and-restore, and wsl --unregister to nuke it — so an experiment you can't trust never touches the distro holding your keys and your ~/code. The trade-off has to be stated honestly: WSL 2 distros are containers inside one shared utility VM, so you get separate mount/PID/user/cgroup namespaces but a shared kernel and a shared network namespace, and .wslconfig caps the whole VM, not one distro. For codeAmani it's the right home for risky installs, security-lab tooling, and customer-bug repros — with docker run --rm --network none as the tighter, lighter tier when a single process is all you need.

███████╗ █████╗ ███╗   ██╗██████╗ ██████╗  ██████╗ ██╗  ██╗
██╔════╝██╔══██╗████╗  ██║██╔══██╗██╔══██╗██╔═══██╗╚██╗██╔╝
███████╗███████║██╔██╗ ██║██║  ██║██████╔╝██║   ██║ ╚███╔╝
╚════██║██╔══██║██║╚██╗██║██║  ██║██╔══██╗██║   ██║ ██╔██╗
███████║██║  ██║██║ ╚████║██████╔╝██████╔╝╚██████╔╝██╔╝ ██╗
╚══════╝╚═╝  ╚═╝╚═╝  ╚═══╝╚═════╝ ╚═════╝  ╚═════╝ ╚═╝  ╚═╝

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.

flowchart TB
  H["Windows 11 host<br/>your files, creds, domain join"]
  H -->|"Hyper-V boundary — the hard wall"| VM
  VM["WSL 2 utility VM<br/>one kernel · one network namespace · one .wslconfig"]
  VM --> DEV["Ubuntu · dev distro<br/>~/code · SSH keys · .env.local"]
  VM --> LAB["Ubuntu-lab · sandbox distro<br/>own mount / PID / user / cgroup ns"]
  LAB --> SNAP["wsl --export lab<br/>lab-clean.tar.gz"]
  SNAP --> REST["wsl --import lab-2<br/>rebuild from clean"]
  LAB --> NUKE["wsl --unregister lab<br/>gone in one second"]
  LAB --> DKR["docker run --rm --network none<br/>one-process sandbox inside the sandbox"]

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 lab

From 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.zst

Restore 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.vhdx

The 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 disk

Or set it inside the distro before you snapshot, in /etc/wsl.conf:

[user]
default=amani

Containing 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
EOF

Then, from Windows:

wsl --terminate lab   # settings only apply after the distro fully stops

What 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 automount matters: when a Linux binary in WSL touches a Windows file, it does so with the permissions of the Windows user who ran wsl.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:

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 delete

Notes that will save you an hour:

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 proxy

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 bash

A 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 --verbose

And the one-way door, spelled out because it deserves it:

wsl --unregister lab

--unregister deletes the distro's root filesystem. There is no recycle bin, no undo, and no prompt. Type the distro name carefully — wsl --unregister Ubuntu and wsl --unregister lab are 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 --verbose

Two more scripting notes:


codeAmani notes

A sandbox is where risky things go, not where secrets go.

Official docs: