Skip to content

Architecture

github-actions[bot] edited this page Nov 10, 2026 · 10 revisions

How server-prep is built: three bash scripts, a step model with safe helpers, a read-only audit, and a setup console generated from the scripts.

The three scripts

server-prep is plain bash. There is nothing to build or install. Each script is a single file, so it still works when you download it alone from a release.

Script When you run it What it does
server-preconfig.sh First, on a fresh server SSH and sudo for your admin user, the hostname, a network config that survives reboots, no sleep, and Tailscale. It ends with a reboot-readiness check (--check).
server-prep.sh Second Secures the server and prepares it for your panel, step by step. It asks questions, or reads a config file.
server-audit.sh Before and after, and any time Read-only security and panel-readiness audit. It changes nothing.

The order is always the same: preconfig, audit, prep in dry run, prep, audit again. Start here walks through it.

server-prep.sh calls the audit itself, before and after its steps. It looks for server-audit.sh next to itself, then at /usr/local/sbin/server-audit. The report then shows a before/after table for every check.

The step model

Every change in server-prep.sh happens inside a step. A step is one bash function named step_<name>(), for example step_firewall(). Today there are 20 steps, in this order:

update users docker firewall ssh ips unattended sysctl auth services laptop logging auditd rkhunter aide endlessh telegraf telegram cloudflared suid

The README has one line per step. You can print the names yourself.

./server-prep.sh --list-steps

You see the 20 step names on one line, in run order.

A step is listed in two places in the script:

  • the ALL_STEPS variable near the top, which sets the names and their order;
  • the run_step <name> "<Title>" step_<name> list in main(), which runs them.

run_step decides the result of each step from its return code:

The step... Result in the report
returns 0 and changed something CHANGED (or WOULD CHANGE in a dry run)
returns 0 and changed nothing OK (already)
returns $SKIP_RC (20), because you turned it off SKIPPED
returns anything else FAILED

Inside a step, three small helpers record what happened:

  • note "...": a line in the step's Details column in the report.
  • changed: marks the step as CHANGED.
  • todo "...": a manual follow-up, printed at the end under To do by hand.

You choose which steps run with --only ssh,firewall or --skip aide,endlessh.

The helpers every change goes through

A step never writes to the system directly. It uses a small set of helpers. This is what makes dry runs, backups and re-runs safe.

Helper What it does
run CMD... Runs a command and sends its output to the log. In a dry run, it only prints the command.
run_secret SECRET CMD... Like run, for a command that takes a token or key: the log and the dry-run output show SECRET as ***.
write_file PATH MODE Writes a file from stdin, only when the content is different. It backs up the old file first.
set_kv FILE KEY VALUE Sets one key = value line in a config file, replacing a commented or old line. It uses write_file.
backup_file PATH Copies a file into the backup folder, once per run.
pkg_install PKG... Installs only the packages that are missing, with "${APT_OPTS[@]}" (it waits up to 10 minutes for the apt lock).
svc_disable UNIT... Disables and stops a service, only when it is enabled or running.

Idempotency

Idempotent means you can run the same thing twice and the second run changes nothing. Each step checks the current state first and only changes what is missing. write_file skips identical content and pkg_install skips installed packages. So you can re-run server-prep after an OS upgrade, or to change one setting. See the Glossary for this word and others.

Dry run

--dry-run previews every change and changes nothing. run prints [dry-run] <command> and write_file prints would write <path>. You can even dry-run without root, with partial detection. The report then goes to the current folder with a -dryrun suffix.

sudo ./server-prep.sh --dry-run

You see the questions, the plan, then each step marked WOULD CHANGE or OK (already).

Safety nets

  • The plan is shown before anything changes, and you confirm it.
  • The SSH step starts a 10-minute auto-rollback. If you do not confirm that a new login works, the old SSH config comes back.
  • Every run writes a rollback.sh that restores the changed files and removes the new ones. It does not remove installed packages or re-enable services. Read it before you run it.

Where things go

All outputs are readable by root only.

What Where
Report (Markdown) /root/server-prep-reports/server-prep-report-<host>-<date>.md, plus a copy in the home of the user who ran sudo
Report (JSON) the same path ending in .json (same facts for scripts and agents, no secrets), plus a copy in the home of the user who ran sudo
Log /var/log/server-prep/run-<date>.log
Backups and rollback.sh /var/lib/server-prep/backups/<date>/
Audit results before and after /var/lib/server-prep/audit-<date>-before.* and ...-after.*
Saved answers (no secrets) /etc/server-prep/server-prep.conf

