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>
88 lines
4.1 KiB
Bash
Executable File
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
|