Skip to content

Hardening

The essentials for a single-box deployment — firewall, SSH keys, automatic security updates. This isn’t a full security audit; it’s the baseline below which you should not operate. Ten minutes, the same day you deploy.

  1. Firewall (ufw): only SSH, HTTP, HTTPS.

    Terminal window
    apt-get -y install ufw
    ufw allow OpenSSH
    ufw allow 80/tcp
    ufw allow 443/tcp
    ufw allow 443/udp # HTTP/3
    ufw --force enable
    ufw status verbose

    Everything else (Postgres, Gunicorn, …) already listens only on Docker’s internal network — nothing but Caddy publishes ports — so the firewall is a second fence, not the only one.

    Hostinger’s hPanel and DigitalOcean’s Cloud Firewalls can enforce the same rules one layer out; if offered, mirror them there too (allow 22, 80, 443).

  2. SSH keys instead of passwords.

    On your laptop (skip if you already have ~/.ssh/id_ed25519.pub):

    Terminal window
    ssh-keygen -t ed25519 -C "you@example.com" # accept the defaults
    ssh-copy-id root@YOUR_VPS_IP # copies the public key over
    ssh root@YOUR_VPS_IP # must log in WITHOUT a password now

    Then, on the server, turn password logins off:

    Terminal window
    sed -i 's/^#\?PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config
    sed -i 's/^#\?KbdInteractiveAuthentication.*/KbdInteractiveAuthentication no/' /etc/ssh/sshd_config
    systemctl restart ssh
  3. Automatic security updates.

    Terminal window
    apt-get -y install unattended-upgrades
    dpkg-reconfigure -plow unattended-upgrades # choose "Yes"

    Ubuntu will now install security patches by itself. Kernel updates still need an occasional reboot — a systemctl reboot during a quiet hour every month or two is fine; the stack starts itself (restart: unless-stopped + auto-migrate on boot).

The application side (already done — verify, don’t configure)

Section titled “The application side (already done — verify, don’t configure)”
  • .env is the only secrets file — it never enters the Docker image and is git-ignored. Don’t commit it, don’t email it.
  • Production settings refuse DEBUG=True, a weak/default DJANGO_SECRET_KEY, and wildcard ALLOWED_HOSTS at boot — if you see such an error in docker compose logs web, that’s the guardrail working.
  • HTTPS is enforced end-to-end (Caddy redirects HTTP; Django sets HSTS).
  • Optional but recommended: error reporting — set ENABLE_SENTRY=True and SENTRY_DSN=... in .env (the free Sentry tier is fine), then docker compose up -d web.
Terminal window
ufw status | grep -E "80|443|22" # the three allowed ports
# password login must FAIL now (this forces password auth, skipping your key):
ssh -o PubkeyAuthentication=no -o PreferredAuthentications=password root@YOUR_VPS_IP
curl -sI http://api.your-domain.com/health/ | head -1 # 308 → HTTPS redirect