97 lines
4.8 KiB
Markdown
97 lines
4.8 KiB
Markdown
---
|
|
status: resolved
|
|
opened: 2026-09-14
|
|
located-in: [mesh-controller (the derived filter)]
|
|
fixed-by: mesh-controller c8d8211 (forward chain drops by default, never closes ssh), 25e42b3; proven by one-node-mesh.test.ts (a machine filters exactly what its modules declared)
|
|
amended-design:
|
|
---
|
|
|
|
# 047 — The firewall does not cover published container ports
|
|
|
|
## Symptom
|
|
|
|
A machine running the mesh's firewall drops what it should drop and leaves every container port
|
|
published on all addresses open to anything that can reach the machine.
|
|
|
|
Rehearsed in the lab on three machines, one probing another, with a single change between the two
|
|
measurements:
|
|
|
|
| | ordinary port on the host | container port published on `0.0.0.0` |
|
|
|---|---|---|
|
|
| before the mesh's rules | reachable | reachable |
|
|
| after the mesh's rules | **refused** | **still reachable** |
|
|
|
|
Same prober, same ports, same second. The first column is the firewall working. The second is the
|
|
question.
|
|
|
|
## Why this is the way it is
|
|
|
|
It is deliberate, and the reasoning is written where the rules are generated: the mesh's table hooks
|
|
`input` and `output` and **no forward chain**, because what a machine forwards is the container
|
|
runtime's business — the runtime writes rules for the bridges it creates, and a second table
|
|
dropping in `forward` would drop container traffic the runtime explicitly allowed, stopping every
|
|
container on the node including the control plane.
|
|
|
|
That reasoning is sound. The conclusion drawn from it is not the only one available, because traffic
|
|
to a published port does not traverse `input` at all: it is redirected and then forwarded. So a
|
|
default-drop `input` chain says nothing whatsoever about it, and the firewall is silent on the ports
|
|
most likely to matter.
|
|
|
|
## Why it matters, measured rather than argued
|
|
|
|
The anchor this mesh will be installed onto publishes **38 distinct container ports on all
|
|
addresses** — a document store, three cache sentinels, mail transfer and retrieval, the forge's own
|
|
SSH, and thirty more.
|
|
|
|
Today those are filtered, by the system being replaced, which does it with rules that hook exactly
|
|
the path this one leaves alone. So the cutover from the old firewall to this one **opens all
|
|
thirty-eight at once, silently, on a machine with a public address**, while every check reports a
|
|
firewall that is up and dropping by default.
|
|
|
|
That is the worst shape a security fault can take: the mechanism is present, it reports success, and
|
|
the thing it is believed to be doing is not the thing it does.
|
|
|
|
## The other half, which runs the opposite way
|
|
|
|
The section above is about ports the firewall **cannot close**. There is a matching fault about
|
|
ports it **does** close and should not, and together they are worse than either alone.
|
|
|
|
The rules are computed from what modules declare they listen on. During a migration a mesh knows
|
|
about almost nothing — the services are still running under the system being replaced — so it opens
|
|
almost nothing. Anything listening **on the host** rather than in a container is then dropped.
|
|
|
|
**Including ssh.** No module in the catalogue declares an ssh port, and the generator has no
|
|
built-in allowance for one. So the ruleset loaded on a machine that has not been told otherwise
|
|
accepts established connections, loopback and ping, and refuses every new ssh connection.
|
|
|
|
The session doing the loading survives, because established connections are accepted. It survives
|
|
until it drops. On a machine reached over the network that is the difference between a mistake and
|
|
a journey to a rescue console.
|
|
|
|
So the two halves are:
|
|
|
|
| | what it does | consequence |
|
|
|---|---|---|
|
|
| container ports, published | cannot see them, does not filter | stay open, protection silently lost |
|
|
| host ports, undeclared | sees them, drops them | **ssh among them** |
|
|
|
|
The mesh gets no say over the ports most worth protecting, and full say over the one that must never
|
|
be closed by accident.
|
|
|
|
## How it would be checked
|
|
|
|
Two probes from another machine, against a port on the host and a published container port, before
|
|
and after the rules are loaded. Both must change together. The lab can do this — it is how the table
|
|
above was produced — and nothing does it today.
|
|
|
|
## Open questions
|
|
|
|
- Can the mesh filter forwarded traffic without owning the whole `forward` chain? A table that
|
|
accepts by default and drops only what the mesh knows about would not stop the runtime's traffic,
|
|
and hooking `forward` at all is the thing the current comment rejects.
|
|
- Should a module's declared reach apply to its published ports? A module that says it listens to
|
|
the mesh only, and is then reachable from everywhere because its port is published, is not doing
|
|
what its manifest says.
|
|
- Is the answer instead that the mesh should not publish on all addresses? A port bound to the
|
|
private network needs no filtering, and most of these want exactly that.
|