Issue 009 (fixed) + Issue 008 (resolved via ADR 0053) — module-runtime config & provider contract #21
@@ -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
|
||||
`<service>.<node>.<suffix>` 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.<provider>.record.created` and `module.<provider>.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.
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user