also looking at this
fix(fleet): carry a site's TLS material when it moves
#179Follow-up to #176 and #178. Refs #167, which names TLS explicitly in operation 2.
The hole
Certificates live in the gateway's cert directory (/etc/rpx/certs by default), which belongs to the box rather than to the site — the same shape as the on-box database #178 fixed, and the same failure if left behind:
- With pinned production certificates, the cutover lands on a box holding no certificate for the hostname, and every browser refuses it.
- With on-demand TLS the target re-issues on first request, so it self-heals — but only after a visible outage.
Like the database, nothing in the plan could catch it: the app starts, the health gate passes on loopback (plain HTTP), the route publishes, DNS moves, and the site is down.
The fix
The certificate and private key for the site's domain and every alias are carried before the route is published and well before DNS moves. That placement is the point: a hostname resolving to a box with no certificate for it is a worse outcome than the site simply still being on the old box.
… → restore → certificates → gateway → dns → drain-sourceChecksum, not existence
Whether the target already holds the material is decided by comparing checksums. An older certificate for the same hostname — expired, or issued while the domain still pointed somewhere else — is not the one being moved, and treating it as "already carried" would hand the cutover a certificate browsers reject.
A hostname the source has no certificate for is skipped rather than blocking: a site behind on-demand TLS may legitimately have none yet, and the pack script exits clean rather than failing on it.
Private keys are re-chmodded 0600 after extraction. tar restores the modes it recorded, so this is belt-and-braces — but a world-readable private key on a shared box is silent until it is not.
What is deliberately not carried
Renewal. The per-project renewal timer is written by the normal provisioning path, so the next cloud deploy against the target establishes it; until then the carried certificates stand on their own remaining validity. Documented in docs/cli.md rather than left to be discovered.
Verification
bun test — 4137 pass, 0 fail. Typecheck and lint clean.
13 new cases: ordering relative to the route and the cutover, carrying when the target has none, replacing a stale certificate the target already holds, skipping when the material already matches, not blocking on an unissued hostname, no step at all when TLS terminates off the box, DNS left on the source when the carry fails, plus the four script builders and the state parser.
Standing caveat, unchanged: site:move has been exercised at the unit level with injected effects, not against two live boxes. Worth a rehearsal on throwaway servers before the first real production move.
4 changed files on the files tab, with 0 review threads.