Step 6, first part: inbuxa Relay ships as its own image and binaries, and every release's relay binaries are signed (RL-31, RL-29).
Image (Dockerfile.relay)
inbuxa-relay alone, built on Alpine as a static musl binary, natively for each architecture.
FROM scratch: the binary, the CA roots and /var/lib/inbuxa-relay. 12.4 MB on amd64.
Runs as uid 2001 with cap_net_bind_service only, and sets INBUXA_RELAY_CONTAINER, so it reports selfUpgrade: false.
Checked locally: the container joined from INBUXA_RELAY_JOIN_TOKEN and listened on 443 as 2001 with CapEff 0x400. It holds no server binary.
Release workflows
GitHub (ci.yml, the builder today):
relay-publish builds per architecture on native runners.
relay-index checks that the amd64 image prints inbuxa-relay <Cargo version> and holds no inbuxa file (RL-T24), then moves the server's tags.
binaries also takes out inbuxa-relay-x86_64-linux-musl and inbuxa-relay-aarch64-linux-musl, adds them to SHA256SUMS, and attaches them.
report waits for the relay jobs.
Gitea (publish.yml, the fallback): the same jobs. publish-relay builds both platforms after the server's arm64 build; arm64 compiles under QEMU there.
relay-sign, on Gitea whichever forge builds:
It downloads the two binaries from the release and signs each with the RELAY_RELEASE_KEY secret (sign-relay.sh, Cargo version, the target from the file name). It attaches .sig files, replacing any old ones.
The key exists as a file only while signing, then it's removed.
Without the secret it warns and signs nothing, so a release never fails over it.
Both release notes mention the relay image, the binaries and the .sig files.
Keys (tools/release/new-relay-keys.sh DIR)
It makes ci.pem, which goes into the secret, and offline.pem, a backup to keep off any server, and prints only their public halves.
Made on 2026-10-30. Their public halves are in RELEASE_KEYS (second commit), with a test that pins the bytes to the printed strings. From this build on, relays take signed upgrades.
Rerun after adding them: relay crate (28 tests), relay-proto release tests, and relay_tests, whose "signed by no release key" case now hits a bad signature instead of a missing key.
Runs /opt/inbuxa-relay/bin/inbuxa-relay run. That directory is owned by the relay's user so an upgrade can replace the binary.
Restart=always, so it restarts after the upgrade exit (75).
Only CAP_NET_BIND_SERVICE, with ProtectSystem=strict and only that directory and the state directory writable, plus the usual hardening. Passes systemd-analyze verify; its only warning is that the binary isn't installed on this machine.
Trust note: with BUILD_ON=github, GitHub's run uploads the binaries and Gitea signs what it finds. A signature means "this release's build", which includes trusting the GitHub build.
Not tested end to end: the workflow jobs only run on a tag. The YAML parses, and every command in them was run by hand against a local image. The first release candidate after this merges is the real test; I'd watch it.
Still to come in step 6: the installer path, and the docs page with the WireGuard setup (RL-32).
Step 6, first part: inbuxa Relay ships as its own image and binaries, and every release's relay binaries are signed (RL-31, RL-29).
**Image** (`Dockerfile.relay`)
- `inbuxa-relay` alone, built on Alpine as a static musl binary, natively for each architecture.
- `FROM scratch`: the binary, the CA roots and `/var/lib/inbuxa-relay`. 12.4 MB on amd64.
- Runs as uid 2001 with `cap_net_bind_service` only, and sets `INBUXA_RELAY_CONTAINER`, so it reports `selfUpgrade: false`.
- Checked locally: the container joined from `INBUXA_RELAY_JOIN_TOKEN` and listened on 443 as 2001 with CapEff `0x400`. It holds no server binary.
**Release workflows**
- **GitHub (`ci.yml`, the builder today):**
- `relay-publish` builds per architecture on native runners.
- `relay-index` checks that the amd64 image prints `inbuxa-relay <Cargo version>` and holds no `inbuxa` file (RL-T24), then moves the server's tags.
- `binaries` also takes out `inbuxa-relay-x86_64-linux-musl` and `inbuxa-relay-aarch64-linux-musl`, adds them to `SHA256SUMS`, and attaches them.
- `report` waits for the relay jobs.
- **Gitea (`publish.yml`, the fallback):** the same jobs. `publish-relay` builds both platforms after the server's arm64 build; arm64 compiles under QEMU there.
- **`relay-sign`, on Gitea whichever forge builds:**
- It downloads the two binaries from the release and signs each with the `RELAY_RELEASE_KEY` secret (`sign-relay.sh`, Cargo version, the target from the file name). It attaches `.sig` files, replacing any old ones.
- The key exists as a file only while signing, then it's removed.
- Without the secret it warns and signs nothing, so a release never fails over it.
- Both release notes mention the relay image, the binaries and the `.sig` files.
**Keys** (`tools/release/new-relay-keys.sh DIR`)
- It makes `ci.pem`, which goes into the secret, and `offline.pem`, a backup to keep off any server, and prints only their public halves.
- Made on 2026-10-30. Their public halves are in `RELEASE_KEYS` (second commit), with a test that pins the bytes to the printed strings. From this build on, relays take signed upgrades.
- Rerun after adding them: relay crate (28 tests), relay-proto release tests, and `relay_tests`, whose "signed by no release key" case now hits a bad signature instead of a missing key.
**Host install** (`resources/systemd/inbuxa-relay.service`)
- Runs `/opt/inbuxa-relay/bin/inbuxa-relay run`. That directory is owned by the relay's user so an upgrade can replace the binary.
- `Restart=always`, so it restarts after the upgrade exit (75).
- Only `CAP_NET_BIND_SERVICE`, with `ProtectSystem=strict` and only that directory and the state directory writable, plus the usual hardening. Passes `systemd-analyze verify`; its only warning is that the binary isn't installed on this machine.
**Trust note:** with `BUILD_ON=github`, GitHub's run uploads the binaries and Gitea signs what it finds. A signature means "this release's build", which includes trusting the GitHub build.
**Not tested end to end:** the workflow jobs only run on a tag. The YAML parses, and every command in them was run by hand against a local image. The first release candidate after this merges is the real test; I'd watch it.
**Still to come in step 6:** the installer path, and the docs page with the WireGuard setup (RL-32).
Dockerfile.relay builds inbuxa-relay alone, as a static musl binary, into
a FROM-scratch image of about 12 MB: the binary, the CA roots and a state
directory, run as uid 2001 with cap_net_bind_service only, and
INBUXA_RELAY_CONTAINER set so the relay doesn't upgrade itself.
A release tag builds it on whichever forge builds, under the server's
tags, after checking that the image prints this version and holds no
server binary (RL-T24). The release gets the two binaries, taken out of
the image, in SHA256SUMS with the server's. Gitea's relay-sign job then
signs them with the RELAY_RELEASE_KEY secret and attaches the .sig files;
without the secret it warns and signs nothing.
tools/release/new-relay-keys.sh makes the CI key and an offline backup
key and prints only their public halves. resources/systemd has the unit
for a host install: binary in /opt/inbuxa-relay/bin, owned by the relay's
user so upgrades can replace it, Restart=always, sandboxed.
The CI key, which the RELAY_RELEASE_KEY secret holds, and the offline
key, as tools/release/new-relay-keys.sh printed their public halves. A
test checks the bytes against those strings.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Step 6, first part: inbuxa Relay ships as its own image and binaries, and every release's relay binaries are signed (RL-31, RL-29).
Image (
Dockerfile.relay)inbuxa-relayalone, built on Alpine as a static musl binary, natively for each architecture.FROM scratch: the binary, the CA roots and/var/lib/inbuxa-relay. 12.4 MB on amd64.cap_net_bind_serviceonly, and setsINBUXA_RELAY_CONTAINER, so it reportsselfUpgrade: false.INBUXA_RELAY_JOIN_TOKENand listened on 443 as 2001 with CapEff0x400. It holds no server binary.Release workflows
ci.yml, the builder today):relay-publishbuilds per architecture on native runners.relay-indexchecks that the amd64 image printsinbuxa-relay <Cargo version>and holds noinbuxafile (RL-T24), then moves the server's tags.binariesalso takes outinbuxa-relay-x86_64-linux-muslandinbuxa-relay-aarch64-linux-musl, adds them toSHA256SUMS, and attaches them.reportwaits for the relay jobs.publish.yml, the fallback): the same jobs.publish-relaybuilds both platforms after the server's arm64 build; arm64 compiles under QEMU there.relay-sign, on Gitea whichever forge builds:RELAY_RELEASE_KEYsecret (sign-relay.sh, Cargo version, the target from the file name). It attaches.sigfiles, replacing any old ones..sigfiles.Keys (
tools/release/new-relay-keys.sh DIR)ci.pem, which goes into the secret, andoffline.pem, a backup to keep off any server, and prints only their public halves.RELEASE_KEYS(second commit), with a test that pins the bytes to the printed strings. From this build on, relays take signed upgrades.relay_tests, whose "signed by no release key" case now hits a bad signature instead of a missing key.Host install (
resources/systemd/inbuxa-relay.service)/opt/inbuxa-relay/bin/inbuxa-relay run. That directory is owned by the relay's user so an upgrade can replace the binary.Restart=always, so it restarts after the upgrade exit (75).CAP_NET_BIND_SERVICE, withProtectSystem=strictand only that directory and the state directory writable, plus the usual hardening. Passessystemd-analyze verify; its only warning is that the binary isn't installed on this machine.Trust note: with
BUILD_ON=github, GitHub's run uploads the binaries and Gitea signs what it finds. A signature means "this release's build", which includes trusting the GitHub build.Not tested end to end: the workflow jobs only run on a tag. The YAML parses, and every command in them was run by hand against a local image. The first release candidate after this merges is the real test; I'd watch it.
Still to come in step 6: the installer path, and the docs page with the WireGuard setup (RL-32).