The saved answers let you re-run one step later with the same settings.

sudo ./server-prep.sh --config /etc/server-prep/server-prep.conf --only firewall --yes

You see only the firewall step run, then a short summary with its result.

Secrets (the Cloudflare tunnel token, the Telegram bot token, the CrowdSec enroll key) are never saved in that file.

The audit

server-audit.sh is a rework of vps-audit. It reads the real state of the server, for example sshd's effective config with sshd -T, and the firewall together with each port's bind address.

Each check calls result <id> <status> "<title>" "<detail>" "<fix hint>". The status is one of:

  • PASS: good.
  • WARN: worth a look, sometimes expected.
  • FAIL: a real problem. The script exits with code 1 when at least one check fails.
  • INFO: for optional features. INFO never lowers the score.

There are 59 checks in 11 sections: System, Updates, SSH, Firewall & network exposure, Intrusion prevention, Accounts & authentication, Kernel & system settings, Logging & monitoring, Services, Resources, and Panel readiness. Check ids start with their section, for example ssh.* or acc.password_dict.

The audit writes a text report by default, and can also write Markdown (--md), JSON (--json) and TSV (--tsv). server-prep.sh reads the TSV for its before/after table, so those columns must stay stable.

sudo ./server-audit.sh --md audit.md --json audit.json

You see the checks scroll by in color, then a PASS / WARN / FAIL summary, and the two report files appear in the current folder.

Setup console: from scripts to GitHub Pages

The setup console shows what server-prep does for your setup: VPS or dedicated, your panel, your access route. Its facts are not written by hand. They come from the scripts.

  1. scripts/gen-data.sh reads server-prep.sh, server-audit.sh, server-preconfig.sh, the README tables and server-prep.conf.example.
  2. It writes site/data.json and site/data.js (the same data, loaded by the page so it also works offline). The output has no timestamps, so CI can compare it.
  3. site/index.html is one static page that reads that data. Its colours and type come from site/tokens.css, which the brand guidelines page (site/brand/) shares. No build step, no CDN, no tracking.
  4. The pages workflow publishes site/ to GitHub Pages.

After you change a step, a setting or an audit check, regenerate the data and commit it.

scripts/gen-data.sh

It prints wrote site/data.json and site/data.js (20 steps, ... settings, 59 audit checks) and updates both files. It needs jq. In CI, scripts/gen-data.sh --check prints nothing and exits 0 when the data is up to date, or prints site/data.json is out of date with a diff.

To preview the page on your machine:

python3 -m http.server --bind 127.0.0.1 -d site 8000

Then open http://localhost:8000 in your browser.

CI workflows

Five workflows live in .github/workflows/.

Workflow When What it does
lint Every push to main and every PR bash -n on every script, ShellCheck at -S warning, each script's --help, scripts/gen-data.sh --check (fails when the site data is out of date), and the quick tests in tests/
e2e PRs and pushes to main that change a script or the tests; every Monday; or by hand Boots each supported system in a KVM virtual machine from its official cloud image and runs the scripts like an operator: preconfig, dry run, interactive questions, real run, SSH and firewall checks from outside, a second run, a reboot, and the audit at each stage. See docs/e2e-tests.md
pages Push to main that touches the site, the scripts, the README or the config example; when an e2e run ends; when a PR closes; or by hand Checks the site data again, rebuilds the end-to-end reports (the last run of main and of each open PR) under /e2e/, and deploys everything to GitHub Pages. It runs from main and never runs PR code
wiki Push to main that touches wiki/; or by hand Publishes wiki/ to the GitHub Wiki
release By hand only (the release button) Bumps the version, updates the changelog and the site data in one commit put on main by the release app (the one exception to main's required approval), tags, and publishes the release with SHA256SUMS. See Maintainer handbook.

More checks are planned: the lint and release workflows running scripts/check.sh with its pinned ShellCheck (#34), a consistency check that keeps steps, README, config example and audit in sync (#35), unit tests (#37), and secret scanning (#40).

Architecture choices are recorded as ADRs (architecture decision records) in docs/decisions/. Its README says when to write one. How decisions are made: GOVERNANCE.md.

Clone this wiki locally