#!/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