public-acme ran nothing and had one consumer. The proxy now states the issuer itself, byte for byte what the binding rendered, so its account directory and every certificate stay put. dhcpcd and cloudflare-dns are assigned nowhere and nothing requires what they provide.
87 lines
5.6 KiB
Markdown
87 lines
5.6 KiB
Markdown
# route-proxy — the mesh's public ingress
|
|
|
|
The shipping form of the reference reverse proxy (novox/hq ADR 0007, novox/hq 08-connectivity §3
|
|
"Exposure"). It is the `traefik` replacement: the module that **provides `route`**.
|
|
|
|
## What it is
|
|
|
|
**A route is a grant.** A module that must be reachable declares `requires: ["route"]` and
|
|
contributes the name it wants and the port it listens on; this module provides `route`, receives
|
|
every consumer that asked as a file, and forwards by the `Host` header of each request to
|
|
`http://<at>:<port>` — where `at` is where the mesh says that consumer's machine is (empty means
|
|
this machine, reached over loopback).
|
|
|
|
The control plane does not write the proxy's configuration. It delivers the facts — the
|
|
contributions file at `receives.route` — and the proxy turns them into whatever it runs. That
|
|
boundary is why swapping this proxy for another costs nothing to the modules that publish through
|
|
it.
|
|
|
|
| field | value | why |
|
|
|---|---|---|
|
|
| `provides` | `route` (scope `mesh`) | a module on any node may require it |
|
|
| `serves.route` | `{}` | a route hands back a name, not a credential — the consumer already knows the name it asked for |
|
|
| `receives.route` | `…/routes/mesh.json` | where the mesh writes every contribution; the proxy reads it as `$ROUTES` and re-reads on change |
|
|
| `listens` | `80` and `443`, both `from: anywhere` | the one machine with a public opening; the firewall opens these for free (ADR 0045) |
|
|
|
|
No provisioner: the proxy neither mints a credential for anybody nor answers a provision other
|
|
than `route`. It reads the file the mesh writes, and its own broker credential is for its tools.
|
|
|
|
## Which issuer: Let's Encrypt, stated here (novox/hq ADR 0226)
|
|
|
|
**The public issuer is this module's own fact, not a provision.** Until 2026-10-06 it came from a
|
|
second module, `public-acme`, that ran nothing and offered `acme-ca` pointing at Let's Encrypt; every
|
|
node running a proxy was assigned it too, and nothing else ever consumed it. It is retired: the proxy
|
|
names the production directory in its own `acme.env`, and the only issuer it is still bound to is the
|
|
mesh's own (`internal-acme-ca`, from `step-ca`).
|
|
|
|
**`acme.env` is byte for byte what the binding rendered.** The proxy keeps each authority's account
|
|
and certificates in a directory under `/var/lib/route-proxy/acme` named after a digest of the
|
|
directory URL — `https://acme-v02.api.letsencrypt.org:443/directory`, port included, as the binding
|
|
spelled it — and of the root bundle the `trust` container copies in. Any change to either is a new
|
|
authority to the proxy: a new account, and every routed name ordered again against Let's Encrypt's
|
|
rate limits. So the URL keeps the `:443`, `ACME_ROOTS_PATH` stays empty, and the `trust` artifact's
|
|
pinned image is not moved without a decision; mesh-controller's
|
|
`TestRouteProxyKeepsItsPublicAccountDirectory` holds all three.
|
|
|
|
## How it ships the Go proxy
|
|
|
|
The proxy is a Go program, unlike the TypeScript tool-runtime modules. The canonical source is
|
|
**not vendored here** — it lives in the mesh-controller repository at `examples/route-proxy`, the
|
|
contract written as something that runs. This module ships only the packaging: a multi-stage
|
|
[`Dockerfile`](Dockerfile) whose build context is the mesh-controller repository root and which
|
|
compiles `./examples/route-proxy` into `mesh-route-proxy`. The committed `module.json` carries the
|
|
placeholder digest every mesh-built image does (`@sha256:0000…`); the mesh pins the real digest at
|
|
publish.
|
|
|
|
In the lab, `mesh-lab/scripts/build-route-proxy-image.sh` builds this into the local daemon as
|
|
`mesh-route-proxy:development`, and a scenario stocks it and serves it by digest.
|
|
|
|
## Certificates, and the endpoint (resolves 04-ISSUES/004)
|
|
|
|
TLS is opt-in and terminates here for public names, via ACME (autocert). A proxy with no
|
|
`TLS_LISTEN` serves plain HTTP; with it, it obtains a publicly-trusted certificate for exactly the
|
|
names the mesh routes here (`HostPolicy` = only routed names, so a scan cannot spend the account's
|
|
issuance quota) and answers the HTTP-01 challenge on `:80` at the name being certified.
|
|
|
|
**`ACME_DIRECTORY` defaults to Let's Encrypt *staging*, not production.** This closes
|
|
novox/hq 04-ISSUES/004 (certificate issuance targets production):
|
|
the issuer is a per-node choice, and the safe endpoint is the default. Production issuance is
|
|
rate-limited per domain and per account and does not replenish quickly; defaulting to production
|
|
would leave the safe path depending on somebody remembering to opt out of it, on exactly the work
|
|
most likely to iterate. A staging certificate is trusted by no browser, so the mistake announces
|
|
itself on the first request rather than a fortnight later at the rate limit.
|
|
|
|
**The module states production.** The binary's staging default stands for anything that runs it
|
|
without the module — the lab, a developer's machine — and the module's `acme.env` names production
|
|
for every node it is assigned to, because a node assigned the public proxy is one that serves public
|
|
traffic.
|
|
|
|
| env | default | meaning |
|
|
|---|---|---|
|
|
| `ROUTES` | `/routes/mesh.json` | the contributions file the mesh writes (`receives.route`) |
|
|
| `LISTEN` | `:80` | HTTP, and the ACME HTTP-01 challenge |
|
|
| `TLS_LISTEN` | `:443` | HTTPS; unset to serve plain HTTP only |
|
|
| `ACME_CACHE` | `/acme` | where issued certificates persist; required when `TLS_LISTEN` is set, so a restart does not re-order |
|
|
| `ACME_DIRECTORY` | Let's Encrypt **staging** in the binary; production in the module's `acme.env` | the issuer |
|
|
| `ACME_CA_BUNDLE` | *(unset)* | a file of roots to trust for the issuer's own API — set only for a private/lab authority whose API certificate the world does not yet trust |
|