From 554b7bffd87d029c92335280142f7443c3917feb Mon Sep 17 00:00:00 2001 From: Ilker Manap Date: Fri, 30 Oct 2026 23:47:28 +0100 Subject: [PATCH] Release builds the tag's commissioning image, without SSH keys build-image.sh copied the build user's authorized_keys into the image unless told otherwise, so the images published with v0.2.0 and v0.2.1 let three lab keys in as root on commissioning machines. Keys are now opt-in (AUTHKEYS=); the lab deploy passes the deploy user's keys. The release workflow gets an image job: it builds the tag's agent/ on the PXE server in a directory of its own (the lab image stays), refuses an image with authorized_keys, and uploads it as commissioning-* (token over stdin). Tried by hand on pxe-server: built, no keys, new agents. Closes #29 Co-Authored-By: Claude Opus 5.5 --- .gitea/workflows/deploy.yml | 3 +- .gitea/workflows/release.yml | 53 ++++++++++++++++++++++++++++++++++++ CHANGELOG.md | 7 +++++ agent/build-image.sh | 6 ++-- 4 files changed, 66 insertions(+), 3 deletions(-) diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index df43228..6b4be55 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -80,7 +80,8 @@ jobs: env: HOST: ${{ vars.PXE_HOST }} USER: ${{ vars.PXE_USER }} - run: ssh "$USER@$HOST" 'sudo /opt/rollcall/agent/build-image.sh 2>&1 | tail -20' + # the lab image lets the deploy user's keys in as root (debugging); release images do not + run: ssh "$USER@$HOST" 'sudo env AUTHKEYS=$HOME/.ssh/authorized_keys /opt/rollcall/agent/build-image.sh 2>&1 | tail -20' site: needs: server diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index 8fbe542..b5d68d3 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -69,3 +69,56 @@ jobs: -F "attachment=@$f" "$API/releases/$id/assets?name=$f" >/dev/null echo "uploaded $f" done + + # The commissioning image of this tag: built with live-build on the PXE server + # (the runner cannot), in a directory of its own (the lab's live image and + # build stay as they are), without anyone's SSH keys, and uploaded to the + # release as commissioning-* for bootstrap.sh --image-url. + image: + needs: release + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: SSH setup + env: + KEY: ${{ secrets.PXE_SSH_KEY }} + run: | + mkdir -p ~/.ssh && chmod 700 ~/.ssh + printf '%s\n' "$KEY" > ~/.ssh/id_ed25519 && chmod 600 ~/.ssh/id_ed25519 + printf 'Host *\n StrictHostKeyChecking accept-new\n ServerAliveInterval 30\n' > ~/.ssh/config + - name: Build the image on the PXE server + env: + HOST: ${{ vars.PXE_HOST }} + USER: ${{ vars.PXE_USER }} + TAG: ${{ github.ref_name }} + run: | + set -eu + W=/var/tmp/rollcall-release-$TAG + ssh "$USER@$HOST" "sudo rm -rf $W && mkdir -p $W/src" + scp -q -r agent deploy/publish-image.sh "$USER@$HOST:$W/src/" + ssh "$USER@$HOST" "sudo env AUTHKEYS= BUILD=$W/build OUT=$W/image bash $W/src/agent/build-image.sh > $W/build.log 2>&1 || { tail -40 $W/build.log; exit 1; }; tail -5 $W/build.log" + # refuse an image that lets someone in (a listing that fails refuses too) + ssh "$USER@$HOST" "sudo unsquashfs -l $W/image/filesystem.squashfs > $W/files" || + { echo "::error::cannot list the image" >&2; exit 1; } + if ssh "$USER@$HOST" "grep -E '/\.ssh/authorized_keys' $W/files"; then + echo "::error::the image carries SSH authorized_keys" >&2; exit 1 + fi + - name: Upload to the release + env: + HOST: ${{ vars.PXE_HOST }} + USER: ${{ vars.PXE_USER }} + TAG: ${{ github.ref_name }} + API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }} + TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -eu + W=/var/tmp/rollcall-release-$TAG + # the token over stdin, not on a command line + printf '%s\n' "$TOKEN" | ssh "$USER@$HOST" "read -r GITEA_TOKEN && export GITEA_TOKEN && IMG=$W/image sh $W/src/publish-image.sh $TAG $API" + - name: Clean up + if: always() + env: + HOST: ${{ vars.PXE_HOST }} + USER: ${{ vars.PXE_USER }} + TAG: ${{ github.ref_name }} + run: ssh "$USER@$HOST" "sudo rm -rf /var/tmp/rollcall-release-$TAG" || true diff --git a/CHANGELOG.md b/CHANGELOG.md index 35eba16..9475319 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,8 @@ publish a tag that has no section here. address (DNS-published in `dns_view`, or IPAM-only), ownership by the `rollcall_machine` extensible attribute (kept with `extattrs+`), leases, fixed addresses and ranges count as taken, delegated networks by the `rollcall` attribute. Not tested against a real grid yet. +- The release workflow builds the tag's commissioning image on the PXE server and publishes it as + `commissioning-*` assets. - Lab NetBox (`lab/netbox/install.sh`): a container on the lab segment for the NetBox drivers. - Fixed leases on the site's DHCP server for proxy-mode VLANs (`dhcp_reservations`), for Kea with `host_cmds` (Kea 3.0+) and OpenWrt (rpcd). Rollcall's own leases are reconciled; foreign ones are @@ -49,6 +51,11 @@ publish a tag that has no section here. ### Security A review of the whole code base; the fixes below. Upgrading: rerun `deploy/install.sh` on the region and `deploy/install-rack.sh` on rack hosts (new dnsmasq layout, signed rack updates). +- **Root SSH keys in published images.** `agent/build-image.sh` took + the build user's `authorized_keys` unless told otherwise, so the images published with v0.2.0 and + v0.2.1 let three lab keys (the maintainer's laptop, the CI deploy key, the lab hypervisor) log in as + root to machines while they commission. Keys are now opt-in (`AUTHKEYS=`, the lab deploy sets it), + and the release workflow builds the image of the tag itself and refuses one with `authorized_keys`. - **Region file read (critical).** A rack controller could read any file on the region (CA key, config) through `/api/v1/rack/binary?arch=../..`. `arch` is now a known architecture. - **Path traversal.** In `/api/v1/render/...` (admin) as well. diff --git a/agent/build-image.sh b/agent/build-image.sh index ba8d10e..3e78be4 100644 --- a/agent/build-image.sh +++ b/agent/build-image.sh @@ -6,7 +6,9 @@ SRC=$(cd "$(dirname "$0")" && pwd) BUILD=${BUILD:-/srv/rollcall/build} OUT=${OUT:-/srv/rollcall/image} MIRROR=${MIRROR:-http://deb.debian.org/debian} -AUTHKEYS=${AUTHKEYS:-/home/ilker/.ssh/authorized_keys} +# root's SSH keys in the commissioning environment: none unless asked for (a lab +# debugging aid; a published image must not carry anyone's keys) +AUTHKEYS=${AUTHKEYS:-} mkdir -p "$BUILD" "$OUT" cd "$BUILD" @@ -98,7 +100,7 @@ install -m 0755 "$SRC/rollcall-agent.py" config/includes.chroot/usr/local/bin/ro install -m 0755 "$SRC/rollcall-deploy.py" config/includes.chroot/usr/local/bin/rollcall-deploy.py install -m 0644 "$SRC/rollcall-agent.service" config/includes.chroot/etc/systemd/system/rollcall-agent.service printf 'ipmi_msghandler\nipmi_devintf\nipmi_si\nipmi_ssif\n' > config/includes.chroot/etc/modules-load.d/ipmi.conf -if [ -r "$AUTHKEYS" ]; then +if [ -n "$AUTHKEYS" ] && [ -r "$AUTHKEYS" ]; then install -m 0600 "$AUTHKEYS" config/includes.chroot/root/.ssh/authorized_keys chmod 0700 config/includes.chroot/root/.ssh fi