Relay: signed upgrades (RL-29), and older relays shown (RL-28) #270

Merged
jcoffey-dev merged 1 commits from relay/upgrade into main 2026-10-30 23:53:24 +00:00
Owner

Step 5's last piece: relay upgrades (RL-29), plus the downstream showing a relay that's older than itself (RL-28).

Signed releases

  • crates/relay-proto/src/release.rs defines the signature file inbuxa-relay-<target>.sig. It has four lines (inbuxa-relay release 1, version, target such as x86_64-linux-musl, and sha256), then signature: Ed25519 over those four lines, in base64url.
    • The release keys' public halves are compiled in as RELEASE_KEYS. It's empty for now, so every upgrade is refused (see below).
    • It also has SemVer ordering, so 27.1.0-rc.5 comes before 27.1.0.
  • tools/release/sign-relay.sh KEY.pem BINARY VERSION TARGET writes BINARY.sig. It needs only openssl and coreutils. A test checks the script's output against the Rust verifier.

Relay (crates/relay/src/upgrade.rs, Relay/upgrade {blobId, signature})

  • It refuses an upgrade in a container image (INBUXA_RELAY_CONTAINER, so selfUpgrade: false), while another upgrade is on its way, for a bad signature, for another target, and for a version that isn't newer.
  • Otherwise it:
    • writes the new binary beside its own;
    • runs it with --version, which must name the signed version within 10 s;
    • keeps the old binary as inbuxa-relay.previous and renames the new one into place;
    • records relay.upgraded, or relay.upgrade-refused with the reason;
    • answers, then exits with 75 (EX_TEMPFAIL) so its supervisor restarts it.

Server (inbuxa:Relay)

  • An update with upgrade: {blobId, signature} must be sent on its own.
    • The server reads the blob with the caller's access, checks the signature, then uploads the binary over this node's link (Sender::upgrade) and calls Relay/upgrade.
    • If this node has no link to the relay, the update is refused with forbidden.
    • The relay's refusal comes back as invalidProperties on upgrade.
  • olderThanServer (server-set): true when any link last heard an older relayVersion than this server's, null before any link has reported.

Tests

  • Relay end-to-end test, over the real link: a forged signature, an older version, another target and an unreadable file are each refused and leave the binary unchanged (RL-T16). A good release goes in, the old one is kept, the run loop is told to exit, and a second upgrade is refused.
  • Unit tests:
    • a binary that reports another version is never put in place;
    • signature parsing and tampering are caught;
    • version ordering is right.
  • relay_tests: an upgrade on create, a malformed one, one sent with other properties, an unknown blob, an unreadable file, and a signature by no release key are each refused before any relay sees them. olderThanServer is null before any link has reported.
  • relay_link_tests: a linked relay from this build has olderThanServer: false.
  • Run locally: relay crate 28 tests; relay-proto, features and fork tools all pass; relay_tests (2 s) and relay_link_tests (39 s); fmt, clippy (no new warnings in the changed files), the fork checks, and the expr-schema check.

Not in this PR, for step 6 (packaging)

  • The release key itself.
  • Signing in publish.yml.
  • A relay service unit with a restart policy.

Until the key exists, every build refuses every upgrade, which is the safe default.

Console: inbuxa-admin PR to follow (Upgrade dialog and the older-version line).

