Assistant Network Egress Policy¶
Control what the assistant container can reach on the network. By default the container has unrestricted network access. Opt in to restrict-local to keep full web egress while restricting local-network access to a user-editable allowlist — so the assistant can reach a specific dev/test container (e.g. an SSH test sidecar) but not the rest of your LAN.
Opt-in, default off. Nothing changes unless you enable it.
# Enable for a project (or globally / per assistant, like any nyia config key):
nyia config project network_egress_policy=restrict-local
# Back to default (unrestricted):
nyia config project network_egress_policy=off
network_egress_policy is a standard nyia config key: off | restrict-local,
default off, validated, and visible in nyia config view. It follows normal scope
precedence (CLI → project → global → team → default).
What restrict-local does (Phase A: network substrate)¶
Today, native-Linux launches use --network host — the container shares the host's
network stack and can reach all of loopback and the entire LAN. That is the
opposite of the goal, and it means any in-container firewall would edit the host
firewall. So restrict-local first moves the container off host networking into its
own network namespace:
- Native Linux: the assistant launches on a dedicated bridge network
(
nyia-egress) instead of--network host, with--add-host=host.docker.internal:host-gatewayso host services stay reachable. The bridge is created on demand; if it cannot be created, the launch is refused (fail-closed). - Docker Desktop / WSL2: already uses a bridge, so the network mode is
unchanged here — only
host.docker.internalis added. (The firewall in Phase B is what differs.) - off: byte-for-byte today's behavior (
--network hoston native Linux).
RAG / ollama still works¶
detect_ollama_host() resolves host.docker.internal first (provided via
host-gateway), then 172.17.0.1, so ollama-backed RAG continues to work on the
bridge. Web egress (e.g. curl https://example.com) also still works — only the
local network is the target of restriction.
⚠️ Limitation (Phase A alone)¶
A plain bridge still routes to the LAN. Phase A only relocates the container into its own netns — it does not yet block local destinations. The actual egress firewall (allow web + DNS, deny private ranges, allow a user-editable host:port allowlist) is Phase B and runs as in-container nftables applied at root then locked (NET_ADMIN dropped before the session). Until Phase B is enabled, treat restrict-local as "isolated netns + host-gateway", not "LAN blocked".
The firewall (Phase B): allowlist + enforcement¶
Phase B adds the actual enforcement as an in-container nftables firewall, shipped
as an opt-in egress-hardened image variant (tag suffix -egress).
End users don't build anything. Under restrict-local, nyia pulls the published
-egress variant automatically and launches it instead of the default image. It then
verifies the pulled image is a genuine hardened variant (by a baked-in label + entrypoint,
not the tag name) before granting it any extra privilege. If the variant can't be obtained
or isn't genuine, the launch is refused (fail-closed) with a clear message.
Maintainers/CI publish the variants; to build one locally use the dev build command (it layers egress on top of the assistant image):
nyia-claude --build --dev --egress # dev image + its -egress variant
nyia-claude --build --egress # release image + its -egress variant
nyia-claude --build-custom-image --egress # egress-hardened version of YOUR overlay
An egress image must always be the outermost layer — you cannot build on top of one, and
--image <x>-egressis rejected (use the config key, which selects it safely). Seedocs/DEV_GUIDE_EGRESS.mdfor the architecture.
What the firewall allows / denies:
- Allow: loopback, established/related return traffic, DNS to the container's configured resolver, every public internet destination, and each entry in your allowlist (matched before the private-range drop).
- Allow (conditional): ollama at
host-gateway:11434, only when RAG is enabled. - Deny: the private / special-use ranges —
10/8,172.16/12,192.168/16,169.254/16(link-local),127/8,100.64/10(CGNAT),192.0.0/24,198.18/15,240/4.
So: full web egress, your allow-listed local host:ports (e.g. the test sidecar), and
nothing else on the LAN.
The allowlist (user-editable)¶
A structural file (like exclusions.conf), layered and merged:
~/.config/nyiakeeper/config/network-allow.conf → .nyiakeeper/network-allow.conf →
.nyiakeeper/private/network-allow.conf. Each line is proto host port:
See config/network-allow.conf.example. Validation is strict (fail-closed): only
tcp/udp, a single port 1..65535, a hostname or IPv4 literal — no CIDRs, no
0.0.0.0, no ranges, capped entry count. Names are resolved to IPs when the firewall
is compiled at container start.
Capability lifecycle (why the session can't tamper)¶
The hardened variant starts as root, applies the nft ruleset, verifies IPv6 is
disabled, then drops NET_ADMIN (effective + bounding set) and setprivs down to
your mapped uid:gid before running the assistant. The entrypoint then self-checks
that NET_ADMIN is gone and refuses to run otherwise. So you edit the policy
between sessions; the AI-driven session cannot change the rules. The debug shell
(--shell) bypasses the entrypoint and is therefore not available under
restrict-local.
Fail-closed¶
Under restrict-local, the launch is refused (never run unfiltered) if: the bridge can't
be created, the hardened image isn't built, the allowlist is malformed/oversized, nft
or kernel modules are missing, IPv6 can't be disabled, or NET_ADMIN isn't dropped.
Platform support¶
| Platform | restrict-local behavior (v1) |
|---|---|
| Native Linux | Enforced: dedicated nyia-egress bridge + host-gateway (host-net dropped) + in-container firewall |
| WSL2 / Docker Desktop | Refused (v1): the firewall variant is not yet verified there; the launch fails with a clear message. Set the policy to off to launch. |
| macOS / Docker Desktop | Refused (v1), same as above |
v1 scopes enforcement to native Linux so the security guarantee is only ever claimed where it is validated. Docker Desktop / WSL2 support is a follow-up (the firewall must run inside the Docker VM).
Notes¶
- The codex OAuth login path is separate from the session launch and is not affected by this policy; if a login flow needs host networking it is handled on its own path.
- This is distinct from mount exclusions (which hide files) — egress policy is about the network, not the filesystem.
- Never mounts
/var/run/docker.sock; no extra capabilities on theoffpath.