Issues 239, 240, and 238's diagnosis: a module name taken over, a dry run recorded, the operator banned

239: two repositories defined photos; a rebuild to the catalogue's main replaced the app with a stub
and nothing refused it. 240: a dry-run build was recorded and its definition reached the machine.
238: the forge refused three ssh logins as the operator's account in 31 seconds; nothing in the mesh's
ssh configuration names the forge, and no jail ignores the mesh's own public addresses.
This commit is contained in:
2026-10-04 17:31:29 +02:00
parent e5042e1e7a
commit 33c10aa85a
3 changed files with 128 additions and 0 deletions
@@ -0,0 +1,43 @@
# 238 — Diagnosis
## 2026-10-04, from the control node
**What the forge refused, from the operator's uplink, in the ban's last minute** (the forge's ssh log):
```
14:05:28 Invalid user jochen from <uplink> port 38564
14:05:31 Accepted publickey for git from <uplink> port 45156 (the laptop's key)
14:05:44 Invalid user jochen from <uplink> port 52074
14:05:59 Invalid user jochen from <uplink> port 37978
```
Three refusals in thirty-one seconds — `maxretry = 3` — and the `gitea` jail banned the uplink at
14:06:00; `recidive` counted it the same second. Between the refusals the same key logged in as `git`:
the agent's own git operations were fine, and the refusals were ssh commands that named no user.
**Why they named the wrong user.** On the laptop and the workstation, `ssh -G <forge's public name>`
resolves to the operator's account and port 22: nothing in the ssh configuration the mesh writes
(`ssh-client`, to-be 29) names the forge. A bare `ssh <forge>` — or a git URL without `git@` — presents
the login name, which the forge does not have.
**Why the operator's uplink is bannable at all.** The jails' `ignoreip` (the `fail2ban` module's
`jail.local`) holds loopback, the mesh's private range and every private range (ADR 0186). A node at
home reaches the control node from the home's public address, which is none of those. Nothing the
mesh knows puts it there.
## What would have stopped it
1. **The forge in the ssh configuration the mesh writes**: a `Host` block for the forge's public and
internal names with `User git` and the forge's ssh port. It cannot be written today: the `git`
provision serves the forge's http port only, and the controller translates a served `port` to the
machine's published port but no other key (`ServedOn`), so an `ssh-port` would reach a consumer as
the container's 22, not the machine's 222.
2. **The mesh's own public addresses in every jail's ignore list.** Two sources: each node's public
egress as the hub sees it (the tunnel's peer endpoints — every node at home shows the uplink there),
or an operator setting naming them. The first is derived and stays true when the uplink changes;
the second is a value somebody must remember to edit.
## Status
Not located further: both remedies are design choices — (1) a served port the controller translates
by listen, (2) the open question 1 of the report.
@@ -0,0 +1,48 @@
---
status: open
opened: 2026-10-04
located-in: []
fixed-by:
amended-design:
---
# 239 — A module name is taken over by another repository, and nothing refuses it
## What was observed
2026-10-04. Five of the photo app's six public names stopped answering on the control node — the API,
two client sites and two aliases — while the sixth answered with a different program. Nothing failed:
the controller composed, the host applied, every check passed.
Two definitions held the module name `photos`:
| | the app's own repository | the catalogue's `modules/photos` |
|---|---|---|
| built from | `photos.git`, branch `nox-mesh`, until 2026-09-28 | from 2026-10-04 04:14 |
| containers | server, admin, two client sites | server, an admin client |
| routes | six | one |
Rebuilding `photos` "to `main`" for [ADR 0202](../../02-DECISIONS/0202-a-provider-declares-what-it-derives-for-each-consumer.md)
(see [issue 227](../227-the-photo-apps-admin-client-asks-for-the-port-the-proxy-holds/00-report.md)) built the
catalogue's main, not the repository the module had been built from. The controller recorded the new
source, the module moved, the next push replaced the app with the stub, and two sites' containers and
five routes went with it. The stub had sat in the catalogue since 2026-09-03 without ever being the
running definition.
Restored the same day by building from `photos.git` again and removing the stub (novox/photos#3,
mesh-catalog#278).
## Why it is an issue and not an incident
**A module's source is a fact the mesh records, and any build may overwrite it.** The build history
showed the switch plainly — `photos.git at nox-mesh` on one line, `mesh-catalog.git at main` on the
next — and nothing asked whether a module built from one repository should now come from another.
"The last build wins" is the rule in practice; it is written nowhere, and it lets a stale or unrelated
definition replace a working one silently.
## Open questions
1. Should a build whose source differs from the module's recorded source be refused unless it says so
explicitly (a `--move-source`, or the operator's confirmation)?
2. Should the catalogue's check refuse a module whose name another registered repository already
defines?
@@ -0,0 +1,37 @@
---
status: open
opened: 2026-10-04
located-in: []
fixed-by:
amended-design:
---
# 240 — A dry-run build is recorded, and what it built is applied
## What was observed
2026-10-04, restoring the photo app ([issue 239](../239-a-module-name-is-taken-over-by-another-repository-and-nothing-refuses/00-report.md)).
`mesh-controller build <repository> --ref <unmerged branch> --dry-run` was run to prove the branch built
before it was merged. The command's help says *"build and print the manifest, recording nothing"*.
Afterwards:
- `builds photos` listed the dry run as a build — `photos.git at <the unmerged branch>`, with its three
images — beside the real ones.
- On the control node, the module's state directory was rewritten at a time between the dry run and
the merged build: the secret files and environment files the branch's definition declares, which the
definition then running did not.
The content happened to equal what was merged a few minutes later, so nothing broke. Had the branch
been rejected in review, its definition would already have been on the machine.
## Why it matters
A dry run is how a change is proven before a person approves it. If it records the build and the mesh
acts on it, review becomes a formality: the unreviewed definition reaches a machine first.
## Open questions
1. Where does the dry run's outcome enter the record — the build machine's `built` event, consumed as
any other build's?
2. Did the controller roll the dry run out, or did a later push compose from it?