#!/usr/bin/env bash # Shared sshd hardening — sourced by bootstrap.sh (machine roles) and by # bootstrap-tenant.sh (the staging tenant). Root-requiring, unlike the pure # parsing libs: it converges /etc/ssh and bounces the daemon. Extracted so the # two roles converging ONE drop-in stay literally the same code — two copies of # a hardening block is drift by construction, the same law that keeps rig's # hands off Incus. Callers provide log/warn/die. # harden_sshd — install the 00-rig.conf hardening drop-in, # validate the merged config before touching the daemon, restart only when the # drop-in actually changed, and assert the EFFECTIVE config (sshd -T), with the # permitrootlogin acceptance gated on the class passed in. harden_sshd() { local class="$1" local dropin=/etc/ssh/sshd_config.d/00-rig.conf local legacy_dropin=/etc/ssh/sshd_config.d/99-rig.conf local tmp backup eff # The name must sort BEFORE cloud-init's drop-in. sshd_config is FIRST-wins # ("for each keyword, the first obtained value will be used" — sshd_config(5)), # and Include expands the glob in lexical order. Cloud images ship # /etc/ssh/sshd_config.d/50-cloud-init.conf carrying `PasswordAuthentication # yes`, so the old 99-rig.conf was read second and silently lost every keyword # it set. 00- wins. (Found 2026-07-12: every Hetzner box rig had bootstrapped # was still serving `passwordauthentication yes`. The Incus rehearsal never # caught it — a pristine Debian container has no cloud-init drop-in.) tmp="$(mktemp)" cat > "$tmp" <<'EOF' PermitRootLogin prohibit-password PasswordAuthentication no EOF if ! cmp -s "$tmp" "$dropin" 2>/dev/null || [ -e "$legacy_dropin" ]; then backup="" [ -e "$dropin" ] && { backup="$(mktemp)"; cp -a "$dropin" "$backup"; } install -m 0644 "$tmp" "$dropin" rm -f "$legacy_dropin" # sweep the losing file from already-bootstrapped boxes # Validate the MERGED config BEFORE bouncing the daemon. On a box whose only # door is SSH, `systemctl restart ssh` against a config sshd refuses to parse # leaves no listener and no way back in. `sshd -t` parses everything sshd # would parse — our drop-in, cloud-init's, and any third-party file — so a # broken neighbour is caught here rather than after the door has shut. if ! sshd -t 2>/dev/null; then if [ -n "$backup" ]; then cp -a "$backup" "$dropin"; else rm -f "$dropin"; fi rm -f "$tmp" "$backup" die "sshd rejects the merged config; drop-in rolled back, daemon untouched. Run 'sshd -t' to see which file is bad." fi rm -f "$backup" systemctl restart ssh log "sshd hardening drop-in installed" else log "sshd hardening drop-in already in place" fi rm -f "$tmp" # Assert the EFFECTIVE config, not the file's existence — asserting the file is # what let the first-wins bug ship green. `sshd -T` is what the daemon actually # resolved, cloud-init and all. eff="$(sshd -T 2>/dev/null)" || die "sshd -T failed; refusing to claim a hardened box" echo "$eff" | grep -qx 'passwordauthentication no' \ || die "sshd still resolves passwordauthentication=yes — a drop-in is beating ${dropin}; check ls /etc/ssh/sshd_config.d/" # The permitrootlogin acceptance is CLASS-gated, because `no` means opposite # things on the two classes. class=human: `no` is the post-`rig users # close-root` state — strictly harder than the prohibit-password this function # installs. Hardening must never read a closed door as a broken one, and it # cannot reopen one either: by first-wins its own drop-in loses to # 00-rig-users.conf. class=server: root SSH is the control plane's automation # door (Coolify SSHes in as root), so `no` is not hardening — it is fleet # management silently dead, and the likely culprit is a drop-in left over from # a former class=human life on a repurposed box. rig can DETECT that but must # not FIX it: silently reopening a root door is worse than a loud stop, so — # same doctrine as the tag checks — detect, refuse, and name the repair. if [ "$class" = "human" ]; then echo "$eff" | grep -qxE 'permitrootlogin (no|prohibit-password|without-password)' \ || die "sshd still permits root password login — check ls /etc/ssh/sshd_config.d/" elif echo "$eff" | grep -qx 'permitrootlogin no'; then die "sshd resolves permitrootlogin=no, but this is a class=server box: root SSH is the control plane's automation door, and with it shut the fleet cannot manage this box. Likely cause: a leftover /etc/ssh/sshd_config.d/00-rig-users.conf from a former class=human life ('rig users close-root' ran here once). Remove that drop-in and re-run bootstrap." else echo "$eff" | grep -qxE 'permitrootlogin (prohibit-password|without-password)' \ || die "sshd still permits root password login — check ls /etc/ssh/sshd_config.d/" fi log "sshd hardening verified (sshd -T: passwordauthentication no)" }