Network profiles
A bunker's network profile decides what traffic it can make. The five profiles range from "no network at all" to "full outbound plus L2 attaches", and each is gated by a separate permission so a tenant can grant exactly the network posture each user or agent should have.
The five profiles#
network_profile is set at create time on the bunker and can't be changed afterwards. Pick before you provision; if the posture needs to change later, take a snapshot, terminate, and create a new bunker.
isolated (default)#
No network. The bunker can talk to the controller's agent backchannel and nothing else — no DNS, no outbound TCP, no inbound anything. Best for running untrusted user-submitted code, evaluating model output, or any workload that genuinely doesn't need the internet. No permission required beyond scaibunker:create.
registry#
Allowlist of common package registries (PyPI, npm, crates.io, Maven Central, RubyGems, the Hugging Face Hub, and a handful of OS package mirrors). pip install, npm install, cargo build, apt update against the platform-managed apt mirror — they all work; nothing else does. Gated by scaibunker:network:registry. Use this for the common case of "I need to install deps but I don't want the bunker reaching arbitrary hosts."
allowlisted#
Tenant-configured domain list. You set network_allowlist on the bunker:
1 2 3 4 5 | |
Allowlist entries are validated at the API edge:
- Plain hostnames (
github.com,pypi.org) and first-level wildcards (*.example.com) only. - No scheme prefixes, no path components, no double wildcards (
**.foo.com), no mid-name wildcards (foo*.example.com). - Each label is 1-63 RFC 1035 characters.
The worker enforces the allowlist via DNS interception plus per-flow connect filtering. Gated by scaibunker:network:allowlisted.
unrestricted#
Full outbound. The bunker can reach anything its worker can. Egress is optionally audited as NDJSON batches written to scaibunker/audit/{bunker_id}/{ts_us}-{worker_pid}.ndjson and surfaced through GET /bunkers/{id}/audit-batches. Gated by scaibunker:network:unrestricted — typically held by a small set of agents that need the full open internet (web-scraping agents, broad SDK testing).
transit#
L2-attached to one or more tenant-scoped bridges. Used to chain network appliances (firewall, IDS, NAT, router) at L2 in front of application bunkers — the same way you'd rack a hardware firewall in front of a server. Each interfaces[] entry names a bridge_name the bunker plugs into; the bunker gets one TAP per interface. Gated by scaibunker:network:transit.
1 2 3 4 5 6 7 8 9 | |
Bridges are provisioned by tenant admins via POST /bridges. They are scoped to a tenant; a transit bunker can only attach to its own tenant's bridges (the worker also enforces this, defence-in-depth).
spoof_guard defaults to true and rejects packets whose source MAC isn't the interface's assigned MAC. Setting it to false is required for legitimate learning-bridge appliances and additionally requires scaibunker:l2_transparency; every such use is audit-logged.
Permission summary#
Each profile beyond isolated is gated by its own module-permission key. A caller without the key gets NETWORK_PROFILE_DENIED (403) at create time — the check happens before any worker round-trip.
| Profile | Permission required |
|---|---|
isolated |
none (any caller with scaibunker:create) |
registry |
scaibunker:network:registry |
allowlisted |
scaibunker:network:allowlisted |
unrestricted |
scaibunker:network:unrestricted |
transit |
scaibunker:network:transit + bridges in this tenant |
Adding spoof_guard: false to any transit interface additionally requires scaibunker:l2_transparency.
Bandwidth caps#
Every profile honours bandwidth_mbit, a per-bunker egress cap in megabits per second. The worker applies it via a TBF qdisc on the bunker's TAP. Acceptable range is 1-10000 Mbit/s; null means use the worker default (typically 100 Mbit/s). This is how tenants tier free vs paid plans — the same image and profile, but a tighter egress shaper for the free tier.
Choosing a profile#
A simple decision tree:
- Does the bunker need outbound? No →
isolated. - Does it only need package installs? →
registry. - Does it need a small, known set of HTTPS endpoints (your API, your databases)? →
allowlisted. - Does it need open-internet web access? →
unrestricted, and turn on the audit batches. - Is it a network appliance plugged between tenants and other bunkers? →
transit.
Default to the most restrictive profile that works. Most agents that need "the internet" actually only need registry plus one or two domains under allowlisted.
Where audit batches live#
When [audit].enabled is on for the worker and the bunker runs under unrestricted, the worker writes per-flow records as NDJSON to S3 under scaibunker/audit/{bunker_id}/. The controller never deletes them; lifecycle is the operator's S3 bucket policy. Callers retrieve batches through GET /bunkers/{id}/audit-batches (list) and GET /bunkers/{id}/audit-batches/{name} (fetch).
Each batch is a {ts_us}-{worker_pid}.ndjson file containing one JSON object per flow: source IP and port, destination IP and port, hostname (if DNS-resolved), protocol, byte counts, duration. The natural lex order of the keys is chronological, so pagination is trivial — list with a since_us cutoff and follow next_continuation_token until done.
Changing posture later#
The profile is set at create time and can't be changed in place. If a bunker needs to move from isolated to registry, take a snapshot, terminate, and create a new bunker referencing the snapshot. This is by design: a profile change rewrites the worker's iptables and namespace setup for the bunker, and applying it to a running microVM has too many failure modes worth supporting.
Common mistakes#
- Putting URLs in
network_allowlist. Entries are hostnames, not URLs.https://api.example.comis rejected;api.example.comis accepted. - Double-wildcard.
**.example.comis rejected — only first-level wildcards. Use one*.example.comentry per leaf domain, or list explicit hostnames. - Forgetting
bandwidth_mbiton the free tier. A high-throughput bunker on the open internet can saturate a worker's uplink. Set a per-bunker cap on the bunkers you give to less-trusted tenants. - Granting
scaibunker:network:unrestrictedwidely. Even with audit batches on, the unrestricted profile is the highest-risk profile by far. Grant it deliberately, per service account, and review the audit batches.
Transit-bunker scheduling#
The scheduler only considers workers that own every bridge a transit bunker references. If you create a transit bunker referencing tenant-wan and tenant-lan and no single worker has both, the call fails with NO_SUITABLE_WORKER. The fix is to create matching bridges on a common worker before launching the bunker. Bridge create is a two-phase operation — the controller reserves the row, calls the worker, and rolls the row back if the worker rejects, so the controller and worker ledgers never drift.
Transit bunkers can't be migrated between workers — the bridges they depend on are worker-scoped. Plan transit workloads with that in mind; for moveable workloads use one of the v0.1 profiles.
DNS#
The registry and allowlisted profiles use a controller-managed resolver that returns A/AAAA records only for hostnames the profile permits. NXDOMAIN comes back for everything else; there's no exfiltration channel through arbitrary DNS lookups. On unrestricted and transit, DNS resolution is unrestricted (and on transit, the bunker's own resolver may be a different appliance — e.g. a downstream firewall — depending on what the operator wires up).
The resolver lives on the worker and is reset between bunkers, so one bunker's leaks don't carry over.