fix: a release job uses only the key it is given, never the runner's own
CI / checks (push) Failing after 2m13s
CI / package and container gate (push) Skipped

Measured on a throwaway Gitea: with the key secret unset, Preview's
publish job would have fallen back to the runner machine's own ssh
configuration — on the development VM, the production publish key — and
published a throwaway 9.9.9 to the real server. It was cancelled before
it ran. And with the key set, the job wrote over the runner account's own
~/.ssh/config and known_hosts.

Now, under Actions, a job without RELEASE_SSH_KEY stops; with it, the key,
its known_hosts and an ssh configuration live in the job's own directory
and every ssh and rsync goes through `ssh -F` (RELEASE_SSH_CONFIG in
release-lib), which reads nothing else. The key is deleted when the job
ends. By hand, nothing changes. Also records the end-to-end measurements
of Release 1 and of Preview's gate job in docs/releasing.md.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
ekklesia
2026-09-29 20:24:50 -05:00
co-authored by Claude Opus 5.5
parent 22f25854aa
commit 14291fde37
5 changed files with 103 additions and 19 deletions
+28 -11
View File
@@ -30,7 +30,8 @@
# release. preview and stable also need the update server, through
# RELEASE_SSH_KEY (the private key's text), RELEASE_SSH_KNOWN_HOSTS,
# RELEASE_SSH_ADDRESS and RELEASE_SSH_USER, which only the release runner's
# jobs are given. Run by hand, ~/.ssh/config is used as it always was.
# jobs are given, and without which a job refuses to publish. Run by hand
# (no GITHUB_ACTIONS), ~/.ssh/config is used as it always was.
set -euo pipefail
shopt -s inherit_errexit
@@ -109,30 +110,46 @@ fetch() {
printf '%s\n' "$WORK/$name"
}
# The update server, for the release runner. The key is written to a file
# only this job can read, and is gone when the job's container is.
# The update server, for the release runner: the key it was given, in a
# configuration of its own (RELEASE_SSH_CONFIG, used with ssh -F), so
# nothing on the runner's own disk is read or overwritten, and the key is
# deleted when this ends. Under Actions there is no other way: a job
# without the key stops, rather than reach for whatever credentials the
# machine it runs on happens to hold. Measured: on the development VM,
# which holds the production publish key, the fallback would have
# published a throwaway build to the real server.
ssh_setup() {
local dir="$WORK/ssh"
if [[ -z "${RELEASE_SSH_KEY:-}" ]]; then
echo "release-ci: no RELEASE_SSH_KEY; using this machine's own ssh configuration" >&2
if [[ -n "${GITHUB_ACTIONS:-}${GITEA_ACTIONS:-}" ]]; then
fail "no RELEASE_SSH_KEY: set the CLASSROOM_UPDATES_SSH_KEY secret. A job never uses the runner's own ssh keys"
fi
echo "release-ci: no RELEASE_SSH_KEY; by hand, so this machine's own ssh configuration" >&2
return
fi
[[ -n "${RELEASE_SSH_KNOWN_HOSTS:-}" ]] || fail "RELEASE_SSH_KNOWN_HOSTS is required with RELEASE_SSH_KEY"
mkdir -p "$HOME/.ssh"
chmod 700 "$HOME/.ssh"
(umask 077; printf '%s\n' "$RELEASE_SSH_KEY" > "$HOME/.ssh/release-key")
printf '%s\n' "$RELEASE_SSH_KNOWN_HOSTS" > "$HOME/.ssh/known_hosts"
cat > "$HOME/.ssh/config" <<EOF
mkdir -p "$dir"
chmod 700 "$dir"
(umask 077; printf '%s\n' "$RELEASE_SSH_KEY" > "$dir/key")
trap 'rm -f "$WORK/ssh/key"' EXIT
printf '%s\n' "$RELEASE_SSH_KNOWN_HOSTS" > "$dir/known_hosts"
cat > "$dir/config" <<EOF
Host $RELEASE_HOST
HostName ${RELEASE_SSH_ADDRESS:?RELEASE_SSH_ADDRESS is required}
User ${RELEASE_SSH_USER:?RELEASE_SSH_USER is required}
IdentityFile $HOME/.ssh/release-key
IdentityFile $dir/key
IdentitiesOnly yes
UserKnownHostsFile $dir/known_hosts
GlobalKnownHostsFile /dev/null
StrictHostKeyChecking yes
BatchMode yes
EOF
chmod 600 "$HOME/.ssh/config"
chmod 600 "$dir/config"
export RELEASE_SSH_CONFIG="$dir/config"
}
# Every package on the server and its checksum, to prove afterwards that