---
summary: "The five network profiles a bunker can run under \u2014 isolated, registry,\
  \ allowlisted, unrestricted, transit \u2014 what they allow, who can use them, how\
  \ to pick."
title: Network profiles
path: concepts/network-profiles
status: published
---

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:

```json
{
  "image": "python-3.12",
  "network_profile": "allowlisted",
  "network_allowlist": ["api.example.com", "*.internal.example.com"]
}
```

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`.

```json
{
  "image": "ubuntu-24.04",
  "network_profile": "transit",
  "interfaces": [
    {"if_name": "wan", "bridge_name": "tenant-wan", "spoof_guard": true},
    {"if_name": "lan", "bridge_name": "tenant-lan", "spoof_guard": true}
  ],
  "bandwidth_mbit": 100
}
```

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.com` is rejected; `api.example.com` is accepted.
- **Double-wildcard.** `**.example.com` is rejected — only first-level wildcards. Use one `*.example.com` entry per leaf domain, or list explicit hostnames.
- **Forgetting `bandwidth_mbit` on 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:unrestricted` widely.** 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.
