Files
rollcall/deploy/rollcall-rack-update.sh
T
Ilker ManapandClaude Opus 5.5 2f708e8f69
CI / agent (push) Successful in 3s
CI / site (push) Successful in 3s
CI / go (push) Successful in 47s
Root rotation without re-joining: prepare, switch, retire
The CA directory holds the active root and, during a rotation, the next
or the previous one; everything that trusts the region gets the trust
bundle (all of them). Rack controllers receive it in their state, signed
by the active root; the root update unit installs it only when the
signature verifies against the roots it already trusts, and binary
signatures verify against any of them. After switch racks renew their
client and server certificates from the new root by themselves and
report what they run on; switch and retire wait for every rack (-force).

/ca.pem stays one certificate, the root of that server's certificate, and
the PXE command line names that root, so agents in older images keep
working; /ca-bundle.pem is the bundle. Deployed machines get one file per
root and a daily timer that refreshes them from their rack over HTTPS.

Refs #12

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 15:37:44 +01:00

88 lines
4.1 KiB
Bash
Executable File

#!/bin/sh
# Installs what the rack controller staged (rollcall-rack-update.path): a new
# trust bundle (root rotation) and a new rollcall binary.
#
# The staging directory belongs to the rack's unprivileged user, so nothing in
# it is trusted: files are copied into a directory of root's and installed only
# if the region's signature over them verifies against one of the roots in the
# CA file (root's, from rack.json ca_file; written at join and by this script).
# A bundle replaces the CA file and restarts the rack. A binary is kept as
# .prev, the rack restarted, and the previous binary put back if the rack does
# not register with the region in 60 s.
set -u
D=/var/lib/rollcall/update
BIN=/usr/local/bin/rollcall
[ -f "$D/ready" ] || [ -f "$D/ca-ready" ] || exit 0
# root's work directory, on a file system that allows executing (/run is noexec)
install -d -m 0700 -o root -g root /var/lib/rollcall-update
W=$(mktemp -d /var/lib/rollcall-update/w.XXXXXX) || exit 1
trap 'rm -rf "$W"' EXIT
# last-result is read by the rack (backoff); renamed in, so a link planted there is replaced, not followed
result() { echo "$(date -Is) $*" > "$W/result" && mv -fT "$W/result" "$D/last-result"; echo "$*"; }
CA=$(jq -r '.ca_file // "/etc/rollcall/rack/ca.pem"' /etc/rollcall/rack.json 2>/dev/null)
[ -n "$CA" ] && [ "$(stat -c %u "$CA" 2>/dev/null)" = 0 ] || { result "no root-owned CA certificate ($CA), nothing installed"; exit 1; }
# split PEM certificates of $1 into $2.1, $2.2, ...
splitpem() { awk -v p="$2" '/-----BEGIN CERTIFICATE-----/{n++; f=p "." n} f{print > f} /-----END CERTIFICATE-----/{close(f); f=""}' "$1"; }
# signed FILE SIG: the signature verifies against one of the roots in $CA
signed() {
rm -f "$W"/root.*; splitpem "$CA" "$W/root"
for c in "$W"/root.*; do
[ -f "$c" ] || continue
openssl x509 -in "$c" -pubkey -noout > "$W/pub" 2>/dev/null &&
openssl dgst -sha256 -verify "$W/pub" -signature "$2" "$1" >/dev/null 2>&1 && return 0
done
return 1
}
if [ -f "$D/ca-ready" ]; then
rm -f "$D/ca-ready"
if cp -- "$D/ca-bundle.pem" "$W/bundle" && cp -- "$D/ca-bundle.sig" "$W/bundle.sig"; then
if ! signed "$W/bundle" "$W/bundle.sig"; then
result "trust bundle: signature does not verify against $CA, not installed"
else
# only CA certificates, rewritten from what was parsed
rm -f "$W"/new.*; splitpem "$W/bundle" "$W/new"; : > "$W/ca.new"; n=0; ok=1
for c in "$W"/new.*; do
[ -f "$c" ] || continue
openssl x509 -in "$c" -noout -text 2>/dev/null | grep -q "CA:TRUE" || ok=
openssl x509 -in "$c" >> "$W/ca.new" 2>/dev/null || ok=
n=$((n+1))
done
if [ -z "$ok" ] || [ $n -eq 0 ]; then
result "trust bundle: not CA certificates only, not installed"
else
cp -p "$CA" "$CA.prev"
install -m 0644 -o root -g root "$W/ca.new" "$CA.tmp" && mv -f "$CA.tmp" "$CA"
rm -f "$D/ca-bundle.pem" "$D/ca-bundle.sig"
systemctl restart rollcall-rack.service
result "trust bundle installed ($n roots)"
fi
fi
fi
fi
[ -f "$D/ready" ] || exit 0
want=$(head -c 64 "$D/ready" | tr -cd 'A-Za-z0-9._+-'); rm -f "$D/ready"
cp -- "$D/rollcall.new" "$W/bin" && cp -- "$D/rollcall.new.sig" "$W/sig" || { result "$want: staged files missing, not installed"; exit 1; }
signed "$W/bin" "$W/sig" || { result "$want: signature does not verify against $CA, not installed"; exit 1; }
chmod 0755 "$W/bin"
got=$("$W/bin" version 2>/dev/null | awk '{print $2}')
[ "$got" = "$want" ] || { result "$want: binary reports '$got', not installed"; exit 1; }
cur=$("$BIN" version 2>/dev/null | awk '{print $2}')
cp -p "$BIN" "$BIN.prev"
install -m 0755 "$W/bin" "$BIN.tmp" && mv -f "$BIN.tmp" "$BIN"
rm -f "$D/rollcall.new" "$D/rollcall.new.sha256" "$D/rollcall.new.sig"
since=$(date +%s)
systemctl restart rollcall-rack.service
for _ in $(seq 1 30); do
sleep 2
if journalctl -u rollcall-rack.service --since "@$since" -o cat 2>/dev/null | grep -q "^.*registered with"; then
result "updated $cur -> $want"
exit 0
fi
done
cp -p "$BIN.prev" "$BIN.tmp" && mv -f "$BIN.tmp" "$BIN"
systemctl restart rollcall-rack.service
result "$want did not register within 60s; rolled back to $cur"
exit 1