# mvirt 0.12.0

migrations: yes
wire: unchanged
rev: 5131e9fffc5ea434f25599c38a4ddacca1febb5e

v0.12.0 — the edge serves from every control-plane member

The release that makes a three-voter control plane serve its public edge from every member
instead of only from the host that happens to be leading. ADR-0073 D7's edge pool was built
from a column only the leader ever wrote, and the leaf it health-checked was a key type Envoy
will not verify; both are fixed, and with them the last thing standing between a working
failover and a public outage. Also lands the platform's own identity as region state, one key
type across every credential the platform mints, storage deployment modes, and the ADR-0077
admin console.

- **A member's REST endpoint is self-reported, never leader-derived.** `ControlPlaneMember.rest_url`
  had exactly one writer — the leader, writing its own row from its own process configuration — so
  however many members the group had, at most one was ever addressable by the edge. The value now
  travels the D4 facet rail from the replica that bound it (`ControlPlaneFacet` field 7, published
  into `replica-status.json` after the listener is up), and the leader only transcribes. A replica
  whose listener never came up contributes nothing rather than a URL nobody answers, and a stopped
  replica retracts itself.
- **`MVIRT_CPLANE_UNDERLAY_REST` is deleted.** It conflated "does this replica have an address
  another host can dial?" — a fact about the host — with "have we proven D7 yet?", which is about
  when to deploy. What remains is the address condition alone: bind, and record a `rest_url`, only
  for an address that is neither loopback nor wildcard. The safety property the flag provided is
  structural now, not optional.
- **The underlay REST stands down where the plain REST already binds it.** A host with
  `restAddr = "0.0.0.0:18090"` would otherwise have had its control plane exit at boot on a bare
  `EADDRINUSE` naming neither listener. It now yields and says which two settings collide.

Measured on the dev fleet through the edge: pool of three members, zero health-check network
failures (they were 1448/1448 when #101 opened), every handshake negotiating
`ecdsa_secp256r1_sha256`, and a leader-stop outage window of **2123 ms** against the D4 bound
of 10 s.

`pki::KEY_ALG` is P-256 for everything the platform mints — the raft mesh leaf, the internal CA
root, the node's identity and raft CSR keys, the k8s cluster CA and client certs, the ACME CSR —
and **both CSR signing entry points refuse anything else**. #101 was not "BoringSSL cannot do
Ed25519"; it is that Envoy's default verify list has no ed25519, so a server holding only an
Ed25519 leaf has no scheme in common and the handshake dies before any HTTP runs. The defect was
that the argument for every *other* Ed25519 credential was the same never-re-checked claim.
On-disk material is repaired on the next resync rather than waited out to its 21-day renewal
window.

> **Hard cut at the node wire, and `wire: unchanged` does not cover it** — that field tracks
> `mvirt-stor-wire` only. The CSR signing boundary now refuses any key that is not P-256, at
> onboarding **and** at control-plane adoption. A node on an older binary cannot re-onboard and
> cannot be adopted as a control-plane member. Existing tunnels keep working; only issuance and
> renewal break. Upgrade control-plane hosts and nodes in the same window.

`MVIRT_API_HOST`/`MVIRT_CONSOLE_HOST` were read from the cplane's process environment and
re-asserted by the edge reconciler on every pass. That was coherent with one control plane on one
host; ADR-0073 broke it, because the reconciler runs on the leader and the leader moves — a
replica whose environment never named the platform compiles no edge, which is how #101's bring-up
left the gw-agent in `MVIRT_LB_ID missing` forever. `LoadBalancerSettings` gains `api_host` +
`console_host`; `bootstrap::ensure_platform_domains` seeds them from host configuration when the
store declares none, and `edge::reconcile` reads the store. Env is bootstrapping input, mvdb is
truth — the `ensure_acme_contact` shape.

The NixOS profile now also sets the four derived per-process URLs and `MVIRT_CONSOLE_DIR` on every
control-plane-capable host rather than the bootstrap one alone: under D7 the edge routes to every
member's REST, so a replica that answers a request needs the same CORS origins, OIDC `return_to`
allowlist and console bundle the bootstrap host has. A host that declares no identity emits none of
the derived URLs rather than the literal `"https://"` (#156) — that string survives origin
normalization as `"https:"`, which would have given a follower a one-entry junk CORS and
`return_to` allowlist, failing console calls and logins on every replica that is not the bootstrap
host. `nix/checks/bare-metal-cplane-env.nix` covers both that and the fact that the bare-metal
profile evaluates at all, which no cargo gate could.

`StorageSettings` (`objectStorage: platform|remote`, `replicatedVolumes`) as a store singleton whose
defaults are today's platform, so an installation that never chooses is unaffected. `GET`/`PATCH
/v1/storage-settings`; the read is open to any authenticated principal because the volume and
launch forms gate their class list on it. D4's refusals name the setting at the RF choke point and
at KaaS cluster create. The console wizard gains a Storage step, and local-only gates every
replicated-class offer.

Platform overview landing, flow-grouped admin rail with fleet badges, overview depth (supply gauge,
error feed, per-cluster rollup), collapsible rail groups with attention rollups, and fleet-table
polish (free/total units, storage facet labelling, honest IP allocation).

- Security audit tracks 00–07: attack-surface inventory, verified listener/auth matrix, threat
  model, and deep-dive reports (issues #114–#119, findings #122–#154). Documentation only.
- `mvdb` declares `rerun-if-changed` for the raft proto build script.

Control-plane host first, then nodes one at a time — and because of the P-256 cut, do not leave a
host on an older node binary across the window. This release migrates the store
(`0128_storage_settings`, `0129_platform_domains`), so it rolls back by store restore alone, not by
booting the previous generation. The control plane writes a pre-migration backup into
`<data dir>/backups` with its `.kek` sibling immediately before applying them.


## Upgrading

Point your flake's `mvirt` input at this release and redeploy the control-plane
host first, then each node in turn:

```
nix run .#colmena -- apply boot --reboot --on <control-plane host>
nix run .#colmena -- apply boot --reboot --on <node>   # one at a time
```

**This release migrates the store.** Once the control plane has started, the
previous version will refuse to run against it — migrations are forward-only, so
rolling the control-plane host back means restoring a backup, with data loss back
to the backup point. The control plane writes a backup immediately before it
applies migrations; nodes roll back freely by booting the previous generation.
