Launch microsandbox microVMs from a
mise mise.toml: Docker inside, tools installed with
mise bootstrap, environment and secrets resolved on the host, and an egress
broker that asks before the sandbox talks to anything new.
$ microkitchen
✓ Created mk-api-1a2b3c4d (41.2s)
✓ Started Docker (27.3s)
running mise bootstrap (log: ~/.microkitchen/logs/mk-api-1a2b3c4d/bootstrap.log)
root@mk-api-1a2b3c4d:~#Status: pre-release (0.1.0). Developed and tested on Linux with KVM. The macOS dialog backend is implemented but untested.
- Requirements
- Install
- Quick start
- Commands
- Configuration
- The sandbox user: chef
- Environment variables and secrets
- Dotfiles and system files
- Network access
- Changing a sandbox:
remodel - Settings
- Files
- mise caveats
- Development
- Linux on x86_64 with KVM (
/dev/kvm), or macOS on Apple Silicon (untested). - microsandbox 0.7.2 (the
msbCLI and its runtime; the version must match the SDK microkitchen is built with). - mise on
PATH, used to discover the configuration and resolve environment variables on the host. - For approval dialogs:
zenityorkdialogon Linux (with a display), or macOS's built-inosascript.notify-sendis used for notifications when present. Without a dialog, approvals fall back to the command line (see Headless use). - Rust (edition 2024) to build —
mise installin a clone sets up the pinned toolchain — plus a C toolchain andlibcap-ng's development headers (libcap-ng-devon Debian/Ubuntu,libcap-ng-develon Fedora), needed to link microsandbox's krun-based VMM backend.
# microsandbox, pinned to the version microkitchen is built against
curl -fsSL -o install.sh \
https://github.com/superradcompany/microsandbox/releases/download/v0.7.2/install.sh
sed -i 's/^ get_latest_version$/ VERSION=v0.7.2/' install.sh && sh install.sh
# microkitchen
mise run install # or: cargo install --locked --path crates/microkitchenThe microsandbox installer otherwise installs the latest release; the sed
pins it (the CI workflow does the same). On macOS, write sed -i '' instead of
sed -i.
Add a [_.microkitchen] table to a project's mise.toml:
[tools]
node = "22"
[env]
GITHUB_TOKEN = { required = true }
[_.microkitchen]
cpus = 4
memory = "8G"
mounts = ["./:/work"]
[_.microkitchen.network]
allow = ["registry.npmjs.org", "*.github.com"]
[_.microkitchen.secrets.GITHUB_TOKEN]
allow = ["github.com", "*.github.com"]Then run microkitchen in the project. It:
- finds the kitchen file the way mise finds its configuration,
- validates it and resolves
[env]on the host with mise, - creates the sandbox (or starts the existing one) from
cruizba/ubuntu-dind:noble-latest, with Docker running inside, - runs
mise bootstraponce, with the network open so tools can install, - from then on mediates the sandbox's network access, and
- attaches a shell as the sandbox user, chef.
Run it again to get back into the same sandbox; microkitchen down removes it.
| Command | What it does |
|---|---|
microkitchen / up [--no-shell] [--recreate] |
Create or start the sandbox, bootstrap it, attach a shell. --recreate replaces it (only the mise cache is kept). |
shell [--root], exec [--root] -- <cmd> |
Attach a shell / run a command in the sandbox, as chef or, with --root, as root. |
stop, start, restart |
Lifecycle. Use microkitchen start, not msb start (see Limitations). |
down [--purge] |
Remove the sandbox; --purge also deletes its state and logs (never the shared mise cache). |
status, list (ls) |
This project's sandbox; all sandboxes microkitchen created. |
logs [--bootstrap | --broker | --sandbox] |
The bootstrap log (default), the broker's log, or the sandbox's own output. |
bootstrap |
Run mise bootstrap again, e.g. after a failed install (remodel runs it after tool changes). |
validate |
Check the configuration and resolve the environment without creating anything. |
remodel [--yes] [--recreate] |
Apply configuration changes to the existing sandbox (details). |
net … |
Network rules and approvals (details). |
broker start | stop | status |
The egress broker daemon (started automatically when needed). |
Global options: -C <dir> (run as if in <dir>), --home <dir> (state
directory, default ~/.microkitchen, or MICROKITCHEN_HOME), -v/-q, and
--json for machine-readable output where supported.
Pulling the image, creating, starting, stopping and removing the sandbox, and
non-interactive exec, show a progress spinner on stderr when it is a
terminal (with per-layer bars while the image is pulled). Finished steps leave
a ✓ line; exec leaves only the command's own output.
microkitchen reads the kitchen file: the highest-precedence file among those
mise loads that contains a [_.microkitchen] table (falling back to the nearest
mise.toml). Approval answers and net allow|deny are written to it.
[_.microkitchen]
cpus = 2 # 1–64 default 2
memory = "4G" # up to 64G default "4G"
disk = "10G" # root disk (flat ext4) default "10G"
mounts = ["./src:/app", "./data:/data:ro"] # host:guest[:ro]
dotfiles = "~/.dotfiles" # staged as mise's dotfiles.root default none
[_.microkitchen.network]
network = "public" # none | public | open default "public"
allow = ["example.com", "*.microsandbox.dev", "203.0.113.7"]
deny = ["potentiallymalicious.com"]
ports = ["8000:8000", "9100:9100/udp"] # host:guest[/udp]
[_.microkitchen.secrets.GITHUB_TOKEN]
allow = ["github.com", "*.github.com"] # hosts that receive the real value- Sizes:
M/MB/MiB/G/GB/GiB(case-insensitive); a bare number is MiB. - Mounts: the host path is relative to the kitchen file's directory and
must exist. Guest paths must be absolute, unique, and must not overlap
/var/cache/mise,/opt/kitchen,/opt/miseor/.msb, which microkitchen manages. dotfiles: a host directory staged into the sandbox as mise'sdotfiles.root(see Dotfiles and system files).- Ports are published on the host's
127.0.0.1only. networkis microsandbox's preset:nonedisables networking,publicblocks private ranges, loopback, link-local and cloud metadata,openallows everything microsandbox allows.allow/denyare enforced by microkitchen's broker, not by microsandbox (Network access).- Rule entries (
allow,deny, secretallow): an exact host name, a*.suffix(the suffix and all its subdomains), an IPv4/IPv6 address, or a CIDR range. Entries are validated, never repaired. - A plain
[microkitchen]table also works, but mise warns about it; use[_.microkitchen], which mise ignores by design. Having both is an error.
The configuration is validated before every create or change, with every
problem reported at its line. microkitchen validate runs the same checks.
Shells and commands run as chef, not root. Unless the kitchen file declares
chef itself, the sandbox gets:
[bootstrap.users.chef]
uid = 1001
group = "chef"
groups = ["sudo", "docker"] # passwordless sudo, and Docker
shell = "/bin/bash"
comment = "sandbox user"
[bootstrap.groups.chef]
gid = 1001The sudo group may use sudo without a password. exec --root and
shell --root run as root directly.
To change chef, declare [bootstrap.users.chef] yourself; it is used as
written (see mise's accounts),
except that a missing uid is 1001 and a missing group is chef (with gid
1001). Leave sudo out of groups and chef has no sudo:
[bootstrap.users.chef]
groups = ["docker"]
shell = "/bin/sh"chef's uid and gid are fixed when the sandbox is created: the sandbox runs as
them before bootstrap has created chef, and files in mounts appear owned by
them, so chef can write there. Changing them needs remodel --recreate, and a
primary group other than chef must declare its gid. chef cannot be removed
(state = "absent") or be uid 0.
[env] is resolved on the host with mise (_.file, _.source, templates
and all). Each resolved variable goes into the sandbox:
- as a plain environment variable, unless
- a
[_.microkitchen.secrets.NAME]table exists for it: then it is a microsandbox secret. The guest sees only a placeholder; the real value is substituted into TLS connections to the secret'sallowhosts, and the placeholder passes through unchanged everywhere else.
Every secret table needs a matching [env] declaration. Variables that resolve
to an empty string are not injected; a secret declared for one is skipped with a
notice. The guest's copy of mise.toml drops _.file, _.source and _.path,
so .env files never enter the sandbox.
A templated dotfile that interpolates a secret
gets the placeholder, not the value. Substitution is a literal match on the
bytes sent, so it works where the value travels verbatim (Authorization: Bearer <token>) and not where it is transformed (a ~/.netrc that curl sends
as Authorization: Basic base64(user:token)). Staging a real credential file
as a plain dotfile copies it into the sandbox in clear text.
mise's dotfiles ([dotfiles]) and
system files
([bootstrap.files], [bootstrap.directories]) work in a kitchen:
mise bootstrap applies them inside the sandbox.
[dotfiles]
"~/.gitconfig" = { source = "dotfiles/gitconfig", mode = "copy" }
"~/.config/nvim" = { source = "dotfiles/nvim", mode = "symlink" }
[bootstrap.files."/etc/apt/apt.conf.d/99custom"]
source = "etc/apt.conf"
owner = "root"
mode = "0644"Only the kitchen file itself reaches the sandbox, so microkitchen stages
the host files an entry names: it copies exactly those paths in and rewrites
each source to its copy. Nothing else from the project is copied. This is
the approach mise takes for remote bootstrap over
SSH, narrowed to the paths the
configuration names.
/opt/kitchen/
├── mise.toml the kitchen file, owned by root
└── files/ staged sources, owned by chef
├── project/… sources under the kitchen file's directory
└── <hash>/… sources from anywhere else
- A
sourcealways names a path on the host, relative (to the kitchen file's directory),~/-prefixed or absolute. It may point outside the project; a missing or unreadable one is an error at its line. - Sources from outside the project are named by a hash of the host directory holding them, so host user names and paths never enter the sandbox.
- Staged files belong to chef, so
mode = "symlink"entries are editable in the sandbox — but those edits stay in the sandbox and are lost when it is recreated. Use a mount to share a directory both ways. microkitchen validatelists every host file that will be copied and where it lands, flagging anything from outside the project.- Editing a staged file changes no TOML, so the contents are tracked:
microkitchen remodelcopies them in again and removes what the kitchen file no longer references. - Whether an entry makes sense in a throwaway sandbox is mise's business:
microkitchen reports only what stops it producing a copy.
mode = "track"keeps its history inside the sandbox, and[dotfiles]in a global host config is never applied.
A mode = "template" entry renders in the sandbox, where a secret is only the
placeholder, so the real value never reaches the sandbox's disk — see
Environment variables and secrets.
Every sandbox's DNS and outbound traffic goes through the egress broker, a
per-user daemon (microkitchen broker). It gives each sandbox its own DNS
resolver, which records which names the sandbox resolved to which addresses,
and its own SOCKS5 proxy for TCP and UDP. microsandbox refuses DNS to any other
resolver and DNS over TLS, so the broker sees the sandbox's name lookups.
In order, first match wins:
- Cloud metadata, link-local, multicast, unspecified and broadcast addresses: always denied.
- While
mise bootstrapruns: everything else is allowed. - The kitchen file's rules, then
~/.microkitchen/rules.toml(for every sandbox). Within each file,denywins overallow; each rule is checked against the address and the names the sandbox resolved to it. A name rule never matches an address the sandbox did not look up for that name.ntp.ubuntu.comis allowed built in (the image's time sync). - Temporary allows ("Allow 5 min",
net temp). - Otherwise: ask.
The dialog shows the sandbox, the destination, the names the sandbox resolved for it (or a warning that it never resolved this address, typical of hard-coded addresses), and the process that opened the connection:
Sandbox: mk-api-1a2b3c4d
Destination: 140.82.121.3 : 443 (TCP)
Resolved as: api.github.com
Process: pid 412 (node)
- Deny adds the name (or address) to the kitchen file's
deny. - Allow adds it to
allow. - Allow 5 min allows it for this sandbox for five minutes, not saved.
- Closing the dialog, or no answer within 60 seconds, denies this connection only and saves nothing.
One dialog is shown at a time, across all sandboxes; identical concurrent requests share one dialog, and a request whose connection is gone is dropped unseen. The process line is for information only: it comes from inside the guest and never decides anything. It shows the short process name, never the command line.
Rate limit: more than 20 prompts within 10 minutes switch that sandbox to
deny-all (even allowed names), with one notification. It stays that way, across
broker restarts, until microkitchen net resume.
Without a desktop (approval.dialog, see Settings), the fallback
is approval.headless:
"deny"(default): anything that would ask is denied."queue": requests wait for an answer frommicrokitchen net pendingandmicrokitchen net decide <id> allow|deny|temp.
net pending / net decide also work while a dialog is up.
| Command | |
|---|---|
net allow <rule> [--global], net deny <rule> [--global] |
Add to the kitchen file (or ~/.microkitchen/rules.toml); moves it out of the other list. |
net revoke <rule> [--global] |
Remove it from both lists. |
net rules |
The rules in effect, including built-ins and global rules. |
net temp <host> |
Allow for five minutes, this sandbox only. |
net pending, net decide <id> allow|deny|temp |
Headless approvals. |
net resume |
Leave deny-all after the rate limit tripped. |
net bindings |
Which names the sandbox resolved to which addresses. |
net mode open|enforce |
Switch mediation off or on for this sandbox. |
Rule changes, by command or by editing either file, apply to the next
connection; nothing restarts. ~/.microkitchen/rules.toml holds top-level
allow = [...] and deny = [...] lists in the same syntax.
- The broker is in the path. While it is not running, sandboxes have no
network access at all.
up,start,execand the other commands that start a sandbox restart it. After a restart, saved rules apply at once; bindings and temporary allows are gone, so the first connections may ask again. - DNS over HTTPS cannot be blocked by port. A program that resolves names that way connects to addresses the sandbox never resolved, and the dialog says exactly that.
- UDP attribution works only for connected sockets; others show no process.
msb start <name>outside microkitchen fails proxy authentication: the proxy password is set by microkitchen when it starts a sandbox.
After editing the kitchen file, microkitchen remodel lists what changed
compared with what the sandbox runs with, says when each change takes effect,
and asks before applying (--yes skips the question):
| Change | Takes effect |
|---|---|
allow / deny |
At once (the broker re-reads the file). |
cpus, memory |
At once when the runtime can resize the running VM, otherwise after microkitchen restart. |
disk, environment variables, secrets |
After microkitchen restart (at the next start for a stopped sandbox). |
mounts, ports, network, chef's uid or gid |
Need a new sandbox: microkitchen remodel --recreate (only the mise cache is kept). |
When anything mise uses changes (tools, [env], [bootstrap] and so on),
remodel also updates the guest's mise.toml and runs mise bootstrap to
apply it; a stopped sandbox is started for this and stopped again. While a
change to chef's ids waits for --recreate, so does the guest's mise.toml. It only ever removes environment variables that
came from the kitchen file, never the image's own.
~/.microkitchen/config.toml (every key optional):
mise_version = "2026.9.6" # mise installed in guests (default: latest)
[approval]
dialog = "auto" # auto | zenity | kdialog | osascript | none
headless = "deny" # no dialog: "deny" or "queue" for `net decide`
timeout_secs = 60 # unanswered approvals deny the connection
max_prompts = 20 # more prompts than this within window_secs
window_secs = 600 # switch a sandbox to deny-all
[broker]
port_range = [40000, 49999] # per-sandbox resolver and proxy ports
upstream_dns = ["1.1.1.1"] # default: the host's /etc/resolv.confdialog = "auto" uses osascript on macOS and, when DISPLAY or
WAYLAND_DISPLAY is set, zenity or kdialog on Linux.
~/.microkitchen/
├── config.toml settings
├── rules.toml network rules for every sandbox
├── sandboxes/<name>/ state.json (applied configuration), proxy-secret
├── logs/<name>/bootstrap.log mise bootstrap output
├── logs/broker.log broker log
└── broker/ audit.log (one JSON line per decision), registry.json
Sandboxes are named mk-<directory>-<hash of the kitchen file path> and carry
microkitchen.* labels linking them to their kitchen file. mise's cache lives
in the microkitchen-mise-cache volume, shared by all kitchens.
- mise rejects
{ required = false }. Declare optional variables as{ default = "" }; they are injected only when set on the host. - Variables that mise passes through unchanged from the host environment (for
example
{ required = true }ones) are read from the host directly. [tools]are installed bymise bootstrapinside the guest when the sandbox is created and again bymicrokitchen remodelafter they change.- Inside the guest the kitchen file is
/opt/kitchen/mise.toml(without the[_.microkitchen]table, and with chef added if missing). It is mise's system config, so tools work from any directory (mounts included), and chef's global config (~/.config/mise/config.toml) stays chef's own formise use -g. - Host files named by
[dotfiles]and[bootstrap.files]are staged into/opt/kitchen/files, and everysourcein the guest'smise.tomlis rewritten to its copy there. mise bootstrapruns in two steps. Root installs mise and sudo and applies the accounts, which creates chef; then chef runs the full bootstrap, so[tasks.bootstrap]runs as chef. Tools are installed in/opt/mise, owned by chef, and mise's cache is/var/cache/mise.
mise provides the toolchains this repository pins (the
Rust toolchain, Node.js and pnpm): mise install installs them, and
mise run install builds and installs the binary into ~/.cargo/bin.
Everything else runs as a mise task too (mise tasks lists them):
mise run test # unit tests and VM-free integration tests
mise run lint # rustfmt and clippy, warnings are errors
mise run check # both (what CI runs on hosted runners)
mise run test:scripts # the guest attribution script against fake /proc trees
mise run test:integration # VM tests: need KVM, msb and cargo-nextest
mise run test:vm # VM tests with cargo test, one at a timeThe VM tests boot real sandboxes against the internet; with
MK_TEST_ISOLATE_HOME=1 they use their own microsandbox home. Their kitchens
use one vCPU: in nested virtualization, 2-vCPU guests were several times slower
to boot and bootstrap.
Design and plan: specs/egress-broker-design.md,
specs/implementation-plan.md.