Commit graph

13 commits

Author SHA1 Message Date
dan-claude-bot
8e6f3a4bb8 grant/rehearsal: the codex round — verified rollback, loud partial states, and the raw-attach guarantee measured (#75)
Review 4727756972 (A2): the backout no longer trusts gpasswd — it re-reads
the live group database after removal; verified-absent gets the safe
message, anything else screams ROLLBACK INCOMPLETE, exits nonzero, and
names the exact remediation. The concurrent-login window (a session begun
between usermod and backout keeps the group) is CLOSED to the extent the
database can't reach: the backout detects live processes and names
loginctl terminate-user, and the success wording claims only what was
verified.

Review 4727641752 (A1): a failed grant for a user whose membership predates
the run (the hand-added-user scenario) now fails LOUDLY — they retain
socket access on part-converged policy, and the message says so with both
remediations (box revoke now, or fix and re-run). Their membership is not
stripped: breaking a working user over a failed re-grant is its own hazard.
The default-profile eth0 removal is deliberately not restored on failure —
that mutation only reduces capability, and restoring it would move the
failure state AWAY from fail-closed. Injected-failure coverage is criterion
(n), both flavors: fresh-user backout (fault at the LAST mutation, so the
rollback runs after every earlier one) with the group's absence verified
and a converging re-run; blocked narrowing staged for real with an
instance-local NIC parked on the private bridge.

Review A3, resolution 3 with the measurement demanded: criterion (m)
launches exactly 'incus launch --network boxnet' as the restricted user and
probes the raw NIC from inside — egress works, RFC1918 dropped (the ACL is
the network's), sibling probes dropped BOTH directions (the nft drop is the
host's), name enumeration blocked. The scoped guarantee is now stated in
box-design.md and measured on every run: box-minted instances carry per-NIC
port_isolation; raw attachments keep every network- and host-owned control,
losing only that redundant L2 layer. Instrument lesson kept as MU-5: the
probe's first cut minted the non-cloud image — no DHCP client, no lease,
and a dead NIC passes every negative probe vacuously; it now requires the
lease before believing its own answers.

Rehearsal: 54/54 (containers). test/cli.sh: 82 checks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 06:41:05 +00:00
dan-claude-bot
565e7693e8 docs: log MU-4 (the CI job's version-drift catch) and refresh the counts
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 05:38:04 +00:00
dan-claude-bot
2097982ac2 docs: the restricted tier — README, design doc, plan doc with measured results (#74)
The plan doc records what was measured and why each decision fell where it
did: the private bridge is worse than unhardened (a live NAT bridge with
IPv6 on), incus-user blocks snapshots, no daemon-level template exists (read
in incus-user's source), widening survives re-sync (same source, then
measured live). box-design gains the access-tiers section — including why
narrowing to boxnet-only is the load-bearing decision and why the nft bridge
drop is the layer a restricted user cannot strip. RUNS.md logs MU-1..3.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 04:09:48 +00:00
claude-hdb
bcc641beff docs(drill): record runs 11–13 — the contract measured at zero, from a bare host
The log stopped at run 10's headline (boxes reach each other). Three
runs later the arc is closed and RUNS.md should say so:

- run 11 (45/2): A3 re-answered with the fixed probe — BLOCKED,
  security.port_isolation; both FAILs were the drill's own empty-host
  assumption
- run 12 (46/0): first zero-failure run
- run 13 (47/0): full teardown → rebuild → drill from a bare host,
  pinned resolver built from scratch on a Tailscale host

Answer table updated (A3/A4 show fixed + re-measured state), headline
flipped to match, traps 13–15 added (TTY stdin under timeout, the
gateway-ping false alarm, demanding an empty host), run history gains
the three rows and a closing note.
2026-07-14 12:52:02 +00:00
Daniel Marin
03546917aa
Merge pull request #41 from claude-hdb/fix/probe-reads-the-message
fix(drill): read curl's message — the exit code cannot tell you what happened
2026-07-14 12:28:36 +01:00
e00b6666da fix(drill): read curl's message — the exit code cannot tell you what happened
Boxes are isolated. The kernel says so: 'isolated on' on both live bridge
ports. A box gets 100% packet loss pinging its sibling, and its TCP
connect burns the full timeout and dies with "Could not connect to
server" — not "Connection refused", which is what an arriving packet
gets, instantly.

The drill called that a FAIL. curl exit 7 is "failed to connect", and it
covers BOTH a refusal (a RST came back — the packet ARRIVED) and an
unreachable host (nothing came back — it was DROPPED). Opposite
conclusions, one exit code, and the drill mapped 7 → "arrived". So for
two runs after the isolation fix had landed and was working, the drill
reported a working boundary as a broken one.

The words distinguish what the number cannot. box_probe now returns
reachable | refused | dropped by reading the message, ping corroborates,
and every isolation probe (sibling, box→host, RFC1918, host→box) uses it.

RUNS.md gains trap 12. It is the same disease as all eleven before it:
trusting a proxy for the fact instead of the fact.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 02:33:02 +00:00
7fb2b28ce5 docs(drill): the audit is complete — and boxes are not isolated from each other
Run 10, on a doctor-clean host: 48/49, every #15 probe answered.

A3, the probe the whole exercise existed for and which nine runs failed
to fire, says sibling isolation DOES NOT HOLD. Box A's SYN reaches box
B and B answers with a RST. #12 reasoned from the code that the
10.0.0.0/8 egress drop covered box-to-box traffic "belt and braces";
two boxes on one bridge share an L2 segment, so their traffic is
switched, never routed, and never meets that rule. The tool's contract —
"a box reaches the public internet and nothing else" — is false: it also
reaches every other box on the host.

Also settled: ipv4_filtering breaks the box (measured on a baseline that
had already passed egress, unlike run 7's void verdict); dns.mode=none is
viable after all, retracting a veto I posted on one bad reading;
@internal is unsupported on bridge ACLs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 01:25:42 +00:00
9736977196 fix(doctor): check that something is actually serving the network
Two cold mints died in cloud-init with "Temporary failure resolving
deb.debian.org", on a host the doctor had just certified clean. The
cause was not DNS forwarding, not leftover mutations, and not the drill:
claudenet had NO dnsmasq. It never respawned after the SIGKILL that
recovered the wedged daemon in run 6, so boxes got no DHCP lease at all
— no address, no gateway, no DNS.

Incus does not surface this. The bridge is up, the config is pristine,
'incus network show' says status: Created. Only the process table knows.
So the doctor now asks the process table, and --fix restarts incus to
respawn it.

An hour of hunting and two dead mints went into learning this. It is a
five-second check.

RUNS.md gains trap 11: a network incus calls "Created" may have nothing
serving it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 01:13:38 +00:00
b0eefd8369 fix(drill): stop poisoning the host, and add a doctor to prove it
Run 8's cold mint failed with 'cloud-init status: error' — the box could
not resolve deb.debian.org, or claude.ai, or anything. The cause was not
in that run at all: run 7's phase D set dns.mode=none on claudenet, the
run ended before reverting it, and every box minted afterwards came up
with no DNS.

This is the worst failure mode the drill has: a poisoned host does not
fail the next run honestly, it produces confident wrong answers. It is
how a false design veto against #16 got posted, and it wasted a cold
mint plus an hour of diagnosis that had nothing to do with the code
under test.

Three defences:
  · the phase-D revert is armed with a trap BEFORE the first mutation,
    so it fires on any exit, Ctrl-C included;
  · the revert is VERIFIED rather than fired into /dev/null, so a failed
    unset can no longer masquerade as a successful one;
  · the drill refuses to start on a host still carrying the mutations.

And drill/doctor.sh answers the question that kept being answered by
hand: what state is this host actually in? Network, profile, ACL,
leftover boxes, and whether a box can still resolve DNS — with --fix to
revert the leftovers.

RUNS.md gains trap 10.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 00:32:03 +00:00
769287eb7e fix(drill): refuse to judge #16 on a broken baseline
Run 7's box had no network — a clone/source IP collision had taken it
down before a single hardening change was applied. Phase D measured
anyway and reported "L2 filtering BREAKS the box — design veto". It does
not. The box was already broken, and #16 would have been redesigned
around a fiction.

Phase D is now gated on baseline egress passing, and says loudly that it
is skipping rather than quietly producing a verdict. The dns.mode
rejection also stops swallowing incus's error message — the message IS
the finding.

RUNS.md gains trap 9: check that the thing you are measuring WITH still
works before you trust what it tells you. Same failure as the B3 flip,
different costume.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 00:04:01 +00:00
6899fc3626 fix(drill): the NIC inside a VM is enp5s0, not eth0 — read by subnet instead
A3, the one probe the whole audit exists for, has never fired in six
runs. It was never the network: the profile names the DEVICE eth0, but
inside a VM guest predictable naming renames it enp5s0, so every address
lookup — first the '(eth0)' CSV match, then 'ip addr show dev eth0' —
was hunting an interface that does not exist. I fixed that symptom twice
without ever questioning the assumption underneath it.

Read the address from inside the box and select by SUBNET (10.87.x, what
claudenet hands out) rather than by interface name. docker0's 172.17.x
is the decoy; the NIC's name is the guest's business, not ours.

A3 also gains a guard it should have had from the start: if the peer and
the source hold the SAME address, refuse to probe. That is not
hypothetical — clones were inheriting their source's machine-id, hence
its DHCP lease, hence its address, so 'archive → peer' was archive
probing itself and would have reported a cheerful 'reachable' as an
isolation failure.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 23:32:22 +00:00
dcc9f001b3 fix(drill): clean the host BEFORE setup-host, and bound it
Run 6 stalled in setup-host.sh, which should be seconds on a host that
already has incus. The ordering was wrong: cleanup ran AFTER setup, so
setup-host reconfigured claudenet's ACLs while an aborted run's boxes
were still attached to that network — 'incus network set' then has to
push the change onto every live NIC. The same aborted run also left the
D-phase mutations (dns.mode=none, NIC filtering) in place, so setup was
converging against a moving target.

Boxes are now deleted and the mutations reverted first, so setup-host is
the no-op it should be. It is also bounded (5 min) and, on timeout,
prints the three things worth checking instead of hanging: instances
still on the network, the firewall unit, the incus daemon. Every cleanup
call gets its own timeout, so a wedged instance cannot stall the run
before the drill has printed a single line.

RUNS.md gains this as trap 8.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 23:09:51 +00:00
a3e5d1226e docs(drill): keep the run log in the repo, not in PR descriptions
Five runs of hard-won knowledge — which probes are answered, which bugs
the drill found in claudebox, and the seven traps the drill itself fell
into — has been living in PR bodies and chat, where the next person
debugging this cannot find it.

RUNS.md carries: the audit scoreboard (A3 still unanswered, and why that
matters), the claudebox findings, a traps section to read BEFORE adding
a probe, stall diagnostics, how to run a single probe by hand instead of
paying for a full run, and the B3 flip-flop as a standing lesson about
verdicts drawn from one observation of a system with restart semantics.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 23:06:38 +00:00