Files
rollcall/deploy/rollcall-dnsmasq-reload.sh
T
Ilker ManapandClaude Opus 5.5 a549f01ef5
CI / agent (push) Successful in 3s
CI / site (push) Successful in 2s
CI / go (push) Successful in 34s
Security review: fix traversal, identity, injection and takeover paths
Findings from a full review (auth/API, CA and rack channel, config
generation, integrations, UI/agents/scripts), each verified in code:

- rack binary endpoint read any region file via arch=../.. (CA key)
- render endpoint traversal; machine-reported MACs used as file paths
- delegated intermediates could mint admin/rack identities and IPs;
  identities now require root-issued certs, intermediates are
  ServerAuth-only with both name types constrained
- rack certs were ServerAuth for rack-chosen SANs: now client-only;
  racks accept only the region's own cert; join checks the CA
  fingerprint; unbound join codes cannot take an existing name; job
  results bound to the rack that took them
- rack self-update: region signs binaries with the CA key, the root
  unit verifies against the join-time CA in a root-owned dir
- dnsmasq reads a checked, root-owned copy limited to generated options
- commissioning could overwrite deployed machines; MAC takeover of PXE
- injection into dnsmasq/cloud-init/PXE/udev/DNS via names and
  machine-reported strings; stored XSS in the UI; CSP and cross-origin
  protection; deploy agent shell injection and seed traversal
- Redfish requests confined to the BMC's /redfish/ tree
- admin_http defaults to loopback; body limits and timeouts; secrets
  off command lines

Also: IPAM claim/reconcile race, sync stopping on one error, watchdog
race, release wiping the wrong disk, SAN LUNs as auto targets.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 17:58:42 +01:00

86 lines
3.7 KiB
Bash

#!/bin/sh
# Called by rollcall-dnsmasq.path when rollcall rewrites dnsmasq's files.
#
# Host files (leases, names) only need a HUP. When the generated configuration
# changed, dnsmasq is restarted. If it does not come back (a conflicting
# hand-written file in /etc/dnsmasq.d, for instance - `dnsmasq --test` misses
# some of those), the last configuration that worked is put back and dnsmasq
# is started with it, so DHCP stays up. The rejected content is kept next to it
# and not tried again until it changes.
#
# rollcall (an unprivileged user) writes $SRC; dnsmasq starts as root and reads
# only the copy in $LIVE, a directory of root's. The copy is made without
# following links and holds only the options rollcall generates: nothing that
# runs a program, includes another file or changes dnsmasq's user.
set -u
SRC=/var/lib/rollcall/dnsmasq/conf.d/rollcall.conf
LIVE=/var/lib/rollcall-dnsmasq
CONF=$LIVE/rollcall.conf
GOOD=$LIVE/rollcall.conf.accepted
BAD=$LIVE/rollcall.conf.rejected
STAMP=/run/rollcall-dnsmasq.sum
ALLOWED='addn-hosts bind-dynamic bogus-priv dhcp-authoritative dhcp-boot dhcp-hostsfile dhcp-match dhcp-option dhcp-range domain domain-needed enable-ra enable-tftp expand-hosts interface local log-dhcp no-resolv pxe-service ra-param server tftp-root'
install -d -m 0755 -o root -g root "$LIVE"
umask 022
sum() { cat "$1" 2>/dev/null | sha256sum | cut -d' ' -f1; }
put() { cp "$1" "$CONF.tmp" && mv -f "$CONF.tmp" "$CONF"; } # within $LIVE: root's
restore() { # put the last working configuration back (or none)
if [ -s "$GOOD" ]; then put "$GOOD"; else rm -f "$CONF"; fi
sum "$CONF" > "$STAMP"
}
# take rollcall's file: a regular file, not a link, at most 1 MB
cand=$LIVE/candidate.tmp
rm -f "$cand"
if [ -e "$SRC" ]; then
if ! dd if="$SRC" of="$cand" iflag=nofollow bs=1M count=1 status=none 2>/dev/null; then
echo "$SRC is not a regular file; ignored" >&2
exit 1
fi
printf '%s\n' $ALLOWED > "$LIVE/allowed.tmp"
bad=$(grep -vE '^[[:space:]]*(#|$)' "$cand" | sed -E 's/^[[:space:]]*([^=[:space:]]*).*/\1/' | sort -u |
grep -vxFf "$LIVE/allowed.tmp" | tr '\n' ' ')
rm -f "$LIVE/allowed.tmp"
if [ -n "$bad" ]; then
rm -f "$cand"
echo "generated dnsmasq configuration has options rollcall does not write ($bad); not used" >&2
exit 1
fi
fi
new=$(sum "$cand")
if [ "$new" = "$(cat "$STAMP" 2>/dev/null)" ] && { [ -e "$CONF" ] || [ ! -e "$cand" ]; }; then
rm -f "$cand"
systemctl kill -s HUP dnsmasq.service
exit 0
fi
if [ -e "$cand" ] && [ "$new" = "$(sum "$BAD")" ]; then
rm -f "$cand"
restore
echo "generated dnsmasq configuration was rejected before and has not changed; keeping the last working one" >&2
exit 1
fi
if [ -e "$cand" ]; then mv -f "$cand" "$CONF"; else rm -f "$CONF"; fi
if out=$(dnsmasq --test 2>&1) && systemctl restart dnsmasq.service; then
echo "$new" > "$STAMP"
if [ -e "$CONF" ]; then cp "$CONF" "$GOOD"; else rm -f "$GOOD"; fi
rm -f "$BAD"
echo "dnsmasq restarted: generated configuration changed"
exit 0
fi
why=$(journalctl -u dnsmasq.service -n 5 --no-pager -o cat 2>/dev/null | grep -v '^$' | tail -3 | tr '\n' ' ')
[ -e "$CONF" ] && cp "$CONF" "$BAD"
restore
if systemctl restart dnsmasq.service; then
echo "generated dnsmasq configuration rejected ($out $why); dnsmasq runs with the last working one, the rejected file is $BAD" >&2
exit 1
fi
# not even that starts: something outside rollcall's file conflicts. Run dnsmasq
# without rollcall's file so whatever else is configured keeps working.
rm -f "$CONF"
sum "$CONF" > "$STAMP"
systemctl restart dnsmasq.service || true
echo "dnsmasq does not start with rollcall's configuration ($why); started without it - check /etc/dnsmasq.conf and /etc/dnsmasq.d for conflicting options" >&2
exit 1