diff --git a/02-DECISIONS/0049-a-public-name-is-provisioned-like-any-capability.md b/02-DECISIONS/0049-a-public-name-is-provisioned-like-any-capability.md new file mode 100644 index 0000000..f0eefcb --- /dev/null +++ b/02-DECISIONS/0049-a-public-name-is-provisioned-like-any-capability.md @@ -0,0 +1,93 @@ +--- +status: proposed +date: 2026-09-04 +deciders: jochen +reconstructed: false +extends: 0005-capabilities-are-provisioned-on-declaration.md +--- + +# 49. A public name is provisioned, not registered by hand + +## Context + +The mesh names and resolves its own machines internally: the overlay generates +`..` wildcards, dnsmasq answers them (`wildcard-resolution`), and the mesh +issues a certificate for each internal name. A service reachable at a *public* domain — +`plex.example.com`, not `plex.anchor.internal` — needs three things that machinery does not give it: + +- a **public DNS record** at a registrar or DNS provider, so the name resolves on the internet; +- a **publicly-trusted certificate** for it, because the mesh's own authority is trusted by nobody + outside the mesh; +- and routing from that name to the module — which the reverse proxy already does: a module + `requires` the `route` capability and the proxy provides it, routing by the host it was asked for. + +The routing exists. The public DNS record does not: the mesh has no way to make a name resolve on +the public internet, so today that is a step someone does by hand at a DNS provider, outside the +mesh, remembered nowhere. A public name is therefore the one part of reaching a service that the +declaration graph cannot grant or withdraw — which means it is created once and outlives whatever it +was for, the shape of drift this project exists to remove. + +## Decision + +### A public name is a capability, requested like any other + +A module reachable at a public host declares `requires: ["public-dns"]` and contributes the hostname +it wants — beside `requires: ["route"]`, which exposes it through the proxy. The name is then +provisioned on declaration ([ADR 0005](0005-capabilities-are-provisioned-on-declaration.md)): created +when the module is assigned, removed when it is withdrawn, reconciled like every provision. + +### The interface is neutral; the providers are the registrars + +`public-dns` is drawn at the consumer's coupling: the consumer wants *a public name that resolves to +me*, and does not care whether Cloudflare, Route 53 or a registrar's own API puts the record there. +So the interface is neutral and the providers are provider-scoped — `cloudflare-dns`, +`route53-dns`, `porkbun-dns` — each implementing the one `public-dns` contract, the same way a +neutral database coupling is answered by `postgres-database` and `mssql-database`. A module names +`public-dns`; it never names a registrar. + +### The record points at the mesh's public ingress, not at the node + +What the name resolves to is the address the reverse proxy answers on, not the consuming machine's. +A public service is reachable only *through* the proxy — the proxy holds the `route` grant and routes +by host to the module — so the public name must resolve to the proxy. `public-dns` and `route` are +the two halves of one public exposure: the name, and what the name reaches. + +### The record is a fact, not a secret + +A DNS record is public by definition, so the grant returns the fully-qualified name and its TTL and +nothing sealed. The only secret is the provider's own API credential, which is the provider module's +own-secret and never leaves it — the module that wanted the name never sees it. + +### Events + +The provider emits `module..record.created` and `module..record.removed` +([ADR 0046](0046-events-are-a-relationship.md)), so *which names the mesh publishes, and where* is a +question answered from the event trail and the grants, not from a folder of records edited at a +provider. + +### The public certificate is the proxy's, and is named here only to pair it + +A public name without a publicly-trusted certificate is reachable and not trusted — the same pairing +the internal name and the mesh-issued certificate already have. Obtaining that certificate (ACME +against the now-resolving public name) is the reverse proxy's to do, and its mechanism is its own +decision; it is named here so the pairing is not forgotten, not resolved here. + +## Consequences + +- A public name is created and torn down with the module, so it cannot outlive it, and the mesh can + say which public names it publishes without anyone reading a registrar's dashboard. +- Adding a registrar is adding a provider that answers `public-dns`; the modules that want names do + not change. +- Public exposure of a service is a trio of separate, declared, enforced relationships: the firewall + opens the proxy's public port ([ADR 0050](0050-a-machine-firewall-is-the-sum-of-what-it-listens-on.md)), + `route` routes the host to the module, and `public-dns` makes the host resolve. + +## References + +- [ADR 0005](0005-capabilities-are-provisioned-on-declaration.md) — a capability is provisioned on + declaration; a public name is one. +- [ADR 0050](0050-a-machine-firewall-is-the-sum-of-what-it-listens-on.md) — the firewall, the other + half of the reachability question this was asked with. +- [ADR 0046](0046-events-are-a-relationship.md) — the provider's record events. +- [ADR 0045](0045-what-a-module-is.md) — a provider and its interface; the neutral-interface, + scoped-provider naming this follows. diff --git a/02-DECISIONS/0050-a-machine-firewall-is-the-sum-of-what-it-listens-on.md b/02-DECISIONS/0050-a-machine-firewall-is-the-sum-of-what-it-listens-on.md new file mode 100644 index 0000000..412d159 --- /dev/null +++ b/02-DECISIONS/0050-a-machine-firewall-is-the-sum-of-what-it-listens-on.md @@ -0,0 +1,92 @@ +--- +status: proposed +date: 2026-09-04 +deciders: jochen +reconstructed: false +extends: 0037-the-host-applies-it-does-not-decide.md +--- + +# 50. A machine's firewall is the sum of what its modules listen on + +## Context + +The reverse proxy is a *provider*: a module `requires` the `route` capability and a running proxy +provides it, routing traffic by name and reaching back to the consumer. A fair question follows — +is the firewall the same shape? Should a module *register* a port with a firewall provider the way +it requests a route? + +It should not, and the difference is the point. A reverse proxy is a service another component +performs; a firewall is a property of the machine — a packet filter the host applies to itself. +Modelling it as a provider would invent a credential and a reach-back for something that has neither. + +And the mesh already has the registration: a module declares `listens: [{ port, from }]` — the port +it accepts connections on, and from where. That *is* how a service says it wants a port open. What is +missing is not a model but enforcement. [04-ISSUES/003](../04-ISSUES/003-firewall-scope-is-read-by-no-code/00-report.md) +records that a `scope:` key five manifests carry is read by no code: a manifest can appear to +restrict a port and restrict nothing — the exact fault +[how-we-build.md](../00-META/how-we-build.md) names, *an unenforced rule is indistinguishable from a +wrong one*, made worse because the declaration reads as a restriction. + +## Decision + +### The firewall is derived and host-applied, not a provider + +A machine's firewall is the sum of what the modules assigned to it declare they listen on, computed +by the host and applied as one of its owned resources ([ADR 0037](0037-the-host-applies-it-does-not-decide.md): +the host applies, it does not decide; [ADR 0043](0043-a-declaration-is-an-ordered-list-of-owned-resources.md): +the declaration is owned resources). It is not a capability, not a per-consumer grant — opening a +port is a declarative fact about a machine, so it is computed and applied, not requested and +credentialed. + +### `from` is the whole of public-versus-internal + +The distinction the question is really about lives in `from`: + +- `listens: [{ port: 5432, from: mesh }]` — open to the private overlay only. +- `listens: [{ port: 443, from: anywhere }]` — open to the public internet. + +A module registers a port on the firewall by listening on it and saying from where. There is no +separate firewall capability, because the firewall is not a thing that reaches back or holds a +secret; it is the machine's own filter over the ports its modules named. + +### The host enforces it both ways, and unknown keys are refused + +A port a module listens on is opened to exactly the scope it named; a port nothing declares is +closed. And a key the firewall does not read — the `scope:` of issue 003 — is refused at the +manifest, not accepted and ignored, so a declaration that reads as a restriction is one. This is the +discipline [ADR 0048](0048-a-module-broker-account-is-scoped-by-emits-and-consumes.md) applied to the +broker account, applied here to the packet filter: the declaration is the enforcement, or it is a +comment. + +### A public service is exposed through the proxy, not by opening its own port + +Reaching the public internet is normally not `from: anywhere` on the service's own port. The service +listens `from: mesh` — only the proxy reaches it — and `requires: route`, so the sole machine with a +public opening is the one running the reverse proxy, and the service is exposed by name through it. +`from: anywhere` is the deliberate direct-exposure case, for a service that is its own front door. + +## Consequences + +- Issue 003 is closed: the firewall is computed from `listens` and enforced, so a declared scope is + real and an undeclared port is shut. Rejecting unknown manifest keys is the general fix, of which + the `scope:` key was one instance. +- The firewall and the reverse proxy stop being confused for one model: the firewall is the machine's + filter (host-derived from `listens.from`); `route` is a name-router (a provider); the public DNS + name is a third thing ([ADR 0049](0049-a-public-name-is-provisioned-like-any-capability.md)). A + public service uses all three. +- The modelling question is answered: a module registers a port by declaring `listens`, and reaches + the public internet by name through `route` + `public-dns` — never by the firewall being a + provider. + +## References + +- [ADR 0037](0037-the-host-applies-it-does-not-decide.md) — the host applies; the firewall is one of + the things it applies. +- [ADR 0043](0043-a-declaration-is-an-ordered-list-of-owned-resources.md) — the firewall is a derived + owned resource, not a grant. +- [ADR 0048](0048-a-module-broker-account-is-scoped-by-emits-and-consumes.md) — the same discipline: + a declaration is enforced, or it is a comment. +- [ADR 0049](0049-a-public-name-is-provisioned-like-any-capability.md) — the public name, the other + half of the reachability question this was asked with. +- [04-ISSUES/003](../04-ISSUES/003-firewall-scope-is-read-by-no-code/00-report.md) — the unenforced + `scope:` this closes.