Take a fresh Ubuntu/Debian VPS to a hardened, Tailscale-sealed state — then, optionally, make it a dev box. A provider-agnostic, idempotent Bash toolkit that firewalls the box down to Tailscale-only ingress (key-only SSH, fail2ban, automatic security updates, kernel/sysctl hardening) and is safe to drive over SSH by a human or an agent.
The end state accepts inbound traffic only over Tailscale — the public internet sees no open ports. Every script prints the single next command when it finishes, so you copy-paste your way through the flow.
- Tailscale-only ingress. UFW default-deny with inbound allowed only on
tailscale0(IPv4 and IPv6). The public interface has no open ports. - Anti-lockout by design. The seal refuses to run unless Tailscale is up and your session is on the tailnet, sshd is asserted listening on the new port before the firewall closes, and password auth is never disabled while the user has no key.
- Idempotent + dry-run. Re-run any step any time; prefix any script with
CHECK=1to preview every change without touching the box. - Self-guiding. Each script prints the one command to run next, walking you through provision → harden → seal → verify → tooling → cleanup.
- Secrets hygiene.
config.envis git-ignored and never exported to child processes; the Tailscale auth key is passed via a0600tmpfs file (never on the command line);cleanup.shshreds it at the end. - Optional dev layer.
tooling.shadds Docker, Homebrew, uv, Python, and Node — kept deliberately out of the security core.
- A target VPS running Ubuntu 24.04 (22.04 also works; 24.04 gives a Python 3.12 baseline). Other distros are out of scope.
- A Tailscale account and an auth key — use an ephemeral, single-use, tagged key (generate one).
- An SSH keypair — the public key is authorized on the box; password SSH is disabled.
- For the Hetzner helper only: the authenticated
hcloudCLI. Other providers: create the box yourself with your SSH key for root, then start atsetup.sh.
Works on any Ubuntu/Debian VPS you can SSH into as root — bring your own box:
# 1. Configure (git-ignored) — your SSH public key, a sudo password, a Tailscale auth key
cp config.env.example config.env && $EDITOR config.env
# 2. Copy the toolkit onto a fresh Ubuntu box you can reach as root (any provider)
scp setup.sh verify.sh tooling.sh cleanup.sh config.env root@<host>:~/
ssh root@<host> 'chmod 600 ~/config.env && chmod +x ~/*.sh'
# 3. Harden + join the tailnet, then follow the "Next:" line each script prints
# (bootstrap → seal over Tailscale → verify → tooling → cleanup)
ssh root@<host> './setup.sh bootstrap'On Hetzner? COPY_TOOLKIT=true ./provision-hetzner.sh creates the box and copies
the toolkit for you in one step — see provisioning. Prefix
any script with CHECK=1 for a full dry-run (changes nothing).
New here? Follow the full guide — the complete end-to-end walkthrough from a fresh box to sealed, verified, and tooled.
| Script | Role |
|---|---|
provision-hetzner.sh |
Hetzner-only. Create the VPS, wait for SSH. Hand off to setup.sh. |
setup.sh |
Security hardening + Tailscale seal. The core. |
verify.sh |
Read-only posture audit (exits non-zero on any fail). |
tooling.sh |
Optional dev layer: Docker, Homebrew, uv, Python, Node. |
cleanup.sh |
Shred secrets + remove the toolkit from the box. |
ssh-alias.sh |
Local, optional. Add an ~/.ssh/config alias so you can ssh <name> over Tailscale. |
Start with the full guide for the end-to-end walkthrough, or the docs index for the "who starts where" map.
- Full guide — fresh box to sealed & tooled
- Security model & what gets hardened
- Configuration reference (
config.env) - Provisioning a Hetzner box (optional helper)
- Running remotely — SSH mechanics & gotchas
- Dev tooling layer
- Cleanup
- Troubleshooting & recovery
Working on the toolkit itself? See CLAUDE.md for conventions and the
anti-lockout invariants.