Commit graph

9 commits

Author SHA1 Message Date
Daniel Marin
0c070ae5f0
Merge pull request #6 from claude-hdb/fix/sshd-hardening-first-wins
fix(bootstrap): sshd hardening never applied on cloud images
2026-07-12 16:31:35 +01:00
6e6bf0dceb fix(bootstrap): validate the merged sshd config before bouncing the daemon
The previous commit restarted ssh and only checked `sshd -T` afterwards. On a
box whose only door is SSH, restarting against a config sshd refuses to parse
leaves no listener and no way back in — the same shape as the firewall-before-
bootstrap lockout this session already found in the migration runbook: commit
to the irreversible act, then verify.

Now `sshd -t` parses the MERGED config (our drop-in, cloud-init's, and any
third-party file) before the restart; on failure the drop-in is rolled back and
the daemon is left untouched. Verified: a bad neighbour drop-in exits 255 and
never reaches `systemctl restart`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 15:29:09 +00:00
77974d99d6 fix(bootstrap): sshd hardening never applied on cloud images
rig wrote its hardening drop-in as 99-rig.conf. Cloud images ship
50-cloud-init.conf carrying `PasswordAuthentication yes`, and sshd_config is
FIRST-wins ("for each keyword, the first obtained value will be used" —
sshd_config(5)) with Include expanding its glob in lexical order. So 50-
was read before 99- and every keyword rig set was silently discarded.

Every Hetzner box rig has bootstrapped was still serving
`passwordauthentication yes` — confirmed today on coolify-box (CX23) and
ci-runner (CX43) by `sshd -T`, and from off-tailnet by
`ssh -o PreferredAuthentications=none`. Root logins were never exposed
(PermitRootLogin resolved to prohibit-password via Debian's stock config),
but any password-bearing non-root account was reachable on a public port 22.

Three fixes:

1. Name the drop-in 00-rig.conf so it is read first and actually wins; sweep
   a stale 99-rig.conf on re-run so existing boxes converge.
2. Assert the EFFECTIVE config with `sshd -T` and refuse to claim a hardened
   box otherwise. Asserting that the file existed is what let this ship green
   — the Incus rehearsal has no cloud-init drop-in to lose to, so the bug was
   invisible to the one gate that could have caught it.
3. Set the system hostname, not just the tailnet one. A box reached as
   `coolify-box` greeted the operator as `root@internal-tooling`; the shell
   prompt is the only "am I on the right box" signal before a destructive
   command, and it was lying on every box rig built.

Also defer the pre-auth key prompt to the join path. rig is convergent by
contract, but re-running it to pick up this fix demanded a Tailscale key it
would never spend — friction on precisely the repair path that matters.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 15:26:34 +00:00
4b9cec210d fix: source /etc/os-release in a subshell — it clobbers $VERSION
On Debian, /etc/os-release defines VERSION="13 (trixie)". runner-install
sourced it into the main shell for the Debian-family guard, overwriting the
script's empty $VERSION: the latest-release resolution was skipped and the
download URL became .../v13 (trixie)/... -> curl (3) malformed URL. A
--version pin was clobbered the same way (guards run after arg parsing).

Read ID/ID_LIKE via a subshell in both runner-install and bootstrap (same
pattern, no collision there yet), and add a harness guard that fails on any
future main-shell sourcing of os-release.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 19:37:48 +00:00
e395d6754a feat: runner bootstrap role — defaults tag:ci, refuses tag:server
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 18:36:25 +00:00
5fb342d64d fix: bootstrap installs openssh-server — sshd_config.d does not exist on pristine images
Found by the Incus rehearsal (prod-migration Task 4): cloud images ship
openssh-server, container/VM images do not; the hardening drop-in and
'systemctl restart ssh' both presume it. A rig box is SSH-managed by
definition, so the dependency is explicit now. No-op on cloud images.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 10:38:14 +00:00
df17851108 refactor: rename deployor to rig; canonical heavy-duty/rig URLs 2026-07-11 08:25:48 +00:00
c19bb7d99a fix: die() prints only the message, not the exit code 2026-07-11 08:25:11 +00:00
aa4185f9ed feat: bootstrap command — hardening, unattended-upgrades, tailscale join 2026-07-11 08:25:11 +00:00