Skip to main content
Every sandbox gets its own tap device, leased from a pre-created pool, attached to a shared Linux bridge. That’s what gives it a real network namespace distinct from every other sandbox on the host.

Outbound access

Sandboxes NAT outbound through the bridge and resolve DNS through a host-local DNS proxy (scripts/host-setup/start-dns-proxy.sh) — no per-sandbox network configuration needed, it just works the way a normal Linux box with internet access would.

Sandbox-to-sandbox isolation

Bridge port isolation means sandboxes can’t reach each other directly — verified live: two sandboxes on the same bridge, neither can open a connection to the other, while both can still reach the gateway and the outbound internet. This is the network-layer half of isolation; the kernel/hardware half is the microVM boundary itself (see The jailer and privilege model).

Why a pre-created tap pool

The daemon runs unprivileged, with exactly one Linux capability raised — CAP_NET_ADMIN, ambient, not full root. That capability covers netlink operations (attaching a tap device to the bridge, bringing a link up or down) but not creating a brand-new tap device, which goes through a different kernel path (TUNSETIFF on /dev/net/tun) that doesn’t honor ambient CAP_NET_ADMIN the way netlink calls do. So tap devices are pre-created once, as real root, by a one-time setup script (scripts/host-setup/create-tap-pool.sh, or scripts/setup.sh end to end) — the daemon only ever leases an existing device from that pool. See TAP devices and bridge networking for the full reasoning, including the trade-off this creates (a fixed pool size caps concurrent sandboxes until it’s grown).

Guest-accessible metadata (MMDS)

Every sandbox created with a name and/or tags automatically serves its own {id, name, tags} to itself at http://169.254.169.254/ — Firecracker’s own native MMDS (Microvm Metadata Service), a link-local endpoint answered by Firecracker’s device model directly, not by the guest agent. Nothing to configure: no sandkiln HTTP route or SDK method is involved on the read side at all, since it’s entirely inside the guest’s own network stack. Configured V2 (token-gated), specifically because a sandbox may run untrusted or AI-generated code that shouldn’t be able to read metadata via an unauthenticated request the way V1 would allow:
Accept: application/json matters — a GET with no Accept header returns an AWS-IMDS-style newline-separated list of top-level key names instead of the actual value, a real Firecracker behavior worth knowing about, not a sandkiln bug.

Egress (outbound network) policy

By default every sandbox keeps unrestricted outbound access — the behavior above, unchanged. An optional egress field on POST /sandboxes (and POST /sandboxes/get-or-create) restricts it per sandbox:
Both SDKs and the CLI wrap this directly — Sandbox.create({ egress }) (JS/TS), Sandbox.create(egress_mode=..., egress_allow_cidrs=..., egress_deny_cidrs=...) (Python), --egress-mode/--allow-cidr/--deny-cidr on kiln sandbox create — rather than needing the raw body shown above. mode is allow_all (today’s default-open behavior, minus whatever deny_cidrs subtracts from it) or deny_all (nothing outbound except what allow_cidrs opens back up). Enforced with one dedicated iptables chain per sandbox, named from its tap device, with a single jump rule inserted ahead of the daemon’s own bridge-wide rule so only that sandbox’s traffic is affected. deny_cidrs rules always win over allow_cidrs on overlap — iptables evaluates a chain top to bottom, first match wins, and every deny rule is placed before every allow rule, so there’s no special-casing needed to make deny authoritative. Rules match only traffic actually leaving through the daemon’s own uplink interface, the same scoping the bridge-wide rule already uses — which has a useful side effect: DNS queries to the bridge’s own gateway IP never transit the uplink at all, so they’re structurally exempt from any egress policy without needing an explicit allowlist entry. Even a deny_all sandbox with nothing allowed can still resolve names; it just can’t reach anything past the gateway that isn’t explicitly permitted. A policy is tied to the sandbox’s network lease, not its VM: applied once the lease goes live (fresh boot, pool claim, resume, fork, or a time-travel restore) and removed only when the lease is finally released (a full destroy, or deleting a held snapshot or retired checkpoint) — a plain stop-and-preserve leaves the chain dormant but intact, exactly like the tap device it’s attached to. Not yet built: domain-level rules (would need the shared DNS proxy, currently one instance with no per-source differentiation, to become source-IP-aware — a substantially bigger change) and port-level matching (-p tcp --dport, a straightforward extension of the same rule shape, just not built yet). IPv4 only, matching every other networking type in this project.