Relay: its own image, binaries and signatures at release (RL-31) #271
No files matched your search
@@ -45,8 +45,17 @@
|
||||
# The push logs in with PACKAGE_TOKEN (jcoffey-dev, write:package): the job's
|
||||
# own token is refused by the container registry.
|
||||
#
|
||||
# inbuxa Relay (relay spec, RL-31) ships from the same tag: publish-relay
|
||||
# builds its image (Dockerfile.relay) for both platforms, binaries takes its
|
||||
# static binaries out too, and relay-sign signs them with the inbuxa release
|
||||
# key, the secret RELAY_RELEASE_KEY, which only this forge holds
|
||||
# (tools/release/new-relay-keys.sh makes it). relay-sign runs whichever
|
||||
# forge built the release, once the binaries are on it. Without the secret
|
||||
# it warns and signs nothing, and no relay takes those binaries as an
|
||||
# upgrade (RL-29).
|
||||
#
|
||||
# BUILD_ON: when the Actions variable BUILD_ON is 'github' (org or repo), every
|
||||
# job here but the announcement skips, and the tag is published by
|
||||
# job here but the announcement and relay-sign skips, and the tag is published by
|
||||
# .github/workflows/ci.yml on the GitHub mirror instead -- same guards, same
|
||||
# tags, the same Release and binaries, created here through the API. The
|
||||
# `github` job waits for that run's commit status, "github/ci (tag)", and the
|
||||
@@ -68,6 +77,7 @@ jobs:
|
||||
version: ${{ steps.v.outputs.version }}
|
||||
tags: ${{ steps.v.outputs.tags }}
|
||||
prerelease: ${{ steps.v.outputs.prerelease }}
|
||||
cargo: ${{ steps.v.outputs.cargo }}
|
||||
steps:
|
||||
# Full history: the ancestry check cannot be answered from a shallow
|
||||
# clone. The checkout also fetches every branch as origin/*.
|
||||
@@ -93,6 +103,7 @@ jobs:
|
||||
echo "version=$V" >> "$GITHUB_OUTPUT"
|
||||
echo "tags=${tags% }" >> "$GITHUB_OUTPUT"
|
||||
echo "prerelease=$pre" >> "$GITHUB_OUTPUT"
|
||||
echo "cargo=$(python3 tools/release/version.py --cargo)" >> "$GITHUB_OUTPUT"
|
||||
echo "version $V; image tags: ${tags% }; prerelease: $pre"
|
||||
|
||||
publish-amd64:
|
||||
@@ -175,6 +186,63 @@ jobs:
|
||||
- if: always()
|
||||
run: docker logout "$REGISTRY" || true
|
||||
|
||||
# The relay's image, both platforms in one build: its builder isn't a
|
||||
# cross build, so arm64 compiles under QEMU here, slowly; the relay is
|
||||
# small. It runs after the server's arm64 build, not beside it. Before any
|
||||
# tag moves, the amd64 image must say it is this version and hold no
|
||||
# server binary (RL-T24).
|
||||
publish-relay:
|
||||
if: ${{ vars.BUILD_ON != 'github' }}
|
||||
needs: [version, publish-arm64]
|
||||
runs-on: docker
|
||||
container:
|
||||
image: docker:28-cli@sha256:625d9431a9f54c5a2bc90f24f0e1c3d55b1349fd857dd85035f98c2c9acbdd4d # 28-cli
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
env:
|
||||
DOCKER_BUILDKIT: "1"
|
||||
REGISTRY: ${{ vars.REGISTRY }}
|
||||
RELAY_IMAGE: ${{ vars.REGISTRY }}/${{ github.repository_owner }}/inbuxa-relay
|
||||
VERSION: ${{ needs.version.outputs.version }}
|
||||
CARGO_VERSION: ${{ needs.version.outputs.cargo }}
|
||||
TAGS: ${{ needs.version.outputs.tags }}
|
||||
PACKAGE_TOKEN: ${{ secrets.PACKAGE_TOKEN }}
|
||||
steps:
|
||||
- uses: coffey-labs/actions/checkout@fab0c4d45e0162963965f1555df27b7bed5e20ec
|
||||
- run: |
|
||||
echo "$PACKAGE_TOKEN" | docker login -u jcoffey-dev --password-stdin "$REGISTRY"
|
||||
docker run --privileged --rm tonistiigi/binfmt --install arm64
|
||||
docker buildx create --use --name gitea-builder --driver docker-container || docker buildx use gitea-builder
|
||||
- run: |
|
||||
set -eu
|
||||
for arch in amd64 arm64; do
|
||||
docker buildx build -f Dockerfile.relay \
|
||||
--platform "linux/$arch" \
|
||||
--provenance=false --sbom=false \
|
||||
--tag "$RELAY_IMAGE:$VERSION-$arch" \
|
||||
--push .
|
||||
done
|
||||
said="$(docker run --rm --platform linux/amd64 "$RELAY_IMAGE:$VERSION-amd64" --version)"
|
||||
[ "$said" = "inbuxa-relay $CARGO_VERSION" ] || { echo "the relay image says '$said'" >&2; exit 1; }
|
||||
id="$(docker create --platform linux/amd64 "$RELAY_IMAGE:$VERSION-amd64")"
|
||||
if docker export "$id" | tar -t | grep -qE '(^|/)inbuxa$'; then
|
||||
docker rm -f "$id" >/dev/null
|
||||
echo "the relay image holds a server binary" >&2; exit 1
|
||||
fi
|
||||
docker rm -f "$id" >/dev/null
|
||||
args=""
|
||||
for t in $TAGS; do args="$args --tag $RELAY_IMAGE:$t"; done
|
||||
docker buildx imagetools create $args \
|
||||
"$RELAY_IMAGE:$VERSION-amd64" "$RELAY_IMAGE:$VERSION-arm64"
|
||||
docker buildx imagetools inspect "$RELAY_IMAGE:$VERSION"
|
||||
- run: |
|
||||
apk add --no-cache -q curl
|
||||
curl -fsS -o /dev/null -X POST -H "Authorization: token $PACKAGE_TOKEN" \
|
||||
"$CI_SERVER_INTERNAL/api/v1/packages/${GITHUB_REPOSITORY%%/*}/container/inbuxa-relay/-/link/${GITHUB_REPOSITORY#*/}" \
|
||||
|| echo "package already linked (or link refused); not fatal"
|
||||
- if: always()
|
||||
run: docker logout "$REGISTRY" || true
|
||||
|
||||
# BUILD_ON=github: waits for the GitHub mirror's run for this tag, which
|
||||
# posts its result back as the commit status "github/ci (tag)", and takes
|
||||
# its answer. Fails after the timeout if no answer comes.
|
||||
@@ -241,6 +309,7 @@ jobs:
|
||||
except urllib.error.HTTPError as e:
|
||||
if e.code != 404: raise
|
||||
image = f"{os.environ['REGISTRY']}/{os.environ['REPO']}:{version}"
|
||||
relay = f"{os.environ['REGISTRY']}/{os.environ['REPO'].split('/')[0]}/inbuxa-relay:{version}"
|
||||
also = " ".join(f"`:{t}`" for t in os.environ["TAGS"].split()[1:])
|
||||
pre = os.environ["PRERELEASE"] == "true"
|
||||
body = (("Release candidate for the next series, to try before it ships. "
|
||||
@@ -252,7 +321,12 @@ jobs:
|
||||
"`inbuxa-linux-arm64.tar.gz`, with `SHA256SUMS`. Each is the binary out of this "
|
||||
"release's image for that architecture, so it is the same build. The image "
|
||||
"grants it `cap_net_bind_service`; a host install has to grant that itself "
|
||||
"(`setcap`, or `AmbientCapabilities` in the unit) to bind port 25.")
|
||||
"(`setcap`, or `AmbientCapabilities` in the unit) to bind port 25.\n\n"
|
||||
f"inbuxa Relay, the edge relay, ships from the same tag: image `{relay}` "
|
||||
"(linux/amd64, linux/arm64), and static binaries `inbuxa-relay-x86_64-linux-musl` "
|
||||
"and `inbuxa-relay-aarch64-linux-musl`, each with a `.sig` file by the inbuxa "
|
||||
"release key. inbuxa-admin's Relays page takes a binary and its `.sig` to "
|
||||
"upgrade a relay.")
|
||||
data = json.dumps({"tag_name": tag, "name": f"INBUXA {version}", "body": body,
|
||||
"prerelease": pre}).encode()
|
||||
r = json.load(urllib.request.urlopen(urllib.request.Request(f"{api}/releases", data=data, headers=h)))
|
||||
@@ -272,7 +346,7 @@ jobs:
|
||||
# amd64 runner and copying a file out of it needs no emulation.
|
||||
binaries:
|
||||
if: ${{ vars.BUILD_ON != 'github' }}
|
||||
needs: [version, publish-arm64, release]
|
||||
needs: [version, publish-arm64, publish-relay, release]
|
||||
runs-on: docker
|
||||
container:
|
||||
image: docker:28-cli@sha256:625d9431a9f54c5a2bc90f24f0e1c3d55b1349fd857dd85035f98c2c9acbdd4d # 28-cli
|
||||
@@ -301,7 +375,17 @@ jobs:
|
||||
tar -czf "inbuxa-linux-$arch.tar.gz" inbuxa
|
||||
rm inbuxa
|
||||
done
|
||||
sha256sum inbuxa-linux-*.tar.gz > SHA256SUMS
|
||||
# The relay's, as bare binaries: an upgrade (RL-29) takes the file itself
|
||||
relay="$REGISTRY/${REPO%%/*}/inbuxa-relay:$VERSION"
|
||||
for pair in amd64:x86_64 arm64:aarch64; do
|
||||
arch="${pair%%:*}"; name="inbuxa-relay-${pair#*:}-linux-musl"
|
||||
docker pull -q --platform "linux/$arch" "$relay"
|
||||
id="$(docker create --platform "linux/$arch" "$relay")"
|
||||
docker cp "$id:/usr/local/bin/inbuxa-relay" "$name"
|
||||
docker rm -f "$id" >/dev/null
|
||||
chmod 0755 "$name"
|
||||
done
|
||||
sha256sum inbuxa-linux-*.tar.gz inbuxa-relay-* > SHA256SUMS
|
||||
cat SHA256SUMS
|
||||
- name: attach them to the release
|
||||
run: |
|
||||
@@ -319,7 +403,9 @@ jobs:
|
||||
rel = get(f"/releases/tags/{tag}")
|
||||
assets = {a["name"]: a["id"] for a in get(f"/releases/{rel['id']}/assets")}
|
||||
|
||||
for path in ["/out/inbuxa-linux-amd64.tar.gz", "/out/inbuxa-linux-arm64.tar.gz", "/out/SHA256SUMS"]:
|
||||
for path in ["/out/inbuxa-linux-amd64.tar.gz", "/out/inbuxa-linux-arm64.tar.gz",
|
||||
"/out/inbuxa-relay-x86_64-linux-musl", "/out/inbuxa-relay-aarch64-linux-musl",
|
||||
"/out/SHA256SUMS"]:
|
||||
name = os.path.basename(path)
|
||||
# A re-run of a tag replaces its assets rather than leaving two
|
||||
# files with the same name and different contents.
|
||||
@@ -342,6 +428,55 @@ jobs:
|
||||
- if: always()
|
||||
run: docker logout "$REGISTRY" || true
|
||||
|
||||
# Signs the relay binaries on the release with the inbuxa release key
|
||||
# (RL-29), whichever forge put them there: this forge's binaries job, or
|
||||
# the GitHub run the `github` job waited for. Each .sig names the Cargo
|
||||
# version the binary's --version prints and the target in its file name.
|
||||
# The key is written to a file only for the signing, and removed.
|
||||
relay-sign:
|
||||
needs: [binaries, github]
|
||||
if: ${{ always() && (needs.binaries.result == 'success' || needs.github.result == 'success') }}
|
||||
runs-on: docker
|
||||
container:
|
||||
image: docker:28-cli@sha256:625d9431a9f54c5a2bc90f24f0e1c3d55b1349fd857dd85035f98c2c9acbdd4d # 28-cli
|
||||
env:
|
||||
TAG: ${{ github.ref_name }}
|
||||
REPO: ${{ github.repository }}
|
||||
TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
RELAY_RELEASE_KEY: ${{ secrets.RELAY_RELEASE_KEY }}
|
||||
steps:
|
||||
- uses: coffey-labs/actions/checkout@fab0c4d45e0162963965f1555df27b7bed5e20ec
|
||||
- shell: sh
|
||||
run: |
|
||||
set -eu
|
||||
if [ -z "$RELAY_RELEASE_KEY" ]; then
|
||||
echo "::warning::RELAY_RELEASE_KEY is not set: the relay binaries stay unsigned, and no relay takes them as an upgrade"
|
||||
exit 0
|
||||
fi
|
||||
apk add --no-cache -q bash coreutils curl jq openssl python3
|
||||
version="$(python3 tools/release/version.py --cargo)"
|
||||
api="$CI_SERVER_INTERNAL/api/v1/repos/$REPO"
|
||||
auth="Authorization: token $TOKEN"
|
||||
rel="$(curl -fsS -H "$auth" "$api/releases/tags/$TAG" | jq -r .id)"
|
||||
assets="$(curl -fsS -H "$auth" "$api/releases/$rel/assets")"
|
||||
umask 077
|
||||
key="$(mktemp)"
|
||||
trap 'rm -f "$key"' EXIT
|
||||
printf '%s\n' "$RELAY_RELEASE_KEY" > "$key"
|
||||
for target in x86_64-linux-musl aarch64-linux-musl; do
|
||||
name="inbuxa-relay-$target"
|
||||
id="$(echo "$assets" | jq -r --arg n "$name" '.[] | select(.name == $n) | .id' | head -n1)"
|
||||
[ -n "$id" ] || { echo "the release has no $name" >&2; exit 1; }
|
||||
curl -fsSL -H "$auth" -o "$name" "$CI_SERVER_INTERNAL/$REPO/releases/download/$TAG/$name"
|
||||
bash tools/release/sign-relay.sh "$key" "$name" "$version" "$target"
|
||||
for old in $(echo "$assets" | jq -r --arg n "$name.sig" '.[] | select(.name == $n) | .id'); do
|
||||
curl -fsS -o /dev/null -X DELETE -H "$auth" "$api/releases/$rel/assets/$old"
|
||||
done
|
||||
curl -fsS -o /dev/null -X POST -H "$auth" -F "attachment=@$name.sig" "$api/releases/$rel/assets?name=$name.sig"
|
||||
echo "signed $name ($version, $target)"
|
||||
cat "$name.sig"
|
||||
done
|
||||
|
||||
# The release above is made with the job's own token, and Gitea starts no
|
||||
# workflow for events the Actions bot causes -- announce.yml's
|
||||
# 'on: release' never fires for it -- so announce it from here.
|
||||
|
||||
+102
-5
@@ -184,6 +184,8 @@ jobs:
|
||||
# candidate gets 27.1-rc.N and 27.1-rc; a release 27.1.N, 27.1 and latest.
|
||||
tags: ${{ steps.v.outputs.tags }}
|
||||
prerelease: ${{ steps.v.outputs.prerelease }}
|
||||
# The Cargo form (27.1.0-rc.N), which `inbuxa-relay --version` prints.
|
||||
cargo: ${{ steps.v.outputs.cargo }}
|
||||
steps:
|
||||
# Full history, and every branch as origin/*: the ancestry check cannot
|
||||
# be answered from a shallow clone.
|
||||
@@ -208,6 +210,7 @@ jobs:
|
||||
echo "version=$V" >> "$GITHUB_OUTPUT"
|
||||
echo "tags=${tags% }" >> "$GITHUB_OUTPUT"
|
||||
echo "prerelease=$pre" >> "$GITHUB_OUTPUT"
|
||||
echo "cargo=$(python3 tools/release/version.py --cargo)" >> "$GITHUB_OUTPUT"
|
||||
echo "image tags: ${tags% }; prerelease: $pre"
|
||||
|
||||
# Each architecture on its own native runner, side by side. The Dockerfile
|
||||
@@ -305,6 +308,83 @@ jobs:
|
||||
"$GITEA_URL/api/v1/packages/${owner,,}/container/$name/-/link/$name" \
|
||||
|| echo "package already linked (or link refused); not fatal"
|
||||
|
||||
# inbuxa Relay's image (relay spec, RL-31), from the same tag: its own
|
||||
# Dockerfile, a static musl build on each architecture's own runner, under
|
||||
# the server's version tags. It is small, so it needs none of the room the
|
||||
# server's build does.
|
||||
relay-publish:
|
||||
needs: [version]
|
||||
runs-on: ${{ matrix.runner }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- arch: amd64
|
||||
runner: ubuntu-latest
|
||||
- arch: arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
env:
|
||||
VERSION: ${{ needs.version.outputs.version }}
|
||||
steps:
|
||||
- run: echo "RELAY_IMAGE=${{ vars.REGISTRY }}/${GITHUB_REPOSITORY_OWNER,,}/inbuxa-relay" >> "$GITHUB_ENV"
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0
|
||||
- uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
with:
|
||||
registry: ${{ vars.REGISTRY }}
|
||||
username: jcoffey-dev
|
||||
password: ${{ secrets.GITEA_TOKEN }}
|
||||
- uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile.relay
|
||||
platforms: linux/${{ matrix.arch }}
|
||||
provenance: false
|
||||
sbom: false
|
||||
push: true
|
||||
tags: ${{ env.RELAY_IMAGE }}:${{ env.VERSION }}-${{ matrix.arch }}
|
||||
|
||||
# The relay's index, under the server's tags. Before it moves any tag, the
|
||||
# amd64 image must say it is this version, and must hold no server binary
|
||||
# (RL-T24).
|
||||
relay-index:
|
||||
needs: [version, relay-publish]
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
VERSION: ${{ needs.version.outputs.version }}
|
||||
CARGO_VERSION: ${{ needs.version.outputs.cargo }}
|
||||
TAGS: ${{ needs.version.outputs.tags }}
|
||||
steps:
|
||||
- run: echo "RELAY_IMAGE=${{ vars.REGISTRY }}/${GITHUB_REPOSITORY_OWNER,,}/inbuxa-relay" >> "$GITHUB_ENV"
|
||||
- uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0
|
||||
- uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
with:
|
||||
registry: ${{ vars.REGISTRY }}
|
||||
username: jcoffey-dev
|
||||
password: ${{ secrets.GITEA_TOKEN }}
|
||||
- run: |
|
||||
set -euo pipefail
|
||||
said="$(docker run --rm "$RELAY_IMAGE:$VERSION-amd64" --version)"
|
||||
[ "$said" = "inbuxa-relay $CARGO_VERSION" ] || { echo "the relay image says '$said'" >&2; exit 1; }
|
||||
id="$(docker create "$RELAY_IMAGE:$VERSION-amd64")"
|
||||
files="$(docker export "$id" | tar -t)"
|
||||
docker rm -f "$id" >/dev/null
|
||||
if grep -qE '(^|/)inbuxa$' <<<"$files"; then
|
||||
echo "the relay image holds a server binary" >&2; exit 1
|
||||
fi
|
||||
args=()
|
||||
for t in $TAGS; do args+=(--tag "$RELAY_IMAGE:$t"); done
|
||||
docker buildx imagetools create "${args[@]}" \
|
||||
"$RELAY_IMAGE:$VERSION-amd64" "$RELAY_IMAGE:$VERSION-arm64"
|
||||
docker buildx imagetools inspect "$RELAY_IMAGE:$VERSION"
|
||||
- env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
run: |
|
||||
owner="${GITHUB_REPOSITORY%%/*}"; name="${GITHUB_REPOSITORY#*/}"
|
||||
curl -fsS -o /dev/null -X POST -H "Authorization: token $GITEA_TOKEN" \
|
||||
"$GITEA_URL/api/v1/packages/${owner,,}/container/inbuxa-relay/-/link/$name" \
|
||||
|| echo "package already linked (or link refused); not fatal"
|
||||
|
||||
# The weekly release creates its Release (and so the tag) on Gitea first; a
|
||||
# tag pushed by hand has none. Either way the tag ends up with exactly one
|
||||
# Release there, created once the image exists so its pull instructions
|
||||
@@ -327,10 +407,13 @@ jobs:
|
||||
if [ "$code" = 200 ]; then echo "$TAG already has a release"; exit 0; fi
|
||||
[ "$code" = 404 ] || { echo "looking up the release for $TAG answered $code" >&2; exit 1; }
|
||||
image="$REGISTRY/${GITHUB_REPOSITORY,,}:$VERSION"
|
||||
relay="$REGISTRY/${GITHUB_REPOSITORY_OWNER,,}/inbuxa-relay:$VERSION"
|
||||
also="$(for t in ${TAGS#"$VERSION"}; do printf ' `:%s`' "$t"; done)"
|
||||
body="Container image: \`$image\` (linux/amd64, linux/arm64); also$also.
|
||||
|
||||
Binaries for a host install are attached: \`inbuxa-linux-amd64.tar.gz\` and \`inbuxa-linux-arm64.tar.gz\`, with \`SHA256SUMS\`. Each is the binary out of this release's image for that architecture, so it is the same build. The image grants it \`cap_net_bind_service\`; a host install has to grant that itself (\`setcap\`, or \`AmbientCapabilities\` in the unit) to bind port 25."
|
||||
Binaries for a host install are attached: \`inbuxa-linux-amd64.tar.gz\` and \`inbuxa-linux-arm64.tar.gz\`, with \`SHA256SUMS\`. Each is the binary out of this release's image for that architecture, so it is the same build. The image grants it \`cap_net_bind_service\`; a host install has to grant that itself (\`setcap\`, or \`AmbientCapabilities\` in the unit) to bind port 25.
|
||||
|
||||
inbuxa Relay, the edge relay, ships from the same tag: image \`$relay\` (linux/amd64, linux/arm64), and static binaries \`inbuxa-relay-x86_64-linux-musl\` and \`inbuxa-relay-aarch64-linux-musl\`, each with a \`.sig\` file by the inbuxa release key. inbuxa-admin's Relays page takes a binary and its \`.sig\` to upgrade a relay."
|
||||
if [ "$PRERELEASE" = true ]; then
|
||||
body="Release candidate for the next series, to try before it ships. \`:latest\` stays on the current release.
|
||||
|
||||
@@ -347,7 +430,7 @@ jobs:
|
||||
# the image runs. `docker create` starts nothing, so copying a file out of
|
||||
# the arm64 image on an amd64 runner needs no emulation.
|
||||
binaries:
|
||||
needs: [version, index, release]
|
||||
needs: [version, index, relay-index, release]
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
VERSION: ${{ needs.version.outputs.version }}
|
||||
@@ -373,8 +456,21 @@ jobs:
|
||||
tar -czf "inbuxa-linux-$arch.tar.gz" inbuxa
|
||||
rm inbuxa
|
||||
done
|
||||
sha256sum inbuxa-linux-*.tar.gz > SHA256SUMS
|
||||
# The relay's, as bare binaries: an upgrade (RL-29) takes the file itself
|
||||
relay="${{ vars.REGISTRY }}/${GITHUB_REPOSITORY_OWNER,,}/inbuxa-relay:$VERSION"
|
||||
for pair in amd64:x86_64 arm64:aarch64; do
|
||||
arch="${pair%%:*}"; name="inbuxa-relay-${pair#*:}-linux-musl"
|
||||
docker pull -q --platform "linux/$arch" "$relay"
|
||||
id="$(docker create --platform "linux/$arch" "$relay")"
|
||||
docker cp "$id:/usr/local/bin/inbuxa-relay" "$name"
|
||||
docker rm -f "$id" >/dev/null
|
||||
chmod 0755 "$name"
|
||||
done
|
||||
sha256sum inbuxa-linux-*.tar.gz inbuxa-relay-* > SHA256SUMS
|
||||
cat SHA256SUMS
|
||||
# The relay binaries' .sig files are made on Gitea, which holds the
|
||||
# release key (publish.yml, relay-sign), once these are attached.
|
||||
#
|
||||
# A re-run of a tag replaces its assets rather than leaving two files
|
||||
# with the same name and different contents.
|
||||
#
|
||||
@@ -389,7 +485,8 @@ jobs:
|
||||
retry=(--retry 5 --retry-all-errors --retry-delay 15)
|
||||
rel="$(curl -fsS "${retry[@]}" -H "$auth" "$api/releases/tags/$TAG" | jq -r .id)"
|
||||
assets="$(curl -fsS "${retry[@]}" -H "$auth" "$api/releases/$rel/assets")"
|
||||
for f in out/inbuxa-linux-amd64.tar.gz out/inbuxa-linux-arm64.tar.gz out/SHA256SUMS; do
|
||||
for f in out/inbuxa-linux-amd64.tar.gz out/inbuxa-linux-arm64.tar.gz \
|
||||
out/inbuxa-relay-x86_64-linux-musl out/inbuxa-relay-aarch64-linux-musl out/SHA256SUMS; do
|
||||
name="$(basename "$f")"
|
||||
old="$(jq -r --arg n "$name" '.[] | select(.name == $n) | .id' <<<"$assets")"
|
||||
for id in $old; do curl -fsS "${retry[@]}" -o /dev/null -X DELETE -H "$auth" "$api/releases/$rel/assets/$id"; done
|
||||
@@ -486,7 +583,7 @@ jobs:
|
||||
# way round) count as passing; a failed or cancelled one does not.
|
||||
report:
|
||||
if: ${{ always() && vars.BUILD_ON == 'github' }}
|
||||
needs: [start, fork-checks, build, version, publish, index, release, binaries]
|
||||
needs: [start, fork-checks, build, version, publish, index, relay-publish, relay-index, release, binaries]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- env:
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
# inbuxa Relay's image (relay spec, RL-31): the inbuxa-relay binary and
|
||||
# nothing of the server. It is a static musl build, built natively for the
|
||||
# platform asked for (each architecture has its own runner), so the binary
|
||||
# taken out of this image for a Release runs on any Linux of that
|
||||
# architecture.
|
||||
#
|
||||
# docker buildx build -f Dockerfile.relay --platform linux/amd64 .
|
||||
#
|
||||
# The image runs as an unprivileged user (2001), may bind ports 25 and 443 and
|
||||
# nothing more, and keeps its state in /var/lib/inbuxa-relay. It sets
|
||||
# INBUXA_RELAY_CONTAINER, so the relay reports selfUpgrade: false and an
|
||||
# upgrade is a new image (RL-29). On its first run it joins with
|
||||
# INBUXA_RELAY_JOIN_TOKEN.
|
||||
#
|
||||
# Docker Hub images, pulled through Google's mirror, as in Dockerfile.
|
||||
FROM mirror.gcr.io/library/rust:1-alpine3.22 AS builder
|
||||
RUN apk add --no-cache musl-dev build-base cmake perl libcap ca-certificates
|
||||
WORKDIR /build
|
||||
COPY . .
|
||||
RUN cargo build --release --locked -p inbuxa-relay && \
|
||||
install -m 0755 target/release/inbuxa-relay /inbuxa-relay && \
|
||||
# Static, or it won't run on a host without musl
|
||||
! ldd /inbuxa-relay 2>/dev/null | grep -q '=>' && \
|
||||
# Set here: the copy below keeps it
|
||||
setcap 'cap_net_bind_service=+ep' /inbuxa-relay && \
|
||||
mkdir -m 0700 /state
|
||||
|
||||
# The binary and the CA roots, nothing else (RL-30): no shell, no package
|
||||
# manager. The user is a number, as there is no /etc/passwd.
|
||||
FROM scratch
|
||||
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
||||
COPY --from=builder /inbuxa-relay /usr/local/bin/inbuxa-relay
|
||||
COPY --from=builder --chown=2001:2001 /state /var/lib/inbuxa-relay
|
||||
USER 2001:2001
|
||||
WORKDIR /var/lib/inbuxa-relay
|
||||
VOLUME ["/var/lib/inbuxa-relay"]
|
||||
ENV INBUXA_RELAY_CONTAINER=1
|
||||
EXPOSE 25 443
|
||||
ENTRYPOINT ["/usr/local/bin/inbuxa-relay"]
|
||||
CMD ["run", "--state", "/var/lib/inbuxa-relay"]
|
||||
@@ -26,9 +26,26 @@ use aws_lc_rs::{
|
||||
};
|
||||
use std::{cmp::Ordering, fmt};
|
||||
|
||||
/// The inbuxa release keys' public halves (raw Ed25519). A build without
|
||||
/// one refuses every upgrade.
|
||||
pub const RELEASE_KEYS: &[[u8; 32]] = &[];
|
||||
/// The inbuxa release keys' public halves (raw Ed25519), as
|
||||
/// `tools/release/new-relay-keys.sh` printed them on 2026-10-30. A build
|
||||
/// without one refuses every upgrade.
|
||||
pub const RELEASE_KEYS: &[[u8; 32]] = &[
|
||||
// ci: the RELAY_RELEASE_KEY secret, which signs every release
|
||||
// J0kY_vzXTC8T8gxErBMYI6cVk4Bwki65HlJGoDp15uw
|
||||
[
|
||||
0x27, 0x49, 0x18, 0xfe, 0xfc, 0xd7, 0x4c, 0x2f, 0x13, 0xf2, 0x0c, 0x44, 0xac, 0x13, 0x18,
|
||||
0x23, 0xa7, 0x15, 0x93, 0x80, 0x70, 0x92, 0x2e, 0xb9, 0x1e, 0x52, 0x46, 0xa0, 0x3a, 0x75,
|
||||
0xe6, 0xec,
|
||||
],
|
||||
// offline: kept off any server, to sign by hand when the CI key has to
|
||||
// be replaced
|
||||
// vGsTE95CJ3vbGKVtfQctFEp0fOZ4wQ9QWTOFe0KF7_8
|
||||
[
|
||||
0xbc, 0x6b, 0x13, 0x13, 0xde, 0x42, 0x27, 0x7b, 0xdb, 0x18, 0xa5, 0x6d, 0x7d, 0x07, 0x2d,
|
||||
0x14, 0x4a, 0x74, 0x7c, 0xe6, 0x78, 0xc1, 0x0f, 0x50, 0x59, 0x33, 0x85, 0x7b, 0x42, 0x85,
|
||||
0xef, 0xff,
|
||||
],
|
||||
];
|
||||
|
||||
const HEADER: &str = "inbuxa-relay release 1";
|
||||
|
||||
@@ -326,6 +343,33 @@ mod tests {
|
||||
const OPENSSL_SIGNATURE: &str =
|
||||
"zRj90ByVN18fmW-GOZ9cvGBK1ZsYYoNvowGsbSBYcslJhgIgmKrYHgRgq1CSIy4G-EzR5uexCzAUoa3FEcluAA";
|
||||
|
||||
/// The keys compiled in are the ones the comments name.
|
||||
#[test]
|
||||
fn release_keys_as_printed() {
|
||||
let printed: Vec<String> = RELEASE_KEYS.iter().map(|key| b64::encode(key)).collect();
|
||||
assert_eq!(
|
||||
printed,
|
||||
[
|
||||
"J0kY_vzXTC8T8gxErBMYI6cVk4Bwki65HlJGoDp15uw",
|
||||
"vGsTE95CJ3vbGKVtfQctFEp0fOZ4wQ9QWTOFe0KF7_8"
|
||||
]
|
||||
);
|
||||
for key in RELEASE_KEYS {
|
||||
// Each is a key a signature can be checked against
|
||||
assert_eq!(
|
||||
verify_signature(
|
||||
&format!(
|
||||
"{HEADER}\nversion 1.0.0\ntarget t\nsha256 {}\nsignature {}\n",
|
||||
"0".repeat(64),
|
||||
"A".repeat(86)
|
||||
),
|
||||
&[*key]
|
||||
),
|
||||
Err(ReleaseError::BadSignature)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn versions_in_order() {
|
||||
let less = |a: &str, b: &str| compare_versions(a, b) == Some(Ordering::Less);
|
||||
|
||||
@@ -651,7 +651,7 @@ A **Relays** page under Settings › Mail flow:
|
||||
| Static musl binary, stripped, x86_64 and aarch64 | ≤ 15 MB |
|
||||
| Resident memory, joined and idle | ≤ 32 MB |
|
||||
| Resident memory, 50 concurrent SMTP sessions | ≤ 128 MB |
|
||||
| Container image | `FROM scratch`: the binary and CA roots only |
|
||||
| Container image | `FROM scratch`: the binary and CA roots only (12.4 MB on amd64, 2026-10-30) |
|
||||
|
||||
- **RL-31.** It ships as its own artifacts, from the same release tag as
|
||||
the server: a multi-arch image (linux/amd64, linux/arm64) named
|
||||
@@ -952,8 +952,39 @@ In progress. Done so far:
|
||||
output checks out against the Rust code. On this server, an upgrade
|
||||
with anything else, an unknown blob, or a signature by no release key
|
||||
is refused before any relay sees it.
|
||||
- Not yet: the release key itself, and signing in the release workflow,
|
||||
come with packaging (step 6). Until then no build accepts an upgrade.
|
||||
- The release keys are made with `tools/release/new-relay-keys.sh`: a CI
|
||||
key, kept as the Gitea Actions secret `RELAY_RELEASE_KEY`, and an
|
||||
offline key, kept off any server, for signing by hand when the CI key
|
||||
has to be replaced. Both public halves are in `RELEASE_KEYS` (made
|
||||
2026-10-30), so every build from then on takes signed upgrades.
|
||||
|
||||
Packaging (step 6, RL-31):
|
||||
|
||||
- `Dockerfile.relay` builds the relay alone on Alpine, as a static musl
|
||||
binary, natively on each architecture's runner. The image is `FROM
|
||||
scratch`: the binary, the CA roots and the state directory, owned by
|
||||
uid 2001; about 12 MB. It runs as 2001, with `cap_net_bind_service` on
|
||||
the binary and nothing else, sets `INBUXA_RELAY_CONTAINER` (so
|
||||
`selfUpgrade: false`), and joins from `INBUXA_RELAY_JOIN_TOKEN` on its
|
||||
first run.
|
||||
- A release tag builds it beside the server, on whichever forge builds:
|
||||
`<registry>/inbuxa/inbuxa-relay` under the server's tags. Before a tag
|
||||
moves, the amd64 image must print `inbuxa-relay <Cargo version>` and
|
||||
hold no `inbuxa` binary (RL-T24). The Release gets the binaries taken
|
||||
out of the image, `inbuxa-relay-x86_64-linux-musl` and
|
||||
`inbuxa-relay-aarch64-linux-musl`, in `SHA256SUMS` with the server's.
|
||||
- Gitea's `relay-sign` job then signs both with `RELAY_RELEASE_KEY`, and
|
||||
attaches the `.sig` files, whichever forge built them. Without the
|
||||
secret it warns and signs nothing. With GitHub building, a binary is
|
||||
only as trustworthy as that build: GitHub uploads it, Gitea signs what
|
||||
it finds.
|
||||
- `resources/systemd/inbuxa-relay.service` runs a host install from
|
||||
`/opt/inbuxa-relay/bin`, owned by the `inbuxa-relay` user so an upgrade
|
||||
can replace the binary, with `Restart=always`, the one capability, and
|
||||
the filesystem read-only but for that directory and the state
|
||||
directory.
|
||||
- Still to come in step 6: the installer path and the docs page (with
|
||||
the WireGuard setup, RL-32).
|
||||
|
||||
Order:
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
# inbuxa Relay on a host (relay spec, RL-31). The binary lives in
|
||||
# /opt/inbuxa-relay/bin, owned by the inbuxa-relay user, so an upgrade from
|
||||
# inbuxa-admin can replace it (RL-29); the relay then exits, and Restart=
|
||||
# starts the new one. It may bind ports 25 and 443 and nothing more.
|
||||
[Unit]
|
||||
Description=inbuxa Relay
|
||||
Conflicts=postfix.service sendmail.service exim4.service
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/opt/inbuxa-relay/bin/inbuxa-relay run --state /var/lib/inbuxa-relay
|
||||
Restart=always
|
||||
RestartSec=2
|
||||
SyslogIdentifier=inbuxa-relay
|
||||
User=inbuxa-relay
|
||||
Group=inbuxa-relay
|
||||
StateDirectory=inbuxa-relay
|
||||
StateDirectoryMode=0700
|
||||
LimitNOFILE=65536
|
||||
AmbientCapabilities=CAP_NET_BIND_SERVICE
|
||||
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
|
||||
NoNewPrivileges=yes
|
||||
ProtectSystem=strict
|
||||
ReadWritePaths=/opt/inbuxa-relay/bin
|
||||
ProtectHome=yes
|
||||
PrivateTmp=yes
|
||||
PrivateDevices=yes
|
||||
ProtectKernelTunables=yes
|
||||
ProtectKernelModules=yes
|
||||
ProtectKernelLogs=yes
|
||||
ProtectControlGroups=yes
|
||||
ProtectClock=yes
|
||||
ProtectHostname=yes
|
||||
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
|
||||
RestrictNamespaces=yes
|
||||
RestrictRealtime=yes
|
||||
LockPersonality=yes
|
||||
MemoryDenyWriteExecute=yes
|
||||
SystemCallArchitectures=native
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
Executable
+42
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env bash
|
||||
# SPDX-FileCopyrightText: 2026 Coffey Labs LLC
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
#
|
||||
# Make the inbuxa relay release keys (relay spec, RL-29):
|
||||
#
|
||||
# tools/release/new-relay-keys.sh DIR
|
||||
#
|
||||
# Writes two Ed25519 keys into DIR, which must not exist yet and is made
|
||||
# readable by you alone:
|
||||
#
|
||||
# ci.pem goes into the inbuxa-server Actions secret
|
||||
# RELAY_RELEASE_KEY; publish.yml signs every relay binary
|
||||
# with it. Delete this copy once the secret is set.
|
||||
# offline.pem stays offline (a password manager, or a USB key in a
|
||||
# drawer). It signs a release by hand with sign-relay.sh
|
||||
# when the CI key has to be replaced.
|
||||
#
|
||||
# Prints only the two public halves, for RELEASE_KEYS in
|
||||
# crates/relay-proto/src/release.rs. Neither private key is printed.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if [ $# -ne 1 ]; then
|
||||
sed -n '5,20p' "$0" | sed 's/^# \{0,1\}//'
|
||||
exit 2
|
||||
fi
|
||||
dir=$1
|
||||
if [ -e "$dir" ]; then
|
||||
echo "$dir exists: choose a new directory" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
umask 077
|
||||
mkdir -p "$dir"
|
||||
here=$(dirname "$0")
|
||||
for name in ci offline; do
|
||||
openssl genpkey -algorithm ed25519 -out "$dir/$name.pem"
|
||||
printf '%-8s %s\n' "$name" "$("$here/sign-relay.sh" --public "$dir/$name.pem")"
|
||||
done
|
||||
echo
|
||||
echo "Set the secret from $dir/ci.pem, move $dir/offline.pem offline, then delete $dir."
|
||||
Reference in new issue
Block a user