diff --git a/README.md b/README.md index aa2e176..468260c 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,15 @@ Persistent=true WantedBy=timers.target ``` +## Design decisions + +- [ADR 0001](docs/adr/0001-bash-with-shared-lib.md): plain Bash with `lib/common.sh`, so + the scripts run on a bare server. ShellCheck and bats keep it honest. +- [ADR 0002](docs/adr/0002-sshd-drop-in-validated.md): SSH settings go into a drop-in + file, checked with `sshd -t` before a reload. +- [ADR 0003](docs/adr/0003-restic-for-backups.md): restic for backups, because it does + encryption, deduplication and retention against any S3 bucket. + ## Limits - Debian and Ubuntu only (apt, UFW, systemd-journald). diff --git a/docs/adr/0001-bash-with-shared-lib.md b/docs/adr/0001-bash-with-shared-lib.md new file mode 100644 index 0000000..3d88f10 --- /dev/null +++ b/docs/adr/0001-bash-with-shared-lib.md @@ -0,0 +1,28 @@ +# 0001. Plain Bash with one shared library + +Status: accepted +Date: 2026-10-17 + +## Context +These scripts run on fresh Debian or Ubuntu servers, often before anything else is +installed, and on small fleets where one person does the ops work. They must be +readable by whoever inherits the server and runnable with nothing but the base system. + +## Options +1. Ansible playbooks: idempotent by design, good for many hosts. Needs Python and + Ansible on a control machine, and a playbook is overkill for a single VPS. +2. A Python or Go CLI: easier to test, but one more runtime or binary to ship. +3. Bash scripts with a shared `lib/common.sh` for logging, validation, dry-run and + alerts: runs everywhere, but Bash needs discipline to stay safe. + +## Decision +Option 3. Zero dependencies on the target host matters more here than the comfort of a +real language. The discipline is enforced by tooling, not by hope. + +## Consequences +- Every script starts with `set -euo pipefail` and validates its arguments. +- `run_cmd` in `common.sh` gives every script the same `DRY_RUN=1` preview. +- ShellCheck runs in CI on `bin/` and `lib/`, and 21 bats tests cover argument + handling, dry-run output and the checkers against local listeners. +- The bats suite cannot test the bootstrap or Docker install against a real VM. +- Revisit, and move to Ansible, once there are more than a handful of hosts to keep in sync. diff --git a/docs/adr/0002-sshd-drop-in-validated.md b/docs/adr/0002-sshd-drop-in-validated.md new file mode 100644 index 0000000..d54d2a7 --- /dev/null +++ b/docs/adr/0002-sshd-drop-in-validated.md @@ -0,0 +1,31 @@ +# 0002. Harden SSH through a validated drop-in file + +Status: accepted +Date: 2026-10-17 + +## Context +`bootstrap-server.sh` changes the SSH port, disables passwords and root login, and +limits logins to one admin user. A broken sshd config on a remote VPS can lock the +operator out, and editing `/etc/ssh/sshd_config` in place conflicts with package +upgrades that ship a new default file. + +## Options +1. Edit `sshd_config` with `sed`: works on any version, but fragile against + formatting changes and leaves a diff that apt asks about on upgrade. +2. Replace `sshd_config` with a full template: predictable, but throws away + distribution defaults and breaks on the next major release. +3. Write `/etc/ssh/sshd_config.d/10-hardening.conf`, run `sshd -t`, and reload only if + the test passes. + +## Decision +Option 3. The distribution keeps its own file, our settings live in one place, and an +invalid config is never loaded. + +## Consequences +- Depends on the `Include sshd_config.d/*.conf` line that current Debian and Ubuntu + ship. Older releases without it would ignore the drop-in. +- If `sshd -t` fails, the script stops but leaves the drop-in on disk. The next sshd + restart would then fail too. Removing the file on failure is an open improvement. +- UFW is configured right after the reload and opens the new port with `ufw limit`. + If UFW was already active with other rules, there is a short window where the new + port is closed. The README asks to keep a second session open for that reason. diff --git a/docs/adr/0003-restic-for-backups.md b/docs/adr/0003-restic-for-backups.md new file mode 100644 index 0000000..55a70ba --- /dev/null +++ b/docs/adr/0003-restic-for-backups.md @@ -0,0 +1,29 @@ +# 0003. Use restic for off-site backups + +Status: accepted +Date: 2026-10-17 + +## Context +Small servers need encrypted backups to cheap storage outside the host, with a +retention policy and an alert when a run fails. The storage is usually some +S3-compatible bucket, and the operator does not control the bucket provider. + +## Options +1. `tar` plus `rclone` or `aws s3 cp`: no new tool, but no deduplication, and + encryption and retention have to be scripted by hand. +2. BorgBackup: deduplicated and encrypted, but needs SSH access to a Borg server, not + plain object storage. +3. restic: one static binary, encrypted and deduplicated, talks to S3 directly, and + has built-in `forget --prune` retention. + +## Decision +Option 3. It works against any S3-compatible bucket and covers encryption, +deduplication and retention in one tool. + +## Consequences +- Config comes from env vars or an env file, which the README tells you to keep at mode 0600. +- Only snapshots tagged `auto` are pruned, so manual snapshots are kept. +- Defaults keep 7 daily, 4 weekly and 6 monthly snapshots. +- Losing `RESTIC_PASSWORD` means losing the backups. The key has to be stored somewhere + other than the server being backed up. +- `--check` exists but does not run on its own. Schedule it, or restore tests do not happen.