Compare commits

..
Author SHA1 Message Date
jschoubben 0b08cdfce1 Merge pull request 'ADR 0147: a module anchors the mesh's authority, and issue 146: the foundation cannot be raised' (#184) from decision/0147-a-module-anchors-the-meshs-authority into main 2026-09-29 14:06:41 +00:00
jschoubben bffd2af40c Issue 146 diagnosed: four faults stacked, three fixed, the fourth is genesis
Raising a first node hits them in order: the bundle's bus image named for a
registry that is gone; the certificate made by openssl in an image that has
none; enrolment dialling TLS at a bus that speaks first, then refusing its own
token for the empty bus. Each was right until the bus changed and nothing has
raised a foundation since.

The fourth is not a patch: the installer carries the bus's first user list and
the controller composes the rest through the bus being a module, and at genesis
there is no module — so the first node cannot be let onto the bus it just
raised.
2026-09-29 15:42:32 +02:00
jschoubben 4a51ea4b3a Issue 146: the foundation cannot be raised on the bus the mesh runs on
Found trying to run ADR 0147's bed. The older bundle raises the previous
broker and a control plane that refuses to start without MESH_BUS_NATS; the
newer one stops a step earlier, asking for a certificate from openssl in an
image that has none. No bed can run while this holds, so 0147's check section
now says what actually stands behind it — the rendering, not a machine.
2026-09-29 15:25:58 +02:00
jschoubben 6c14d313b8 ADR 0147: a module anchors the mesh's authority on a machine, and takes it away again
Issue 129: every internal HTTPS name fails verification on every machine,
because nothing has ever written the mesh's root into a trust store. The
report proposed the controller inject it the way the private network writes
the registry's trust; this record rejects that — reachability and trust are
not the same fact, and where anchors live is the host's difference, not the
controller's. A module requiring internal-acme-ca does the whole of it, and
being unassigned undoes it.
2026-09-29 15:02:37 +02:00
mesh-admin ced547dae9 Merge pull request 'ADR 0146: connectivity is checked by name, per hosting form' (#183) from decision/0146-connectivity-by-name into main 2026-09-29 12:01:57 +00:00
jschoubben 38482435af ADR 0146: connectivity is checked by name, per hosting form, with a valid certificate
0145's core stands — a module checks what the mesh claims, from where the callers
are — and everything it said about what to dial was wrong.

A raw port is not how anything in this mesh is reached. A real caller resolves a
name, the proxy answers, the proxy reaches the service; dialling a port tests the
last hop of a four-hop path and skips the three where most connectivity lives.

So each hosting form gets its own endpoint, route and name:
connect-docker.<node>.internal for a container, connect-process for the mesh's own
code in a unit it writes, connect-unit for a unit a package ships — and the same
under each public domain. Every machine checks every machine, by name, over TLS,
verifying the certificate against the authority that should have issued it. The
names are the instrument: a failure reads as connect-docker.g14.internal did not
answer.

No name is written anywhere: the machines come from the roster fact, the labels are
the module's, and a machine that joins is checked by the others on the next push.

Five things this needs that the mesh does not have, named rather than assumed —
a module every machine has (ScopeNode is exclusivity, not obligation), a container
running a bundle, a unit a package ships, the public domain in the roster fact, and
issue 129, until which every internal name will correctly fail verification.
2026-09-29 14:01:55 +02:00
mesh-admin cf8a8d78c9 Merge pull request 'ADR 0145: a module checks what the mesh claims is reachable' (#182) from decision/0145-a-module-checks-what-the-mesh-claims into main 2026-09-29 11:44:58 +00:00
jschoubben 9de25994e9 ADR 0145: a module checks what the mesh claims is reachable, and it checks itself
The mesh asserts three things are callable and has never checked any of them. A
module on every machine serves an endpoint of its own and dials every other
machine's, from the position the callers are in — not the host and not the control
plane, both of which reach those addresses by paths no ordinary caller uses and
would have passed throughout the outage.

Its probe is its own endpoint, declared reachable over the private network, so it
is admitted by exactly the rule that governs every internally-exposed service. The
tempting target is a service every machine has, and those are the ones never closed
— ssh above all — which would have passed for all eleven hours.

Resolution and connection reported separately, because they have different owners.
One failure is not a fault, and the count travels with the result. It reports and
repairs nothing.

Built on a scheduled container, a rendered roster fact and a bundle: no new
vocabulary. Adopted on its own merits, which is what 0143 was not.
2026-09-29 13:44:56 +02:00
mesh-admin 64ea47b11d Merge pull request 'ADR 0144: anything on a machine may call anything on it' (#181) from decision/0144-local-is-not-a-boundary into main 2026-09-29 11:32:03 +00:00
10 changed files with 619 additions and 6 deletions
@@ -0,0 +1,120 @@
---
topic: what runs on it
status: superseded
date: 2026-09-29
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0144-anything-on-a-machine-may-call-anything-on-it.md
superseded-by: 02-DECISIONS/0146-connectivity-is-checked-by-name-per-hosting-form.md
---
# 145. A module checks what the mesh claims is reachable, and it checks itself
## Context
The mesh asserts three things are callable ([ADR 0144](0144-anything-on-a-machine-may-call-anything-on-it.md)):
what runs on the same machine, another machine's service exposed to the private network, and another
machine's service exposed publicly. It has never checked any of them.
[Issue 145](../04-ISSUES/145-a-machine-reads-healthy-while-its-modules-cannot-reach-each-other/00-report.md):
the first of the three was broken for eleven hours and the mesh answered *all heard from, every module
current with its source* throughout. Every check it makes is about the relationship between the mesh and
a machine — applied, current, containers running — and none about whether anything can reach anything.
**A first answer was drafted and withdrawn.** [ADR 0143](0143-a-consumer-verifies-the-grant-it-is-given.md)
put the check inside the host, verifying each grant from the consumer's network position. It was
superseded because the difference it worked so hard to reproduce — whether a caller sat in a container —
was the bug itself. What survives from it is the part that was right: a check run from the wrong place
proves nothing, and the mesh's own reports are not evidence about the network.
**The mesh already has the shape for this and it is a module.** A module can declare a container that
runs on a cadence ([ADR 0053](0053-a-step-that-runs-on-a-schedule.md), and three modules already use
`*/5 * * * *`), can be given the mesh's roster as a rendered fact — every machine's name, address and
this node's own identity, the same mechanism the resolver and the operator's ssh configuration use — and
can emit what it found on the bus. Nothing new is needed to build this except the module.
**What it must not check is the trap.** The obvious probe target is ssh: present on every machine, never
closed by design. Dialling it would have passed throughout the outage, because ssh is admitted
unconditionally and the thing that broke was a service exposed to the private network. A checker whose
probe is unconditionally open measures the one path that cannot fail, which is the failure this whole
sequence keeps producing — a check that reads as verification and verifies nothing.
## Considered Options
1. **The host verifies each grant** ([ADR 0143](0143-a-consumer-verifies-the-grant-it-is-given.md)).
Superseded. It needed the host to act from another network position, which is machinery that exists
only while local calls are filtered wrongly.
2. **The control plane dials every node.** Rejected: it sits on one machine and reaches the others by a
path no ordinary caller uses. It would have passed throughout the outage.
3. **Probe an existing service.** Rejected for the target problem above: the services guaranteed on every
machine are the ones that are never closed, so they cannot fail the way the mesh fails.
4. **A module on every machine that serves its own probe and dials the others'.** Adopted.
## Decision
**A module runs on every machine, serves an endpoint of its own, and dials every other machine's.** The
probe is the module's own endpoint, declared reachable over the private network — so the thing being
dialled is admitted by exactly the rule that governs every other internally-exposed service, and fails
when that rule is wrong. A second endpoint, declared public, does the same for the public path where a
machine has one.
**It checks the three cases the mesh claims, by name:**
- its **own machine**, by dialling its own machine's address — the case that broke, and the only one that
distinguishes a caller on the machine from a caller in one of its containers;
- **each other machine over the private network**;
- **each machine's public path**, where one is recorded.
**It resolves before it dials, and says which failed.** A name that does not resolve and a port that does
not answer are different faults with different owners, and a checker that reports one sentence for both
sends a reader to the wrong place.
**It runs where the callers run.** The module's own code in its own container, on the cadence the mesh
already has, from the same position as every other module on that machine. It is not the host and not the
control plane, and that is the whole point.
**It says what it found and nothing else.** It emits results; it repairs nothing, opens nothing and holds
no credential beyond its own. A checker that fixes things is a second control plane.
**One failure is not a fault.** A machine rebooting is ordinary. A path is reported broken after it has
failed on consecutive runs, and the count travels with the result so a reader can tell "briefly away"
from "never worked" — the one thing [ADR 0143](0143-a-consumer-verifies-the-grant-it-is-given.md) got
right and worth keeping.
## Consequences
- **The mesh gains the ability to be wrong out loud about the network.** Eleven hours becomes two runs.
- **It is a module, so it is assigned, built, pushed and reported on like everything else** — no new host
capability, no new vocabulary, nothing in the control plane that has to know about checking.
- **Its own endpoint is the instrument.** That is what makes it able to fail; it also means the checker
must be assigned to a machine before that machine can be checked, and a machine without it is
unchecked rather than healthy.
- **It cannot check what it cannot be told.** The roster gives it machines; it does not give it every
module's endpoints, so this checks the paths the mesh claims and not every grant in the mesh. That is
the honest scope of a first one, and the difference is worth saying rather than growing quietly.
- **What got harder:** one more module on every machine, and a module whose whole purpose is to fail
visibly when something else is wrong. Its own failures will be read as the mesh's, which is the cost of
an instrument.
## How it is checked
- **It catches the measured outage.** A bed closes the path from a container to a service exposed to the
private network on its own machine — issue 145's shape — and the checker reports its own machine
unreachable while every other path still reads reachable. This fails against a probe on a port that is
never closed, which is the wrong target this record exists to name.
- **A machine rebooting is not a fault**: one failed run reports nothing, the count rises and falls.
- **A name that does not resolve is reported as that**, not as a port that did not answer.
- **It reports and does not act**: asserted by giving it a broken path and checking nothing on the machine
changed.
- **A machine without the module reads unchecked**, never healthy — asserted on what the mesh says about
a machine it is not assigned to.
## References
- [ADR 0144](0144-anything-on-a-machine-may-call-anything-on-it.md) — the three things that must be callable
- [ADR 0143](0143-a-consumer-verifies-the-grant-it-is-given.md) — superseded; what survives is that the
position matters
- [ADR 0053](0053-a-step-that-runs-on-a-schedule.md) — the cadence
- [ADR 0138](0138-an-assignment-binds-an-endpoint-and-says-how-far-it-reaches.md) — internal and public,
which the probe endpoints declare
- [issue 145](../04-ISSUES/145-a-machine-reads-healthy-while-its-modules-cannot-reach-each-other/00-report.md)
@@ -0,0 +1,125 @@
---
topic: what runs on it
status: accepted
date: 2026-09-29
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0145-a-module-checks-what-the-mesh-claims-is-reachable.md
supersedes: 02-DECISIONS/0145-a-module-checks-what-the-mesh-claims-is-reachable.md
---
# 146. Connectivity is checked by name, per hosting form, with a valid certificate
## Context
[ADR 0145](0145-a-module-checks-what-the-mesh-claims-is-reachable.md) decided that a module checks what
the mesh claims is reachable, from where the callers are, because the mesh reported four machines healthy
for eleven hours while a module could not reach its database
([issue 145](../04-ISSUES/145-a-machine-reads-healthy-while-its-modules-cannot-reach-each-other/00-report.md)).
That decision stands. What it got wrong is everything about *what* is dialled.
It dialled a raw port on each machine's address. Three things are wrong with that:
- **A raw port is not how anything in this mesh is reached.** A real caller resolves a name, the proxy
answers it, and the proxy reaches the service. A check that dials a port tests the last hop of a path
with four hops in it, and the three it skips — resolution, the proxy, the certificate — are where most
of the mesh's connectivity actually lives.
- **It tested one hosting form.** A module is software that delivers services, and it may deliver them
from a container, from a unit the mesh writes for its own code, or from a unit a package ships. Those
are three different paths to the same machine, and the outage that produced this was two of them
disagreeing. A probe served one way measures one way.
- **It said nothing about certificates.** An internal name that resolves, routes and answers over TLS
that nothing can verify is not a working path; it is a working path for whoever holds the proxy's
trust and nobody else.
## Decision
**Each hosting form gets its own endpoint, its own route and therefore its own name.** On every machine:
| name | what serves it |
|---|---|
| `connect-docker.<node>.internal` | a container |
| `connect-process.<node>.internal` | the mesh's own code, in a unit the mesh writes |
| `connect-unit.<node>.internal` | a unit a package ships |
and the same set under each machine's public domain where it has one — `connect-docker.<domain>` and its
siblings. The names are the instrument: a failure reads as *`connect-docker.g14.internal` did not answer*,
which says which machine and which hosting form without anybody interpreting anything.
**Every machine checks every machine, by name, over TLS, verifying the certificate.** Not a port, not an
address: resolve the name, connect, complete the handshake, check the certificate against the authority
that should have issued it — the mesh's own for an internal name, a public one for a public name. That is
the whole path a real caller takes, and each step failing is reported as itself.
**No name is written anywhere.** The machines come from the roster the mesh already renders as a fact, and
the labels are the module's. A machine that joins appears in every other machine's roster on the next
push, and they begin checking it without an edit.
**And the module arrives on a machine because the machine exists, not because somebody assigned it.** A
machine that joins and does not have it is worse than unchecked: every other machine is already dialling
its names, so it reads as broken everywhere until someone notices. This is the part the mesh cannot
currently express — see below — and it is the part that makes the rest safe.
**What survives from 0145**, unchanged: it reports and repairs nothing; one failure is not a fault and a
path is broken after consecutive runs with the count travelling with the result; findings are said on the
bus, because a finding in a file on the machine is what this exists to end; and the bus is the one path
that cannot report its own failure, so an emit that does not land is written locally and nowhere else.
## What this needs that the mesh does not have
Named here rather than assumed, because each is a decision of its own and this record is not the place to
make them:
1. **A module that every machine has.** `ScopeNode` means *at most one holder per node* — an exclusivity
rule, not an obligation — and nothing assigns a module at enrolment. Today the resolver, the packet
filter, ssh and intrusion prevention are each assigned per machine by hand, which is the same gap
wearing different clothes.
2. **A container running a module's own bundle.** A `process` runs the mesh's own compiled code with no
image; a `container` needs an image of the module's own, which means a Dockerfile — the thing the
`bundle` artifact exists to abolish. Nothing in the catalogue runs a bundle in a container, so
`connect-docker` has no shape yet.
3. **A unit a package ships, for `connect-unit`.** The `service` resource puts an existing unit into a
state and deliberately installs none, so this form needs a package that serves a port — and naming a
program the machine may not have is
[issue 136](../04-ISSUES/136-a-module-may-name-a-program-the-machine-does-not-have/00-report.md).
4. **A machine's public domain in the roster fact.** The fact carries each machine's name, mesh name,
address and operator account. The public names cannot be composed without the domain.
5. **Something that installs the mesh's own root on a machine.** This is
[issue 129](../04-ISSUES/129-nothing-makes-a-machine-trust-the-meshs-authority/00-report.md), open
since before any of this. Until it is closed, every internal name will fail certificate verification
from every machine — correctly, because nothing can verify it. That is the checker working, and it is
worth saying in advance so the first run is not read as the checker being broken.
## Consequences
- **A failure names the machine and the hosting form.** That is the whole gain over a port: eleven hours
became two runs under 0145, and under this it also becomes one line that says where to look.
- **The checker surfaces issue 129 immediately**, and will report every internal name unverifiable until
it is fixed. A reader must be told that before the first run rather than after.
- **Five things must be built before this is what it says it is**, and until they are, what exists is a
port dial from one position — useful, and not this.
- **What got harder:** a module with three hosting forms of the same trivial service is a strange thing to
read. It is justified only because those three forms are how the mesh actually runs software, and a
checker that tested one of them would keep the class of outage it exists to catch.
## How it is checked
- **A name per hosting form answers from every machine**, asserted by name and not by port.
- **A certificate that does not verify is reported as that**, distinctly from a name that does not resolve
and a port that does not answer — three faults, three owners.
- **A machine that joins is checked by every other machine without an edit**, asserted by adding one to a
bed and looking at what the others dial on their next run.
- **A machine that joins has the module**, which is gap 1 above and is the assertion that cannot be
written yet.
- **The measured outage is still caught**: the path from a container to a service on its own machine is
closed and `connect-docker.<that node>.internal` fails from that machine while the others still pass.
## References
- [ADR 0145](0145-a-module-checks-what-the-mesh-claims-is-reachable.md) — superseded; its core stands
- [ADR 0138](0138-an-assignment-binds-an-endpoint-and-says-how-far-it-reaches.md) — the two reaches these
names come from
- [ADR 0066](0066-public-routing-is-name-agnostic.md) — a label plus a domain, which is why no name is written
- [issue 129](../04-ISSUES/129-nothing-makes-a-machine-trust-the-meshs-authority/00-report.md) — what the
internal names will fail on until it is closed
- [issue 145](../04-ISSUES/145-a-machine-reads-healthy-while-its-modules-cannot-reach-each-other/00-report.md)
@@ -0,0 +1,139 @@
---
topic: what runs on it
status: accepted
date: 2026-09-29
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0098-a-fact-a-provider-makes-at-first-start-is-fetched-from-it.md
---
# 147. A module anchors the mesh's authority on a machine, and takes it away again
## Context
The mesh runs its own certificate authority and every internal name is served with a certificate
from it. No machine trusts it. On an enrolled, adopted workstation — on the private network,
resolving through the mesh's resolver — every internal HTTPS name fails verification with
*unable to get local issuer certificate*
([issue 129](../04-ISSUES/129-nothing-makes-a-machine-trust-the-meshs-authority/00-report.md)).
The certificates are genuine; nothing on the machine has ever been told what issued them.
The authority's only consumer today is a proxy, which fetches the root into a directory of its own
and hands it to one program ([ADR 0098](0098-a-fact-a-provider-makes-at-first-start-is-fetched-from-it.md)).
That is enough for the proxy and for nothing else: a browser, `git` over HTTPS, `curl`, a package
manager and every module that calls another module by an internal name read the machine's trust
store, which holds the predecessor's authority and a developer tool's local root, and nothing of
the mesh's.
The predecessor wrote its root into every machine it set up. Removing it was deliberate — an
honest failure beats a name that verifies for the wrong reason — and it leaves the mesh with no
answer at all until this one lands. It is also what keeps the predecessor alive on the machines
that still speak TLS to a mesh name.
**What makes this a decision rather than a patch** is where the knowledge goes. Two mechanisms in
the mesh already write things onto a machine because it is on the private network: `/etc/hosts`
and the registry's plaintext trust ([ADR 0082](0082-the-registry-is-reached-by-name-and-trusted-by-the-overlay.md)).
Following that precedent, the controller would inject an anchor into every such machine's
declaration, and issue 129 proposed exactly that. It would work. It would also put *where this
operating system keeps trust anchors* and *which command refreshes its bundles* into the control
plane, for a fact the control plane does not have (the root does not exist until the authority has
run) and a machine that may have no reason to verify a mesh name at all.
## Considered Options
1. **The controller injects the anchor into every machine on the private network**, the
`/etc/hosts` and insecure-registry shape. Rejected: being on the network is what makes the
registry reachable, and that is why network presence is the right trigger *there* — the trust
and the reachability are the same fact. Trusting an authority is not the same fact as being
able to reach it, and the anchor's path and the bundle refresh are a property of the machine's
operating system, which is the host's half of the mesh, not the controller's.
2. **A new host primitive — a `trust-anchor` resource type.** Rejected for now, not on principle.
The host's vocabulary should grow when a shape cannot be said with what exists, and this one
can: a file and a service already express it, as the packet filter proves
([ADR 0140](0140-the-filter-constrains-what-arrives-from-outside.md), whose module writes a
unit file and a service and nothing else). The primitive becomes right the moment a second
operating system is in play, because the anchor directory and the refresh command are exactly
the difference `internal/system` exists to hold. Until then it would be a vocabulary word with
one speaker.
3. **A module that requires the authority, fetches its root, installs it as an anchor and
refreshes the machine's bundles — and removes both when it is no longer assigned.** Adopted.
## Decision
**A machine trusts the mesh's authority because a module put its root there, and stops trusting it
when that module is taken away.**
1. **The module requires `internal-acme-ca`** and reads the provider's bound address and the path
it serves its root at. It requires nothing else and provides nothing: it is a consumer of the
authority like any other.
2. **It fetches the root over the mesh's own network, without prior trust**, because there is no
prior trust to have — this is the module that establishes it — and the network is what
authenticates the fetch ([ADR 0098](0098-a-fact-a-provider-makes-at-first-start-is-fetched-from-it.md),
the same reasoning that lets the proxy fetch it). What it accepts is checked: a body that is
not a certificate fails, and the failure is the module's, not a later handshake's.
3. **It installs the root where this machine's TLS clients look, and refreshes the extracted
bundles** — the command that does the refresh is an ordinary part of the unit that places the
anchor, not a new thing the mesh can be asked to do.
4. **Removal is symmetric and is the same unit's business.** Undeclared, the host stops the unit;
stopping it removes the anchor and refreshes the bundles again. A machine that leaves the mesh
stops trusting the mesh, without anybody remembering to go and look.
5. **It is an ordinary assignment.** No machine is given it automatically. A machine that verifies
a mesh name is assigned it, and a machine that does not is not — which is the same statement
the mesh already makes about every other module, and is why this is not the controller's
business.
**One operating system, said out loud.** The anchor directory and the refresh command in the
module today are Arch's. On a machine that is not Arch the unit fails, visibly, rather than
writing a file nothing reads. That is the accurate failure, and it is the signal that option 2
above has become right.
## How this is checked
- **The verification that could not succeed before.** On a machine holding the module, a plain
client fetches an internal HTTPS name with no `-k` and no bundle argument and verifies. On a
machine without it, the same fetch fails with *unable to get local issuer certificate*. Both
halves, because only the pair distinguishes "the anchor works" from "something else already
trusted it".
- **The removal half, in the same bed:** unassign the module, refetch, and the failure returns.
Checking only the arrival is how a trust store fills up with authorities nobody can account for.
- **What is deliberately not checked here:** that the authority issues, that a name resolves, that
the proxy serves. Those have their own beds, and this module's bed passing for those reasons is
the failure mode this record is most exposed to — which is why the negative half is not optional.
**What this bed is dialled at, and why it is the authority itself.** The authority serves its own
API with a certificate it issued, so the handshake under test needs nothing else in the mesh to be
right. A trust bed that reached for a routed name through the proxy would be passing or failing for
the proxy's reasons and the resolver's.
**Written, and not yet run** *(2026-09-29)*. The bed is `trust-anchor` in the lab, and it cannot
execute: raising a foundation fails before any module is reached, in both bundles that exist
([issue 146](../04-ISSUES/146-the-foundation-cannot-be-raised-on-the-bus-the-mesh-runs-on/00-report.md)).
So what stands behind this record today is the rendering — the script the machine would run names
the authority it was bound to, checked in the control plane's own test suite — and **not** a machine
that verified anything. That is a weaker thing than the paragraph above describes, and it stays
written this way until the bed runs.
## Consequences
The predecessor's authority can be retired from a machine once this module is assigned to it,
which is the first time that has been true. `git` over HTTPS to the mesh's forge starts working,
so the ssh-only clone URL stops being a rule. A module on any machine can call another module's
internal name and verify it.
What got harder: one more module to assign to a machine that needs it, and the machine's trust
store now changes when an assignment changes — which is the point, and is also a thing an operator
can be surprised by. The fetch without prior trust is the same exposure ADR 0098 accepted, now on
every machine that holds the module rather than only where a proxy runs: anything that can stand
in the middle of the mesh's own network at the moment of the fetch can be believed. The mesh
already treats that network as the thing it authenticates.
## References
- [issue 129](../04-ISSUES/129-nothing-makes-a-machine-trust-the-meshs-authority/00-report.md) —
the symptom and the evidence.
- [ADR 0098](0098-a-fact-a-provider-makes-at-first-start-is-fetched-from-it.md) — a fact made at
first start is fetched from its provider; this extends it from one program to the machine.
- [ADR 0082](0082-the-registry-is-reached-by-name-and-trusted-by-the-overlay.md) — the precedent
this deliberately does not follow, and why it is right where it is.
- [ADR 0005](0005-the-node-host.md) — the host is where one operating system's difference lives.
- [`03-DESIGN/01-to-be/08-connectivity.md`](../03-DESIGN/01-to-be/08-connectivity.md).
+3
View File
@@ -225,6 +225,9 @@ python3 00-META/checks/index.py fail if stale
- **0141** — [The host delivers its own successor, and versions live side by side](0141-the-host-delivers-its-own-successor.md)
- **0143** — [A consumer verifies the grant it is given](0143-a-consumer-verifies-the-grant-it-is-given.md) *(superseded)*
- **0144** — [Anything on a machine may call anything on it, and that is the whole of "local"](0144-anything-on-a-machine-may-call-anything-on-it.md)
- **0145** — [A module checks what the mesh claims is reachable, and it checks itself](0145-a-module-checks-what-the-mesh-claims-is-reachable.md) *(superseded)*
- **0146** — [Connectivity is checked by name, per hosting form, with a valid certificate](0146-connectivity-is-checked-by-name-per-hosting-form.md)
- **0147** — [A module anchors the mesh's authority on a machine, and takes it away again](0147-a-module-anchors-the-meshs-authority.md)
### How it is built
+18 -1
View File
@@ -7,8 +7,9 @@ code:
- mesh-controller internal/identity/authority.go
- mesh-host internal/identity/serving.go
- mesh-host internal/apply (the service that reflects a rule set)
updated: 2026-09-28
updated: 2026-09-29
decisions:
- 02-DECISIONS/0147-a-module-anchors-the-meshs-authority.md
- 02-DECISIONS/0138-an-assignment-binds-an-endpoint-and-says-how-far-it-reaches.md
- 02-DECISIONS/0140-the-filter-constrains-what-arrives-from-outside.md
- 02-DECISIONS/0104-a-provision-may-be-answered-by-an-adapter-to-the-predecessor.md
@@ -710,6 +711,22 @@ step, so when the authority moves the root is fetched again and the proxy is rec
*How it is checked:* the route-forwarding bed installs the authority, the proxy and a consumer
from the catalogue and asserts the routed name is served.
**And a machine trusts that authority because a module put its root in its trust store**
([ADR 0147](../../02-DECISIONS/0147-a-module-anchors-the-meshs-authority.md)). The proxy's fetch
answers for the proxy and for nothing else: a browser, `git` over HTTPS, a package manager and
every module calling another by an internal name read the machine's own trust store, and the mesh
had never written anything there. A module requiring the authority does the whole of it — fetch
the root over the mesh network, place it where this machine's TLS clients look, refresh the
extracted bundles — and stopping it, which is what being unassigned does, takes the anchor away
and refreshes them again. Not the controller's business, because being on the private network is
what makes the authority *reachable* and is not the same fact as having a reason to *verify* a
mesh name; and because where anchors live and which command refreshes them is one operating
system's difference, which is the host's half of the mesh
([ADR 0005](../../02-DECISIONS/0005-the-node-host.md)).
*How it is checked:* on a machine holding the module a plain client verifies an internal HTTPS
name with no bundle argument, and on one without it the same fetch fails to find an issuer — both
halves, because only the pair tells the anchor apart from something that already trusted it.
### What was built
*2026-08-31.*
@@ -1,7 +1,7 @@
---
status: open
status: located
opened: 2026-09-26
located-in: [mesh-controller, mesh-catalog step-ca]
located-in: [mesh-catalog ca-trust]
---
# 129 — nothing makes a machine trust the mesh's own certificate authority
@@ -0,0 +1,32 @@
# Diagnosis
*2026-09-29.*
## What was ruled out
**That something already carries the root and it is only misplaced.** It does not. The authority
serves its root at a path beside its ACME directory, and the one thing that fetches it — the route
proxy — puts it in a directory of its own and hands it to one program. Nothing has ever written
into a machine's trust store. Measured on three converged machines: the anchors present are the
predecessor's authority and a developer tool's local root, and on the machines where the
predecessor's was deliberately removed, every internal name fails verification.
**That the private network could carry it, the way it carries the registry's trust.** That is what
the report proposed, and it was rejected on consideration rather than on difficulty
([ADR 0147](../../02-DECISIONS/0147-a-module-anchors-the-meshs-authority.md), option 1): being on
the network is what makes the registry *reachable* and is therefore the right trigger there, while
trusting an authority is a separate fact from being able to reach it. The anchor's directory and
the command that refreshes the extracted bundles are also one operating system's difference, which
is the host's half of the mesh and not the controller's.
**That it needs a new host resource type.** It does not, today. A file and a service say the whole
of it, which the packet filter already proves. The primitive becomes the right answer when a second
operating system is in play, and not before.
## Where it belongs
A module in the catalogue: it requires `internal-acme-ca`, fetches the root over the mesh's own
network, installs it as a trust anchor, refreshes the machine's bundles, and — because being
unassigned stops its unit, and stopping the unit is what undoes it — takes both away again.
The owner is therefore `mesh-catalog`, module `ca-trust`, and nothing in the control plane.
@@ -90,9 +90,17 @@ same machine, another machine's service exposed to the private network, and anot
exposed publicly. The filter had the second and third and expressed the first as a list of addresses that
no container could match.
**The question this issue is actually about is still open.** Nothing in the mesh would have said a grant
had stopped working, and nothing does now. That is not answered by a configuration fix, and it should not
be answered by a mechanism adopted as a remedy for one.
**And then the question this issue is actually about was answered on its own terms.**
[ADR 0145](../../02-DECISIONS/0145-a-module-checks-what-the-mesh-claims-is-reachable.md): a module on
every machine serves an endpoint of its own and dials every other machine's, from the position the
callers are in. Its probe is its own endpoint declared reachable over the private network, so it is
admitted by exactly the rule that governs every internally-exposed service and fails when that rule is
wrong — where a probe on a service every machine has would have passed for all eleven hours, because the
services every machine has are the ones never closed.
Adopted on its merits rather than as the remedy for a configuration error, which is what 0143 was and
why it went. The module is written and merged; it is not yet assigned, so every machine currently reads
unchecked.
## Open questions
@@ -0,0 +1,73 @@
---
status: located
opened: 2026-09-29
located-in: [mesh-host examples + internal/link, mesh-controller internal/broker]
---
# 146 — the foundation cannot be raised on the bus the mesh runs on
## What was observed
Raising a first node in the lab, to check a module against a real mesh, fails before any module is
reached. Two separate faults, in the two bundles that exist:
**The older bundle raises a control plane that cannot start.** It brings up the previous broker,
and the control plane it then starts says, once every few seconds, for ever:
```
mesh-controller: this control plane has no MESH_BUS_NATS, so it cannot reach the mesh's bus
```
That is the control plane being right. The mesh moved to one bus
([ADR 0131](../../02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.md)) and the
bundle did not. Every bed that raises a foundation raises this one, so every bed is in this state.
**The newer bundle, written for the new bus, stops one step earlier.** Its certificate step asks a
container to make the broker's certificate:
```
docker run --rm --entrypoint sh -v <the broker's tls volume>:/tls <the bus image> \
-c "test -f /tls/tls.crt || (openssl req -x509 ... )"
...
failed bus-certificate: running the action: docker exited 127
```
127 is *command not found*. The bus's image has a shell and no `openssl`; the previous broker's
image had both, which is why the step worked when it was written against that one. Substituting the
store's image — the only other image the bundle carries — does not help: it has no `openssl`
either. So the step as written cannot succeed with anything the bundle names, and the fault is not
one image's: **the bundle asks for a certificate to be made by a tool it never says must be there.**
Measured 2026-09-29 on a fresh lab machine, both bundles, from bare.
## Why this is here and not a note in the knowledge base
The mesh's own foundation is the one thing it cannot raise. Nothing reports that: the bundles are
files in a repository, nothing applies them but a person raising a node, and the last thing that
did was the hand-driven cut-over
([ADR 0131](../../02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.md), whose
work was done on the machines rather than from a bundle). So the state where the mesh cannot make
another one of itself is reachable, and was reached, without anything saying so.
It is also load-bearing for everything else: a lab bed proves a claim by raising a mesh, so while
this holds, **no bed can run**, and every "checked in the lab" written from now on is a promise
against a suite nobody can execute.
## What would have prevented it
- **Something raising the foundation on a schedule, from the bundle, as it is written** — the
bundle is the mesh's own installer and nothing installs from it. A bed that raises a first node
is exactly that check, and it is the bed that cannot run.
- **A step naming what it needs.** The certificate step names an image and assumes a program inside
it. An action that said which tool it requires would have failed at the declaration rather than
at 127 on a machine.
## Evidence to carry into diagnosis
- `mesh-host examples/foundation-first-node.lock` — the previous broker, no `MESH_BUS_NATS`.
- `mesh-host examples/foundation-first-node-nats.lock` — the new bus; `bus-certificate` and its
`verify` both run `openssl` in the bus's image.
- The bus image the bundle pins has `sh` and no `openssl`; the store's image likewise.
- The lab rewrites a bundle's registry-prefixed third-party references to upstream ones for a
machine with an uplink (`test/integration/harness.ts`); the new bundle's bus reference needed
that rule added, which is done and is not this issue.
@@ -0,0 +1,96 @@
# Diagnosis
*2026-09-29, by raising a first node in the lab over and over and writing down each thing it hit.*
Not one fault. **Four, stacked**, each hidden behind the one before it, and every one of them the
same shape: a step that was right while the mesh ran on the previous broker and was never asked a
question again after the bus changed. Nothing had raised a foundation since, so nothing said so.
## 1 — the bundle's bus image is named for a registry that is gone *(fixed)*
The newer bundle pins `<a lab registry>/nats@…`, which resolves nowhere outside the lab that
raised that registry. The lab already rewrites the store's and the previous broker's references to
upstream ones for a machine with an uplink; the bus had no such rule because no bed had ever tried
to raise this bundle. Added (`mesh-lab test/integration/harness.ts`). The digest is the bundle's
own — what the registry served was a copy, so the same digest resolves upstream, and this is a
prefix being removed rather than a reference being replaced.
## 2 — the bus's certificate was made by a tool the bus does not have *(fixed)*
```
failed bus-certificate … docker exited 127
```
The step ran `openssl` inside the broker's image. The previous broker's image carried it; the bus's
does not — it is Alpine with a shell and no `openssl` — and neither does any other image the bundle
names, so there was nothing to substitute. **The program that needs the certificate now makes it**:
`mesh-controller broker certificate --into <dir>`, with `--check` as the step's verify. The
controller is already on the machine at that point (the schema step ran it) and needs nothing from
the image it writes into. Self-signed, as before and on purpose — a host pins this server's exact
certificate ([ADR 0004](../../02-DECISIONS/0004-a-node-and-how-it-joins.md)) and at that moment
there is no authority to ask. Idempotent, because a second certificate is one every host that
pinned the first no longer believes. It runs `--user 0:0`: the volume is root's, and the control
plane's image runs as nobody, which is right for the long-lived server and wrong for a one-shot
writing into a fresh volume.
## 3 — enrolment dialled TLS at a bus that speaks first *(fixed)*
```
mesh-host: cannot reach the broker at …:5671: tls: first record does not look like a TLS handshake
```
Enrolment opened a raw TLS connection to check the pinned certificate before saying anything. NATS
speaks its own protocol and upgrades afterwards, so the handshake met a plaintext greeting. The pin
was never the problem: the same pinned configuration is handed to the client that presents the
token, and the verification runs inside *that* handshake — so what
[ADR 0004](../../02-DECISIONS/0004-a-node-and-how-it-joins.md) requires still holds, and holds
better, because the one-time secret is sent only after the certificate has been checked. The raw
dial is gone from the enrolment path and kept only as what its tests always proved: that a wrong
certificate is refused before a byte of application data is sent.
**Then, immediately behind it:**
```
mesh-host: this token is for the "" bus, and the mesh's bus is nats
```
The enrolment left the transport empty and meant *whatever the mesh runs today*, which was true
while two buses existed and became a refusal the moment one did. The host knows which bus the mesh
runs; it says so now.
## 4 — a first node cannot be let onto its own bus *(open, and this is the real one)*
```
mesh-host: cannot reach the bus at …:5671 as anchor: nats: Authorization Violation
```
The bus's user list is a file beside its configuration. The installer carries the first one — the
controller's own account at a bootstrap password — and **the controller composes every user after
that** (design 25 §6; the controller's own test asserts the carried list matches what it would
derive). On the running mesh that composition reaches the bus because the bus is a *module*, with
the list delivered to it the way anything is delivered to a module.
At genesis there is no module. The foundation's bus is raised by the installer, the control plane is
given no way to write beside its configuration — it mounts the certificate and nothing else — and so
the account a joining node needs cannot come into existence. **The first node cannot join the mesh
it just raised.**
That is not a line to fix in a bundle. It is the open half of the mesh delivering its own components
([ADR 0142](../../02-DECISIONS/0142-the-mesh-delivers-its-own-components-as-binaries.md)) and of the
bus becoming a module: either the installer's bus is raised as the module the mesh will go on
managing, or genesis carries a user list that includes the first node's enrolment and the controller
takes over from there. Both are decisions, not patches, and both belong to the genesis step that was
deliberately left until last.
## Where it belongs
`mesh-host` (the bundle and the enrolment path) and `mesh-controller` (the certificate command, and
the composition that cannot reach the bus at genesis). Three of the four are fixed on branches; the
fourth is the genesis work.
## What it cost, for the next person
Every lab bed still names `foundation-first-node.lock` in its own instructions, and that bundle
raises the previous broker with a control plane that refuses to start without `MESH_BUS_NATS`. Until
the fourth fault is answered and the two bundles become one, a bed runs with `MESH_LAB_BUNDLE`
pointing at the NATS bundle by hand, and stops at the enrolment.