ADR 0226: assign the private network by its own name, let the proxy name its public issuer

Two modules existed only to say one thing each: networking required mesh-wireguard and nothing else,
and public-acme pointed the one proxy at Let's Encrypt. dhcpcd and cloudflare-dns are assigned
nowhere. Retire all four, keeping every machine's network and every certificate as they are.
This commit is contained in:
jochen
2026-10-06 02:21:15 +02:00
parent 202f2aa144
commit edad6acf49
8 changed files with 207 additions and 12 deletions
@@ -38,6 +38,11 @@ hand.
### What a domain module turns out to be, and why it is not the one refused above
> **Narrowed, not replaced — 2026-10-06, by [ADR 0226](0226-the-private-network-is-assigned-by-its-own-name-and-the-proxy-names-its-public-issuer.md).** The catalogue no longer
> holds a domain module: `networking` was retired, and every machine is assigned the private network's
> own module. A module with requirements and no files stays a thing a module may be; "why it is worth
> having" below describes what this record decided for `networking`, not what runs.
*Written 2026-08-29, from building it. The heading above reads as a contradiction of what now
exists and is not one — but only if the difference is stated, so it is stated here.*
@@ -9,6 +9,10 @@ extends: 0027-a-provision-names-what-the-consumer-is-coupled-to.md
# 44. A public name is provisioned, not registered by hand
> **The mechanism changed — 2026-10-06, by [ADR 0226](0226-the-private-network-is-assigned-by-its-own-name-and-the-proxy-names-its-public-issuer.md).** The interface stands; its one provider,
> `cloudflare-dns`, left the catalogue, assigned nowhere and required by nothing. A mesh that needs a
> public name made for it adds a provider of `public-dns` again.
## Context
The mesh names and resolves its own machines internally: the overlay generates
@@ -9,6 +9,10 @@ extends: 0110-a-seat-is-a-module-assignment-from-a-closed-set.md
# 117. A machine's uplink is a seat: the mesh configures the manager, never the link
> **The mechanism changed — 2026-10-06, by [ADR 0226](0226-the-private-network-is-assigned-by-its-own-name-and-the-proxy-names-its-public-issuer.md).** The seat stands; the catalogue holds two
> of its three modules. `dhcpcd`, held by no machine, left it. What this record says of dhcpcd is what
> a module for it must do, if one is written again.
## Context
The mesh installs on top of a machine's own networking. The private network's generator says
@@ -0,0 +1,155 @@
---
topic: the tiers
status: accepted
date: 2026-10-06
deciders: jochen
reconstructed: false
supersedes-in-part:
- 0009-modules-and-the-graph.md
extends: 02-DECISIONS/0007-connectivity.md
---
# 226. The private network is assigned by its own name, and the proxy names its public issuer
## Context
**The operator, 2026-10-06: "too many network-related modules … we simply have a private network,
some docker stuff, a firewall and a public and internal certificate resolver."** Counted on the
production mesh that day, from the controller's module listing and each machine's plan:
| module | on | what it is |
|---|---|---|
| `networking` | all four machines | requirements only: `private-network`, nothing else. Ships nothing |
| `mesh-wireguard` | on none directly; on all four through `networking` | the private network; its resources are computed by the controller |
| `public-acme` | the anchor and the home server | runs nothing; offers `acme-ca`, pointing at Let's Encrypt's production directory |
| `route-proxy` | the anchor and the home server | the public front door; the only consumer of `acme-ca` |
| `dhcpcd` | none | a third holder of `node-uplink`, for a machine whose link is dhcpcd's |
| `cloudflare-dns` | none | the only provider of `public-dns`; nothing in the catalogue requires `public-dns` |
**`networking` is the domain module [ADR 0009](0009-modules-and-the-graph.md) argued for in its
"what a domain module turns out to be" section**: a module holding nothing, so `assign networking`
finds one VPN and takes it, and choosing another is assigning that instead. It has never been more
than a second name for one module: one VPN exists, every machine runs it, and the name resolution it
once also required became a fact (ADR 0199). Its cost is real: every machine carries two modules for
one network, genesis assigns a module that is not the network, and the resolver's hint for a machine
off the network names the bundle rather than what it installs.
**`public-acme` is a provision with one consumer and one answer.** It was introduced so a lab without
a public issuer could answer `acme-ca` from `step-ca` instead (ADR 0066); `step-ca` now offers only
`internal-acme-ca`, and the mesh itself is the test bed (ADR 0149). What remained was a module
assigned beside every proxy to say "Let's Encrypt", and a pin when two answers stood on one machine
(issue 258).
**The proxy's account directory is named after the issuer as rendered.** `route-proxy` keeps each
authority's ACME account and certificates under `/var/lib/route-proxy/acme`, in a directory named by
a digest of the directory URL and of the root bundle its `trust` step copies in (the proxy's
`forThisAuthority`). The binding rendered the URL as `https://acme-v02.api.letsencrypt.org:443/directory`
— with the port — and the root as the system bundle of the pinned `trust` image. Spelled any other
way, the proxy sees a new authority, registers a new account and orders every routed name again. On
2026-10-06 that is 25 names under one registered domain on the home server, all issued on 2026-10-01,
and 26 on the anchor: a reissue of the home server's would reach Let's Encrypt's 50-per-week limit for that
domain inside the week.
## Considered Options
1. **The private network a default of every machine**, with no assignment. Rejected: "a machine is
on the private network because it was assigned the module" is what made the network a module at
all (connectivity §1, 2026-08-29); a default is a second path to the same state, and a machine
that should stay off would need an exception mechanism that does not exist.
2. **Keep `networking`, document it better.** Rejected: it answers a question — which VPN — that has
had one answer since it was written, and costs a module on every machine to do so. Choosing
another VPN is still assigning it; the bundle never added anything to that.
3. **Assign `mesh-wireguard` directly on every machine and retire the bundle.** Chosen.
4. **`route-proxy` provides `acme-ca` itself**, keeping the provision. Rejected: nothing else
consumes it, and a provision whose only consumer is its provider is a field, not an edge.
5. **The public issuer as a setting of `route-proxy`.** Rejected: a setting has no default (ADR
0112), so every mesh would need it set before the proxy resolves, and the one value that must not
change — the URL as rendered, port included — would become a value anybody can change with
`settings set`. Changing the public issuer of a live proxy is a reissue of every certificate it
holds; it should take an edit of the module and a decision, not a command.
6. **`route-proxy` states Let's Encrypt in its own `acme.env`, byte for byte what the binding
rendered, and `public-acme` retires.** Chosen.
## Decision
**1. The private network is assigned by its own name.** Every machine is assigned `mesh-wireguard`
directly. The `networking` module is retired: the controller no longer ships it, genesis assigns
`mesh-wireguard`, and the resolver's refusal for two machines that share no private network names
`mesh-wireguard`. Another VPN is still chosen by assigning it instead, and the node-scoped claim
`the-private-network` still refuses two. This supersedes ADR 0009's section *what a domain module
turns out to be*: the mechanism — a module with requirements and no files — stands, and the
catalogue no longer holds one.
**2. A module the controller stops shipping is retired at its next start, never from under a
machine.** `module forget` refuses a module the controller ships, because the next start would put
it back; so a retired one could be removed by nothing. At start the controller removes every module
it recorded as its own and no longer ships — unless a machine is still assigned it, or the mesh still
holds settings, secrets or ports for it, in which case it is kept and the start says why.
**3. The proxy names its public issuer.** `route-proxy` no longer requires `acme-ca`; its `acme.env`
states Let's Encrypt's production directory exactly as the binding rendered it. `public-acme` leaves
the catalogue and the mesh. The internal issuer stays a provision (`internal-acme-ca`, from
`step-ca`): it is a module that runs, held on one machine and consumed by every proxy and by
`ca-trust`. The proxy binary's own default stays Let's Encrypt *staging*, for anything that runs it
without the module.
**4. `dhcpcd` and `cloudflare-dns` leave the catalogue.** Neither is assigned anywhere; nothing
requires `public-dns`. `node-uplink` keeps two holders (NetworkManager, systemd-networkd), and the
`public-dns` interface of [ADR 0044](0044-a-public-name-is-provisioned-like-any-capability.md) has no
provider until a mesh needs one.
`avahi` and `netcheck` are not decided here.
## Consequences
- **The rollout changes no machine's network or certificates.** Assigning `mesh-wireguard` beside
`networking` and then unassigning `networking` leaves every machine's declaration of the private
network as it was; the proxy's `acme.env` is unchanged, so its `trust` step does not run again and
its account directory is the one it has. The one file that goes is the proxy's binding to
`acme-ca`, which nothing reads.
- **The order is assign, then unassign.** Unassigning `networking` first takes `mesh-wireguard` off
every machine that does not also run `dnsmasq` (which requires the mesh's addressing) at the next
push — the private network down. The controller's retirement refuses nothing here; it only keeps
the bundle while it is assigned.
- **The public issuer moves only with a plan for the account.** The proxy's account directory depends
on the URL as spelled and on the pinned `trust` image's root bundle. Moving that image's digest
reorders every certificate on both proxies; a test holds both values and says so.
- **The lab's beds that assigned `networking`, or pointed the proxy at `step-ca` for public names,
need changing before they run again.** A lab proxy now orders public names from Let's Encrypt's
production directory, so a bed that routes public names must not assign the module as it stands.
- **Genesis from an older host binary assigns `networking`, which a newer controller refuses.** The
host's change is merged before the controller's.
- **A mesh that wants another public issuer edits `route-proxy`**, rather than assigning a different
provider. That is deliberate (option 5).
## How it is checked
| Rule | Checked by |
|---|---|
| The controller ships one network module and no bundle; it resolves on its own | mesh-controller `internal/catalogue/provided_test.go` |
| A refusal for want of the private network names `mesh-wireguard` | the same file, `TestARefusalForWantOfThePrivateNetworkNamesItsModule` |
| Another VPN is chosen by assigning it, and WireGuard is not dragged in; two VPNs collide | the same file |
| A module no longer shipped is retired at start; kept while assigned or holding anything; a registered module is never touched | mesh-controller `internal/inventory/retire_test.go`, against a live database |
| Genesis assigns `mesh-wireguard`, never `networking` | mesh-host `internal/bootstrap/network_module_test.go` |
| `route-proxy` requires no `acme-ca`; its `acme.env` is byte for byte what the binding rendered; its `trust` image is the pinned one | mesh-controller `internal/catalogue/public_issuer_test.go` against the sibling catalogue |
| `public-acme`, `dhcpcd`, `cloudflare-dns` are not in the catalogue; nothing in it requires `acme-ca` or `public-dns` | the same file |
| Live: every machine still on the private network; every public name's certificate serial and expiry unchanged; no new file in a proxy's account directory | the rollout's checks, before and after each step: the controller's network view, `wg show` through each machine's tools, `openssl s_client` against every routed public name, and a listing of `/var/lib/route-proxy/acme` |
## References
- [ADR 0009](0009-modules-and-the-graph.md) — its section on a domain module is superseded; the rest
stands.
- [ADR 0007](0007-connectivity.md), [connectivity](../03-DESIGN/01-to-be/08-connectivity.md) §1 and §5,
amended alongside.
- [ADR 0066](0066-public-routing-is-name-agnostic.md) — the proxy requiring an issuer; the public one
is now its own fact.
- [ADR 0044](0044-a-public-name-is-provisioned-like-any-capability.md) — `public-dns`, without a
provider in the catalogue.
- [ADR 0117](0117-a-machines-uplink-is-a-seat.md) — `node-uplink`, now with two holders.
- [ADR 0112](0112-a-module-definition-names-no-node-mesh-or-path.md), [ADR 0149](0149-the-live-mesh-is-the-test-bed.md).
- [Issue 258](../04-ISSUES/258-every-machine-bound-the-resolver-to-itself/00-report.md) — the pin two
issuers on one machine needed.
- mesh-controller `internal/overlay/generator.go`, `cmd/mesh-controller/modules.go`, `stores.go`,
`internal/inventory/catalogue.go`, `internal/catalogue/resolve.go`; mesh-host
`internal/bootstrap/phase2.go`; mesh-catalog `modules/route-proxy`, and the removal of
`modules/public-acme`, `modules/dhcpcd`, `modules/cloudflare-dns`.
+1
View File
@@ -237,6 +237,7 @@ python3 00-META/checks/index.py fail if stale
- **0196** — [A node asks the mesh's resolver first, and a public one only when it is silent](0196-a-node-asks-the-meshs-resolver-first-and-a-public-one-only-when-it-is-silent.md)
- **0199** — [A module that answers names declares its zone, and a node's hosts file is one module's](0199-a-module-that-answers-names-declares-its-zone-and-a-nodes-hosts-file-is-one-modules.md)
- **0223** — [The mesh has two resolvers, and a machine lists only them](0223-the-mesh-has-two-resolvers-and-a-machine-lists-only-them.md)
- **0226** — [The private network is assigned by its own name, and the proxy names its public issuer](0226-the-private-network-is-assigned-by-its-own-name-and-the-proxy-names-its-public-issuer.md)
### What runs on them, and how it gets there