Files
homelab/active/device_raspberry_pi/systemd
ducoterra e94d4f718c add README index for the Raspberry Pi systemd runbooks
Ties the two runbooks together as one host-agnostic directory: which runbook
solves which problem, a file -> target-path map for the units, and the shared
deploy pattern (one tar | ssh pipe into /root, then install with explicit
modes rather than copying a whole tree into /etc).

States the conventions both units follow - configs under /usr/local as the
source of truth with thin systemd wrappers, idempotent installers that re-derive
desired state and never delete what they did not create, and gap-limited
self-healing so a broken dependency cannot make a unit flap.

Also documents the redaction recipe for pasting a WireGuard config: match
`PrivateKey[[:space:]]*=` because wg-quick strip emits spaces around the `=`,
which a `PrivateKey=` regex misses.

LLM-Generated: true
2026-10-16 09:08:16 -04:00
..

Raspberry Pi — boot-time hardening units

Two reusable runbooks for a headless Pi that must stay reachable over SSH. Deliberately host-agnostic: no addresses, key material, or peer identities live in this repo — those stay on each host. Set HOST and fill in the placeholders from the runbook before running anything.

Runbook Solves
root-ssh-key.md root authorized_keys gets wiped (imaging, config management) → key is re-installed at boot and re-applied on change
wireguard.md SSH through a WireGuard tunnel address fails after reboot / NAT timeout / roaming to another network

Verified against Debian 13 (trixie) with systemd 257 and NetworkManager. The units are plain POSIX sh + systemd, so they carry to any other distribution with the same systemd version or newer.

Layout → target path on the host

File in this directory Installs to
ensure-root-ssh-key /usr/local/sbin/ensure-root-ssh-key (0755)
ensure-root-ssh-key.service /etc/systemd/system/
ensure-root-ssh-key.path /etc/systemd/system/
root-ssh-keys/authorized_keys.example /usr/local/share/root-ssh-keys/authorized_keys (0644) — copy, fill in your public key(s)
wg0-watchdog /usr/local/sbin/wg0-watchdog (0755)
wg0-watchdog.service /etc/systemd/system/
wg0-watchdog.timer /etc/systemd/system/
wg-quick@wg0.service.d-10-boot-resilience.conf /etc/systemd/system/wg-quick@wg0.service.d/10-boot-resilience.conf
wg0.conf.template documents /etc/wireguard/wg0.conf (0600) — the real file is never copied here

Deploy pattern

Every command in both runbooks follows the same shape, so a fresh Pi needs only HOST and the placeholders:

HOST=root@<pi-address>
D=$PWD   # run the snippets from a checkout of this directory

Files travel over one tar | ssh pipe into /root, then install moves them into place with explicit modes (no scp of a whole tree into /etc, no ambiguous ownership). Reload and enable once at the end:

ssh "$HOST" 'systemctl daemon-reload && systemctl enable --now <units>'

Conventions worth keeping on any host

  • Config files under /usr/local/{sbin,share} are the source of truth; systemd units stay thin wrappers so they can be re-imaged independently.
  • Idempotent installers over clever state: every script re-derives the desired state from its own config file and never deletes what it did not create.
  • Anything that must heal itself is gap-limited (a minimum interval between restarts) so a broken dependency cannot make a unit flap.
  • Private keys and addresses are excluded from the repo; when dumping a WireGuard config, redact with a regex that tolerates PrivateKey = spacing (wg-quick strip emits spaces, a PrivateKey= regex misses them): sed -E 's/^(PrivateKey[[:space:]]*=).*/\1 [REDACTED]/'