This chapter is a project. Starting from a blank Ubuntu server with only a root password or cloud key, you will end with a hardened machine that serves an application over HTTPS, restarts it on failure, backs itself up nightly, and can be rebuilt from a script. Every step uses a command from an earlier lesson; the value here is the order, the checks between steps, and the habit of writing down what you did. Budget an hour, use a throwaway cloud VM, and destroy it when you are done.
Before touching the server, write a short runbook — a text file in a repository — containing the hostname, IP, provider, purpose, the user you will create, and the ports you intend to expose. Add every command you run to it as you go. Six months from now this file is the difference between a five-minute rebuild and a lost weekend.
ssh root@203.0.113.10 # or ubuntu@ with a cloud key
cat /etc/os-release && uname -r
apt update && apt -y upgrade
[ -f /var/run/reboot-required ] && reboot
hostnamectl set-hostname web-01
timedatectl set-timezone UTCCheck: hostname prints web-01, date shows UTC, apt list --upgradable is empty.
adduser alice
usermod -aG sudo alice
rsync --archive --chown=alice:alice ~/.ssh /home/alice # copy root's authorized keyFrom your laptop, confirm the new account works before locking down SSH:
ssh alice@203.0.113.10 'sudo -n true && echo sudo-ok'Then, on the server:
sudo nano /etc/ssh/sshd_config.d/hardening.confPasswordAuthentication no
PermitRootLogin no
KbdInteractiveAuthentication no
MaxAuthTries 3sudo sshd -t && sudo systemctl reload sshCheck: a second terminal can still log in as alice; ssh root@... is refused.
sudo apt install -y ufw fail2ban unattended-upgrades
sudo ufw default deny incoming && sudo ufw default allow outgoing
sudo ufw limit OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable
sudo ufw status verbose
printf '[DEFAULT]\nbantime = 1h\nfindtime = 10m\nmaxretry = 5\n\n[sshd]\nenabled = true\n' | sudo tee /etc/fail2ban/jail.local
sudo systemctl enable --now fail2ban
sudo dpkg-reconfigure -plow unattended-upgradesCheck: sudo ss -tulpn shows only 22 listening (and later 80/443); sudo fail2ban-client status sshd reports the jail active.
Follow the previous lesson: install Node and Nginx, create the app system user, clone the code into /srv/app/current, write app.service, enable it, configure the Nginx site, run certbot. Then verify from outside:
curl -sI https://app.example.com | head -1 # HTTP/2 200
curl -s https://app.example.com/health # {"status":"ok"}
sudo systemctl is-active app nginx # active activeDeliberately break something to see the safety net work: sudo kill -9 $(systemctl show -p MainPID --value app) and watch journalctl -u app -f show the restart within five seconds.
sudo mkdir -p /backups && sudo chown alice:alice /backups
cat > /home/alice/bin/backup.sh <<'EOF'
#!/usr/bin/env bash
set -Eeuo pipefail
today=$(date +%F)
dest=/backups
rsync -a --delete --link-dest="$dest/latest" /srv/app/current/ "$dest/$today/app/"
ln -sfn "$dest/$today" "$dest/latest"
find "$dest" -maxdepth 1 -type d -name '20*' -mtime +14 -exec rm -rf {} +
echo "backup $today ok"
EOF
chmod +x /home/alice/bin/backup.shSchedule it with a systemd timer at 02:30 with Persistent=true (cron works too), run it once by hand, and restore one file from /backups/latest to /tmp to prove it. Then sync /backups off the machine with rsync to a second host or object storage.
The proof that the runbook is complete is a second server built from it. Turn Phases 1–3 into bootstrap.sh (see the sample above), create a fresh VM, run the script, and time how long it takes to reach a green health check. Anything you had to do by hand is a missing line in the script. Destroy both VMs when finished.
| Check | Command | Expected |
|---|---|---|
| Root SSH blocked | ssh root@host | Permission denied |
| Password SSH blocked | ssh -o PubkeyAuthentication=no alice@host | Permission denied |
| Only 22/80/443 open | sudo ss -tulpn | three listeners |
| HTTPS valid | curl -sI https://app.example.com | HTTP/2 200 |
| App restarts on crash | kill the PID, systemctl status app | active within 5 s |
| Backup exists and restores | ls /backups/latest, copy a file | matches source |
| Updates automatic | cat /etc/apt/apt.conf.d/20auto-upgrades | both "1" |
| fail2ban active | sudo fail2ban-client status sshd | jail listed |
sudo.ufw before allowing SSH./root or /home/alice instead of /srv/app.What should you verify immediately before disabling password authentication in `sshd_config`?
rsync --link-dest, schedule it, restore a file, and copy backups off the machine.bootstrap.sh; the rebuild test is the real acceptance criterion.Next lesson: Linux Command Line Cheat Sheet and Interview Questions — consolidate the whole course into a one-page reference and practise the questions interviewers ask.