Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
28 changes: 28 additions & 0 deletions docs/adr/0001-bash-with-shared-lib.md
Original file line number Diff line number Diff line change
@@ -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.
31 changes: 31 additions & 0 deletions docs/adr/0002-sshd-drop-in-validated.md
Original file line number Diff line number Diff line change
@@ -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.
29 changes: 29 additions & 0 deletions docs/adr/0003-restic-for-backups.md
Original file line number Diff line number Diff line change
@@ -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.
Loading