The node firewall's outbound DROPs named IPv4 ranges only, so the control this project exists to provide did not apply to IPv6 at all. grep -i ipv6 over the repository returned nothing.
This is not the kind of gap where a control turns out weaker than advertised. It is the kind where it does not apply, on a path that is open by default on every network — including networks with no IPv6 anywhere in them. Every Linux host brings up an fe80:: address on every interface whether or not anything routes v6, and a daemon bound to :: answers on it. So the machine beside a devbox was reachable across the segment with all four DROPs in force, ip -4 route showed nothing unusual, and dejima-devbox-audit reach reported PASS. That last part is what makes it worth a fix rather than a note: the audit was reporting containment it had not checked, which is the failure the README's own three-verdict design exists to prevent.
The rules
OUT DROP -dest ::/0, as rule 12 of the security group.
Dropped whole rather than mirrored from the v4 list, because IPv6 has no RFC 1918. A v6 network is numbered out of a globally routable prefix delegated by an ISP, so there is no equivalent list to write down: fc00::/7 catches unique-local addresses most networks do not use and misses the prefix a neighbour is actually on. A rule set that looks symmetrical and covers nothing is worse than a blanket drop, because it reads as handled. That argument, and the conditions under which it should be revisited, are recorded under "Rejected, with reasons".
This costs nothing that was being claimed. Internet egress stays unbounded and stays over IPv4, for the reasons already argued in firewall.fw.template. Rule 12 removes a second route to the internet and a first route to the LAN.
On the node rather than in the guest. net.ipv6.conf.all.disable_ipv6=1 looks equivalent and is not: it is a setting on a machine this model assumes an attacker holds root on. It is also worse than merely ineffective, because it would make the audit's own check pass — the guest would be certifying its own containment on the strength of a setting the guest controls.
Also adds OUT DROP -dest 100.64.0.0/10 as rule 11. A tailnet is numbered out of carrier-grade NAT space, which is none of the three RFC 1918 ranges, so a devbox on one reached every machine on the overlay. Worth adding even where no overlay exists today: it costs nothing, and the day someone installs Tailscale is not a day anyone revisits the firewall.
The audit
A second check on the other address family, reported on its own line, failing the whole run if it does not pass. It asks ff02::1 — the all-nodes link-local multicast address — who is on the link, and counts the replies that are not its own. A global or unique-local address configured here is a failure on its own and needs no probe, since it arrived by SLAAC or DHCPv6 and is therefore evidence rather than inference.
No positive control, unlike the v4 half. Both reasons a segment stays silent over IPv6 — dropped at the node, or never configured in the guest — are the outcome wanted, and no service a devbox needs lives on that family for the silence to be hiding one. dejima-ci-hardener argues the same way in test/ci-runner.bats for the same reason.
It does still report INDETERMINATE, for a different thing than the v4 half does: the probe not running at all. This was found by running the check in a container with no ping installed, where the first version returned "0 neighbours" and passed — nobody answered because nobody was asked. dejima-devbox-bootstrap now installs iputils-ping so that a bootstrapped host does not hit it, and a host set up before this change gets told which package to install.
ff02::1 is not the target list the audit refuses to keep. It is the same address on every network there has ever been, so naming it describes no topology and leaks nothing about this one.
Docs
docs/threat-model.md gains a Not bounded section: the page's negative space collected in one list — exfiltration, container escape, root, the model API credential, the two LAN exceptions, inbound exposure, other people's work, history, the hypervisor. The page was organised entirely by control, which left a reader to assemble that themselves, and most will not.
A section on the two address families, with the reasoning above.
Two new entries under Rejected, with reasons: mirroring the DROPs into IPv6, and disabling IPv6 in the guest. Each names what would have to change to reopen it.
This page has a date on it: a quarterly review cadence, matching dejima's, listing conditions to check rather than judgements to re-make. The argument applies harder at this layer than above it, since this is the layer that catches what the layer above fails to hold — and IPv6 was missing from the rule list for as long as the list existed, with nothing failing in the meantime.
proxmox/README.md gains rules 11 and 12 in the runbook table, the reasoning, an ip6tables -S verification step (a v6 rule is absent from iptables -S whether or not it is there, so checking only the v4 table shows it missing either way), and an ff02::1 probe in the by-hand check block.
docs/reference.md documents the new check's three verdicts. docs/building-a-devbox.md §6 covers it in the verify step.
Tested
bats test/ — 114 passing, up from 109.
New coverage: a global v6 address failing without a probe; link-local alone not failing, since the kernel brings one up regardless; a neighbour answering with no v6 address configured here; our own multicast reply not counting as a neighbour; an unrunnable probe reading as INDETERMINATE rather than a pass; a v6 failure failing the whole reach check while a v4 INDETERMINATE survives as the more specific verdict; and the remedy text pointing at the node rather than the guest.
The parsing was also exercised end-to-end against recorded ping output including the PING ... from ... header, our own reply, and (DUP!) lines, confirming it counts two neighbours and excludes self.
test/addresses.bats caught 100.64.0.0/10 as an unexplained literal on the first run, which is the leak guard working — it is now on that allowlist with a comment saying what it is for.
Not done here
The same gap exists in dejima-ci-hardener's hypervisor template and is being handled separately. Its in-guest firewall already blocks IPv6 on the job network, which covers container-to-LAN but not the VM's own reach.
**Summary generated by LLM agent**
## What this fixes
The node firewall's outbound DROPs named IPv4 ranges only, so the control this project exists to provide did not apply to IPv6 at all. `grep -i ipv6` over the repository returned nothing.
This is not the kind of gap where a control turns out weaker than advertised. It is the kind where it does not apply, on a path that is open by default on every network — including networks with no IPv6 anywhere in them. Every Linux host brings up an `fe80::` address on every interface whether or not anything routes v6, and a daemon bound to `::` answers on it. So the machine beside a devbox was reachable across the segment with all four DROPs in force, `ip -4 route` showed nothing unusual, and `dejima-devbox-audit reach` reported `PASS`. That last part is what makes it worth a fix rather than a note: the audit was reporting containment it had not checked, which is the failure the README's own three-verdict design exists to prevent.
## The rules
`OUT DROP -dest ::/0`, as rule 12 of the security group.
Dropped whole rather than mirrored from the v4 list, because **IPv6 has no RFC 1918**. A v6 network is numbered out of a globally routable prefix delegated by an ISP, so there is no equivalent list to write down: `fc00::/7` catches unique-local addresses most networks do not use and misses the prefix a neighbour is actually on. A rule set that looks symmetrical and covers nothing is worse than a blanket drop, because it reads as handled. That argument, and the conditions under which it should be revisited, are recorded under "Rejected, with reasons".
This costs nothing that was being claimed. Internet egress stays unbounded and stays over IPv4, for the reasons already argued in `firewall.fw.template`. Rule 12 removes a second route to the internet and a first route to the LAN.
On the node rather than in the guest. `net.ipv6.conf.all.disable_ipv6=1` looks equivalent and is not: it is a setting on a machine this model assumes an attacker holds root on. It is also worse than merely ineffective, because it would make the audit's own check pass — the guest would be certifying its own containment on the strength of a setting the guest controls.
Also adds `OUT DROP -dest 100.64.0.0/10` as rule 11. A tailnet is numbered out of carrier-grade NAT space, which is none of the three RFC 1918 ranges, so a devbox on one reached every machine on the overlay. Worth adding even where no overlay exists today: it costs nothing, and the day someone installs Tailscale is not a day anyone revisits the firewall.
## The audit
A second check on the other address family, reported on its own line, failing the whole run if it does not pass. It asks `ff02::1` — the all-nodes link-local multicast address — who is on the link, and counts the replies that are not its own. A global or unique-local address configured here is a failure on its own and needs no probe, since it arrived by SLAAC or DHCPv6 and is therefore evidence rather than inference.
No positive control, unlike the v4 half. Both reasons a segment stays silent over IPv6 — dropped at the node, or never configured in the guest — are the outcome wanted, and no service a devbox needs lives on that family for the silence to be hiding one. `dejima-ci-hardener` argues the same way in `test/ci-runner.bats` for the same reason.
It does still report `INDETERMINATE`, for a different thing than the v4 half does: the probe not running at all. This was found by running the check in a container with no `ping` installed, where the first version returned "0 neighbours" and passed — nobody answered because nobody was asked. `dejima-devbox-bootstrap` now installs `iputils-ping` so that a bootstrapped host does not hit it, and a host set up before this change gets told which package to install.
`ff02::1` is not the target list the audit refuses to keep. It is the same address on every network there has ever been, so naming it describes no topology and leaks nothing about this one.
## Docs
- **`docs/threat-model.md`** gains a **Not bounded** section: the page's negative space collected in one list — exfiltration, container escape, root, the model API credential, the two LAN exceptions, inbound exposure, other people's work, history, the hypervisor. The page was organised entirely by control, which left a reader to assemble that themselves, and most will not.
- A section on the two address families, with the reasoning above.
- Two new entries under **Rejected, with reasons**: mirroring the DROPs into IPv6, and disabling IPv6 in the guest. Each names what would have to change to reopen it.
- **This page has a date on it**: a quarterly review cadence, matching dejima's, listing conditions to check rather than judgements to re-make. The argument applies harder at this layer than above it, since this is the layer that catches what the layer above fails to hold — and IPv6 was missing from the rule list for as long as the list existed, with nothing failing in the meantime.
- `proxmox/README.md` gains rules 11 and 12 in the runbook table, the reasoning, an `ip6tables -S` verification step (a v6 rule is absent from `iptables -S` whether or not it is there, so checking only the v4 table shows it missing either way), and an `ff02::1` probe in the by-hand check block.
- `docs/reference.md` documents the new check's three verdicts. `docs/building-a-devbox.md` §6 covers it in the verify step.
## Tested
`bats test/` — 114 passing, up from 109.
New coverage: a global v6 address failing without a probe; link-local alone not failing, since the kernel brings one up regardless; a neighbour answering with no v6 address configured here; our own multicast reply not counting as a neighbour; an unrunnable probe reading as `INDETERMINATE` rather than a pass; a v6 failure failing the whole reach check while a v4 `INDETERMINATE` survives as the more specific verdict; and the remedy text pointing at the node rather than the guest.
The parsing was also exercised end-to-end against recorded `ping` output including the `PING ... from ...` header, our own reply, and `(DUP!)` lines, confirming it counts two neighbours and excludes self.
`test/addresses.bats` caught `100.64.0.0/10` as an unexplained literal on the first run, which is the leak guard working — it is now on that allowlist with a comment saying what it is for.
## Not done here
The same gap exists in `dejima-ci-hardener`'s hypervisor template and is being handled separately. Its in-guest firewall already blocks IPv6 on the job network, which covers container-to-LAN but not the VM's own reach.
The node firewall's DROPs named IPv4 ranges only, so the control this
project exists to provide did not apply to IPv6 at all. That is open by
default on every network including ones with no IPv6 in them: every Linux
host brings up an fe80:: address on every interface whether or not anything
routes v6, and a daemon bound to :: answers on it. Neighbours were reachable
across the segment with all four DROPs in force, `ip -4 route` showed
nothing unusual, and `dejima-devbox-audit reach` reported PASS.
Dropped whole rather than enumerated, because IPv6 has no RFC 1918. A v6
network is numbered out of a globally routable delegated prefix, so no
equivalent range list can be written here - fc00::/7 catches ULAs most
networks do not use and misses the prefix a neighbour is actually on. On the
node rather than in the guest, for the same reason as every other rule here:
a sysctl is a setting root on the guest puts back.
Also drops 100.64.0.0/10. A tailnet is numbered there and is none of the
three RFC 1918 ranges, so a devbox on one reached every machine on the
overlay.
The audit gets a matching check: it asks ff02::1 who is on the link and
counts the replies that are not its own. No positive control, because both
reasons for v6 silence are the outcome wanted - unlike the v4 case, where a
working firewall and a broken network look identical. It does report
INDETERMINATE when the probe cannot run at all, which is why bootstrap now
installs iputils-ping; without it the check would have passed vacuously,
which is the one thing this program is built not to do.
Docs: the threat model gains a "Not bounded" list collecting the negative
space in one place, a section on the two address families, two new entries
under "Rejected, with reasons", and a quarterly review cadence with the
conditions to check rather than re-argue.
Tested with `bats test/` (114 passing) and by exercising the probe against
recorded ping output, including its own reply and DUP! lines.
The threat model and the runbook are descriptions of the current tool, not
a record of what moved. The IPv6 sections read as a changelog: what the rule
list used to name, what was reachable before, what the audit used to report.
That belongs in the pull request, which has it.
Same rules, same reasoning, stated in the present tense.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary generated by LLM agent
What this fixes
The node firewall's outbound DROPs named IPv4 ranges only, so the control this project exists to provide did not apply to IPv6 at all.
grep -i ipv6over the repository returned nothing.This is not the kind of gap where a control turns out weaker than advertised. It is the kind where it does not apply, on a path that is open by default on every network — including networks with no IPv6 anywhere in them. Every Linux host brings up an
fe80::address on every interface whether or not anything routes v6, and a daemon bound to::answers on it. So the machine beside a devbox was reachable across the segment with all four DROPs in force,ip -4 routeshowed nothing unusual, anddejima-devbox-audit reachreportedPASS. That last part is what makes it worth a fix rather than a note: the audit was reporting containment it had not checked, which is the failure the README's own three-verdict design exists to prevent.The rules
OUT DROP -dest ::/0, as rule 12 of the security group.Dropped whole rather than mirrored from the v4 list, because IPv6 has no RFC 1918. A v6 network is numbered out of a globally routable prefix delegated by an ISP, so there is no equivalent list to write down:
fc00::/7catches unique-local addresses most networks do not use and misses the prefix a neighbour is actually on. A rule set that looks symmetrical and covers nothing is worse than a blanket drop, because it reads as handled. That argument, and the conditions under which it should be revisited, are recorded under "Rejected, with reasons".This costs nothing that was being claimed. Internet egress stays unbounded and stays over IPv4, for the reasons already argued in
firewall.fw.template. Rule 12 removes a second route to the internet and a first route to the LAN.On the node rather than in the guest.
net.ipv6.conf.all.disable_ipv6=1looks equivalent and is not: it is a setting on a machine this model assumes an attacker holds root on. It is also worse than merely ineffective, because it would make the audit's own check pass — the guest would be certifying its own containment on the strength of a setting the guest controls.Also adds
OUT DROP -dest 100.64.0.0/10as rule 11. A tailnet is numbered out of carrier-grade NAT space, which is none of the three RFC 1918 ranges, so a devbox on one reached every machine on the overlay. Worth adding even where no overlay exists today: it costs nothing, and the day someone installs Tailscale is not a day anyone revisits the firewall.The audit
A second check on the other address family, reported on its own line, failing the whole run if it does not pass. It asks
ff02::1— the all-nodes link-local multicast address — who is on the link, and counts the replies that are not its own. A global or unique-local address configured here is a failure on its own and needs no probe, since it arrived by SLAAC or DHCPv6 and is therefore evidence rather than inference.No positive control, unlike the v4 half. Both reasons a segment stays silent over IPv6 — dropped at the node, or never configured in the guest — are the outcome wanted, and no service a devbox needs lives on that family for the silence to be hiding one.
dejima-ci-hardenerargues the same way intest/ci-runner.batsfor the same reason.It does still report
INDETERMINATE, for a different thing than the v4 half does: the probe not running at all. This was found by running the check in a container with nopinginstalled, where the first version returned "0 neighbours" and passed — nobody answered because nobody was asked.dejima-devbox-bootstrapnow installsiputils-pingso that a bootstrapped host does not hit it, and a host set up before this change gets told which package to install.ff02::1is not the target list the audit refuses to keep. It is the same address on every network there has ever been, so naming it describes no topology and leaks nothing about this one.Docs
docs/threat-model.mdgains a Not bounded section: the page's negative space collected in one list — exfiltration, container escape, root, the model API credential, the two LAN exceptions, inbound exposure, other people's work, history, the hypervisor. The page was organised entirely by control, which left a reader to assemble that themselves, and most will not.proxmox/README.mdgains rules 11 and 12 in the runbook table, the reasoning, anip6tables -Sverification step (a v6 rule is absent fromiptables -Swhether or not it is there, so checking only the v4 table shows it missing either way), and anff02::1probe in the by-hand check block.docs/reference.mddocuments the new check's three verdicts.docs/building-a-devbox.md§6 covers it in the verify step.Tested
bats test/— 114 passing, up from 109.New coverage: a global v6 address failing without a probe; link-local alone not failing, since the kernel brings one up regardless; a neighbour answering with no v6 address configured here; our own multicast reply not counting as a neighbour; an unrunnable probe reading as
INDETERMINATErather than a pass; a v6 failure failing the whole reach check while a v4INDETERMINATEsurvives as the more specific verdict; and the remedy text pointing at the node rather than the guest.The parsing was also exercised end-to-end against recorded
pingoutput including thePING ... from ...header, our own reply, and(DUP!)lines, confirming it counts two neighbours and excludes self.test/addresses.batscaught100.64.0.0/10as an unexplained literal on the first run, which is the leak guard working — it is now on that allowlist with a comment saying what it is for.Not done here
The same gap exists in
dejima-ci-hardener's hypervisor template and is being handled separately. Its in-guest firewall already blocks IPv6 on the job network, which covers container-to-LAN but not the VM's own reach.