Step 5's last piece: relay upgrades (RL-29), plus the downstream showing a relay that's older than itself (RL-28). **Signed releases** - `crates/relay-proto/src/release.rs` defines the signature file `inbuxa-relay-<target>.sig`. It has four lines (`inbuxa-relay release 1`, `version`, `target` such as `x86_64-linux-musl`, and `sha256`), then `signature`: Ed25519 over those four lines, in base64url. - The release keys' public halves are compiled in as `RELEASE_KEYS`. It's empty for now, so every upgrade is refused (see below). - It also has SemVer ordering, so `27.1.0-rc.5` comes before `27.1.0`. - `tools/release/sign-relay.sh KEY.pem BINARY VERSION TARGET` writes `BINARY.sig`. It needs only openssl and coreutils. A test checks the script's output against the Rust verifier. **Relay** (`crates/relay/src/upgrade.rs`, `Relay/upgrade {blobId, signature}`) - It refuses an upgrade in a container image (`INBUXA_RELAY_CONTAINER`, so `selfUpgrade: false`), while another upgrade is on its way, for a bad signature, for another target, and for a version that isn't newer. - Otherwise it: - writes the new binary beside its own; - runs it with `--version`, which must name the signed version within 10 s; - keeps the old binary as `inbuxa-relay.previous` and renames the new one into place; - records `relay.upgraded`, or `relay.upgrade-refused` with the reason; - answers, then exits with 75 (EX_TEMPFAIL) so its supervisor restarts it. **Server** (`inbuxa:Relay`) - An update with `upgrade: {blobId, signature}` must be sent on its own. - The server reads the blob with the caller's access, checks the signature, then uploads the binary over this node's link (`Sender::upgrade`) and calls `Relay/upgrade`. - If this node has no link to the relay, the update is refused with `forbidden`. - The relay's refusal comes back as `invalidProperties` on `upgrade`. - `olderThanServer` (server-set): true when any link last heard an older `relayVersion` than this server's, null before any link has reported. **Tests** - Relay end-to-end test, over the real link: a forged signature, an older version, another target and an unreadable file are each refused and leave the binary unchanged (RL-T16). A good release goes in, the old one is kept, the run loop is told to exit, and a second upgrade is refused. - Unit tests: - a binary that reports another version is never put in place; - signature parsing and tampering are caught; - version ordering is right. - `relay_tests`: an upgrade on create, a malformed one, one sent with other properties, an unknown blob, an unreadable file, and a signature by no release key are each refused before any relay sees them. `olderThanServer` is null before any link has reported. - `relay_link_tests`: a linked relay from this build has `olderThanServer: false`. - Run locally: relay crate 28 tests; relay-proto, features and fork tools all pass; `relay_tests` (2 s) and `relay_link_tests` (39 s); fmt, clippy (no new warnings in the changed files), the fork checks, and the expr-schema check. **Not in this PR, for step 6 (packaging)** - The release key itself. - Signing in `publish.yml`. - A relay service unit with a restart policy. Until the key exists, every build refuses every upgrade, which is the safe default. Console: inbuxa-admin PR to follow (Upgrade dialog and the older-version line).
jcoffey-dev added 1 commit 2026-10-30 23:42:52 +00:00
Relay: signed upgrades (RL-29), and older relays shown (RL-28)
ci / fork-checks (pull_request) Skipped
ci / build (pull_request) Skipped
github/ci (branch) GitHub Actions
ci / github (pull_request) Successful in 10m5s
658bf324ef
A relay release binary comes with a signature file: version, target and
SHA-256, signed with Ed25519 by the inbuxa release key, whose public half
is compiled into relay-proto. tools/release/sign-relay.sh makes the file
with openssl.

inbuxa:Relay/set takes an update with upgrade: {blobId, signature}. The
server checks the signature, uploads the binary over this node's link and
calls Relay/upgrade. The relay refuses it in a container image, while
another upgrade is on its way, and for a bad signature, another target or
a version that isn't newer. Otherwise it runs the new binary's --version,
keeps the old one as inbuxa-relay.previous, swaps it in and exits with 75
for its supervisor to restart it.

inbuxa:Relay gains olderThanServer, from each link's last relay version.

No release key is set yet: that and signing in the release workflow come
with packaging (step 6), so for now every upgrade is refused.
jcoffey-dev merged commit fefded0e47 into main 2026-10-30 23:53:24 +00:00
jcoffey-dev deleted branch relay/upgrade 2026-10-30 23:53:24 +00:00
Sign in to join this conversation.