94 lines
6.1 KiB
Markdown
94 lines
6.1 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 |
|
|
|
|
## Answered by the provider helper (issue 792)
|
|
|
|
A small Go program beside the proxy (`cmd/route-provider`) runs the SDK's provider helper for `route`, with no credential,
|
|
and the offer says `"answered-by": "helper"`: a handed consumer (gitea's origins file) is filled by its machine's client
|
|
from this module's entry, which carries the public names the controller composed for it. The module is **not** marked
|
|
moved: its own files, `acme.env` and the broker secret, are composed as before, and the proxy itself is unchanged.
|