Commit 87348c7
The fleet key a running daemon signs with: live, not boot-time (#43)
* fix(daemon): read the fleet signing identity live, not once at boot
The fleet key rides relay.json, and relay.json is written by another
process: `flue relay setup` and `flue relay join` are terminal commands,
and the daemon they configure has been running since login. The relay
leg already knew this and re-reads the file on every start; the identity
did not, so a daemon that was running when its fleet was created came up
live on the relay — publishing a machine cert that verifies — and
silently unable to sign for it. The two records that make a device part
of a fleet are each written exactly once, by the pairing ceremony: the
`f=` the browser pins, and the device certificate. Neither has a repair
path, so every device paired in that window was fleet-blind forever.
daemon.Identity.Fleet is now a FleetSource — asked, at the moment of
signing, what this process may sign with — and cmd/flue answers it by
reading relay.json (fleetOnDisk). Read-on-use rather than a watcher or
an in-memory hook: the writers are in another process, the file is small,
and all four callers have a human waiting on them. The machine id a
certificate names is read from the same place for the same reason, so a
daemon whose relay leg has not started yet still signs certs somebody can
attribute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(relay): replace the relay leg in process, and tell the daemon from the CLI
A deploy from the Remote screen used to end with "restart the daemon":
the leg was dialling the relay.json that existed when it started, the
deploy had just replaced that file, and nothing in the process could
reconcile the two. PR #42 gave relayRuntime a real teardown, so it can
now — stop the old leg, wait for it to be gone, start one that reads the
file that is there. RestartNeeded is deleted rather than reworded, on
the API type and on the Remote screen.
The waiting is load-bearing: Transport.Run reports the relay off from a
deferred call on its own goroutine, and that line landing after a
replacement leg said "connecting" would leave every welcome announcing a
relay this daemon is in fact dialling, and every pairing URL naming
loopback until the next dial succeeded. startRelay hands back a wait for
both of its legs; the stop is bounded at five seconds.
`flue relay setup`, `join`, `address` and `leave` run in another process,
so they now knock on the daemon's loopback door — POST /api/relay/reload,
session token in the header, no body — and the daemon replaces its leg
from the relay.json that is on disk. Their "restart the daemon" notes are
gone; the only surviving restart line is for a daemon that is running and
could not be told, which is the one case where it is still true.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(fleet): back-fill the certificate a device was paired without
Device.Cert is written at the ceremony and nowhere else, and the other
door into the registry — AddFromFleetCert — wants a certificate the
device already holds. So every device paired while its machine had no
fleet key was stuck: it reached the machine it paired with, could
present nothing to any sibling, and re-pairing was the only way out.
That also made the re-supply path web/src/fleet/fleet.ts documents a
promise the daemon could not keep — "a browser paired before its machine
had a fleet key picks one up from any machine it can still reach" needs
a machine with one to hand over.
The welcome mints it now, on the first connection where this machine
can: crypto.DeviceStore.SetCert fills an empty cert and refuses every
other case (an existing one, an unpaired key, a revoked key), and
Server.backfillFleetCert signs {device key, label, this machine's id,
the pairing's own iat}. Lazily rather than in a startup sweep, for the
reason the fleet key itself is read lazily: the key can arrive at any
moment from a file another process writes, and the welcome is both the
moment the answer is needed and the moment the device is there to take
it. One mint per device, not one per connection — the blob is persisted.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(daemon): refuse a pairing QR this process cannot sign, and name the fault
A daemon on a relay that holds no fleet key still drew a QR, and the
device that scanned it pinned no fleet key and was minted no certificate.
Both records are written exactly once, by that ceremony, so the device
reached that one machine for the life of the pairing — silently, and with
nothing able to repair it afterwards.
The welcome's relay snapshot now carries noFleetKey (no key in relay.json,
one it cannot parse, or no machine id for a certificate to name), and the
Devices screen refuses to draw a QR the way it already refuses one that
could only name loopback: button shut, one paragraph, tied to the button
by aria-describedby. Stated as the fault rather than as a capability so an
older daemon's silence is never read as a refusal.
relayProblems gains the seed that is present and unusable — which is also
the line that answers "can this process sign", now that the daemon parses
that same value at the moment it mints — and `flue relay status`'s fleet
line spells out what an unusable key costs and how to fix it.
After the live read this is nearly unreachable, which is why it is a
backstop rather than a feature: the state it catches is permanent for
every device paired into it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(web): say what this browser cannot reach instead of skipping it
fleetSources dropped two facts on the floor. A machine the fleet
directory names that this browser holds no certificate for was skipped
with a `continue`, and a browser with no pinned fleet key got an empty
fleet view — so the maintainer's second machine simply was not on the
screen, and nothing anywhere said the list was short.
Both are now counted and reported (FleetGaps), and the sessions screen
states them in the same muted band the unreachable and revoked machines
already use: "N machines in this fleet have no certificate this browser
can present", or, for a browser with no fleet key at all, that it is
pinned to one machine and pairing again shows the rest.
Neither band offers a button, and that is the honest part: a certificate
comes from a daemon over a connection that will not accept this browser
without one, and a fleet key must never be learned from a connection —
that is the trust-on-first-use the pinned key exists to refuse. Pairing
again is the way out of both, and it starts on the machine.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test: the setup-then-pair path, in the order a real machine does it
Every piece of the defect was covered and the seam between them was not,
because every test built its daemon after the relay.json it would read.
Real machines are the other way round: the daemon starts at login and the
relay is set up in a terminal an hour later.
TestASetupInOneTerminalPairsInTheNextBreath runs it in that order and
across the two processes the user has — a daemon serving on loopback, and
`flue relay join` writing relay.json beside it — then pairs with no
restart anywhere and checks what the phone actually gets: a link carrying
`&f=` that decodes to the fleet key from the join line and names the relay
origin, and a device certificate that verifies under that key and names
the machine id the join minted.
Against the un-fixed identity (a boot-time read, restored locally to
check) it fails on the missing `&f=`, and with that assertion relaxed it
fails again on "the ceremony minted no certificate under the fleet key
this machine just joined" — the two records that made the maintainer's
phone permanently a one-machine phone.
TestPairStartURLCarriesTheFleetKey also asserts the literal `&f=` now:
conn.go splices that parameter in by hand, so the joining `&` is code, and
a link that lost it parses fine and pins nothing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs(relay): stop teaching a restart that is no longer needed
RELAY.md taught the trap as a step: restart after setup, restart after
join, "the restart is not optional", and a release checklist that
restarted the daemon before pairing anything — which is exactly why the
window between the two was never noticed. Setup and join now tell the
running daemon, and the fleet key is read at every signature, so the
instructions are wrong rather than merely dated.
The release gate now says the opposite where it matters: pair a phone
*before* touching the daemon, and check the QR link carries `&f=`. That
is the check that would have caught this by hand.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>1 parent 7ff9861 commit 87348c7
26 files changed
Lines changed: 2091 additions & 230 deletions
File tree
- cmd/flue
- docs
- internal
- crypto
- daemon
- transport/relay
- wire
- web/src
- client
- components
- fleet
- routes
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
| 8 | + | |
8 | 9 | | |
9 | 10 | | |
10 | 11 | | |
| |||
1337 | 1338 | | |
1338 | 1339 | | |
1339 | 1340 | | |
1340 | | - | |
| 1341 | + | |
1341 | 1342 | | |
1342 | 1343 | | |
1343 | 1344 | | |
| |||
1413 | 1414 | | |
1414 | 1415 | | |
1415 | 1416 | | |
1416 | | - | |
| 1417 | + | |
1417 | 1418 | | |
1418 | 1419 | | |
1419 | 1420 | | |
| |||
1506 | 1507 | | |
1507 | 1508 | | |
1508 | 1509 | | |
| 1510 | + | |
| 1511 | + | |
| 1512 | + | |
| 1513 | + | |
| 1514 | + | |
| 1515 | + | |
| 1516 | + | |
| 1517 | + | |
| 1518 | + | |
| 1519 | + | |
| 1520 | + | |
| 1521 | + | |
| 1522 | + | |
| 1523 | + | |
| 1524 | + | |
| 1525 | + | |
| 1526 | + | |
| 1527 | + | |
| 1528 | + | |
| 1529 | + | |
| 1530 | + | |
| 1531 | + | |
| 1532 | + | |
| 1533 | + | |
| 1534 | + | |
| 1535 | + | |
| 1536 | + | |
| 1537 | + | |
| 1538 | + | |
| 1539 | + | |
| 1540 | + | |
| 1541 | + | |
| 1542 | + | |
| 1543 | + | |
| 1544 | + | |
| 1545 | + | |
| 1546 | + | |
| 1547 | + | |
| 1548 | + | |
| 1549 | + | |
| 1550 | + | |
| 1551 | + | |
| 1552 | + | |
| 1553 | + | |
| 1554 | + | |
| 1555 | + | |
| 1556 | + | |
| 1557 | + | |
| 1558 | + | |
| 1559 | + | |
| 1560 | + | |
| 1561 | + | |
| 1562 | + | |
| 1563 | + | |
| 1564 | + | |
| 1565 | + | |
| 1566 | + | |
| 1567 | + | |
| 1568 | + | |
1509 | 1569 | | |
1510 | 1570 | | |
1511 | 1571 | | |
| |||
1531 | 1591 | | |
1532 | 1592 | | |
1533 | 1593 | | |
| 1594 | + | |
| 1595 | + | |
| 1596 | + | |
| 1597 | + | |
| 1598 | + | |
| 1599 | + | |
| 1600 | + | |
1534 | 1601 | | |
1535 | 1602 | | |
1536 | 1603 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
0 commit comments