docs(readme): the isolation contract as it ships — and measured, not claimed #47
1 changed files with 49 additions and 8 deletions
57
README.md
57
README.md
|
|
@ -32,8 +32,10 @@ Installs the tree to `~/.local/share/claudebox` and links `claudebox` onto your
|
||||||
```
|
```
|
||||||
|
|
||||||
Idempotent. Installs Incus and creates the isolation stack: the `claudenet` NAT
|
Idempotent. Installs Incus and creates the isolation stack: the `claudenet` NAT
|
||||||
bridge, the `claude-isolate` ACL (drops all RFC1918/CGNAT/link-local egress),
|
bridge (sibling-name resolution off, resolver pinned to public upstreams —
|
||||||
the `claude-dev` profile, and firewall rules blocking instance → host. All rules
|
`BOX_DNS` overrides), the `claude-isolate` ACL (drops all RFC1918/CGNAT/
|
||||||
|
link-local egress), the `claude-dev` profile (port-isolated NICs — boxes can't
|
||||||
|
reach each other), and firewall rules blocking instance → host. All rules
|
||||||
re-apply at boot via `claudebox-firewall.service` — no post-reboot ritual. If
|
re-apply at boot via `claudebox-firewall.service` — no post-reboot ritual. If
|
||||||
the host lacks `dnsmasq-base` (Debian cloud images skip Recommends):
|
the host lacks `dnsmasq-base` (Debian cloud images skip Recommends):
|
||||||
`sudo apt-get install -y dnsmasq-base`.
|
`sudo apt-get install -y dnsmasq-base`.
|
||||||
|
|
@ -122,12 +124,51 @@ command surface is a table.
|
||||||
|
|
||||||
## Isolation
|
## Isolation
|
||||||
|
|
||||||
Dedicated NAT bridge `claudenet` + Incus `claude-isolate` ACL dropping all
|
The contract: **a box reaches the public internet and nothing else.** Not the
|
||||||
private-range egress, plus host-firewall rules that block instance → host
|
host, not your LAN, not another box, not even another box's *name*. What
|
||||||
(including the host's public IPs). The box reaches the public internet and
|
enforces it, layer by layer:
|
||||||
nothing else. Entry is `incus exec` over the local socket only — **no inbound
|
|
||||||
path exists.** The VM is the trust boundary: Claude can run arbitrary code inside
|
- **Dedicated NAT bridge** `claudenet`, IPv6 off. Every rule below is
|
||||||
and touch nothing you care about.
|
IPv4-only, so IPv6 would be an uncovered path — off is part of the
|
||||||
|
contract, not a default.
|
||||||
|
- **`claude-isolate` ACL** — drops all egress to private space (RFC1918,
|
||||||
|
CGNAT, link-local), with a single carve-out to the gateway so DNS works.
|
||||||
|
- **Sibling isolation, at L2** — two boxes on one bridge are *switched*,
|
||||||
|
never routed, so no L3 rule can separate them (learned the hard way; see
|
||||||
|
below). `security.port_isolation` on every box NIC plus an nft
|
||||||
|
bridge-family drop mean box A cannot exchange frames with box B at all.
|
||||||
|
- **No name-level reconnaissance** — `dns.mode=none` stops the gateway
|
||||||
|
resolving sibling names, and the bridge's resolver is pinned to public
|
||||||
|
upstreams (`no-resolv`), so tailnet names and split-DNS zones from a
|
||||||
|
host-level VPN don't resolve inside a box either.
|
||||||
|
- **Host firewall** — instance → host is dropped except DNS/DHCP, including
|
||||||
|
the host's public IPs. Entry is `incus exec` over the local socket only —
|
||||||
|
**no inbound path exists.**
|
||||||
|
|
||||||
|
The VM is the trust boundary: Claude can run arbitrary code inside and touch
|
||||||
|
nothing you care about.
|
||||||
|
|
||||||
|
### Measured, not claimed
|
||||||
|
|
||||||
|
Every clause above is probed live by an end-to-end drill, because the one time
|
||||||
|
this contract was reasoned about instead of measured, the reasoning was wrong:
|
||||||
|
box→box traffic was "covered" by an L3 drop that L2-switched frames never
|
||||||
|
meet — a hole found by probing, not by reading the rules. On a bare host the
|
||||||
|
drill installs the whole stack, mints a box cold, snapshots and clones it,
|
||||||
|
probes every boundary from inside the boxes, and removes what it minted —
|
||||||
|
currently **47 checks, 47 passing**. [drill/RUNS.md](drill/RUNS.md) is the full
|
||||||
|
history, including every trap that fooled a run into a wrong verdict.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
bash drill/doctor.sh # read-only: is this host healthy and the stack live?
|
||||||
|
bash drill/drill.sh # FULL end-to-end — mutates the host; use a machine you own
|
||||||
|
```
|
||||||
|
|
||||||
|
The doctor reads ground truth, not config claims — the kernel's `isolated on`
|
||||||
|
flag per bridge port, the process table, the resolver actually in use — and
|
||||||
|
diagnoses the host faults that have actually happened: a wedged Incus daemon,
|
||||||
|
a dnsmasq that silently isn't serving, a VPN resolver that boxes would
|
||||||
|
inherit.
|
||||||
|
|
||||||
## Recipes: the `.claudebox/` convention
|
## Recipes: the `.claudebox/` convention
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue