Hardening a Production Linux VPS: SSH, UFW, and Fail2ban
Essential security measures to secure a clean Debian or Ubuntu server before hosting production workloads.
1. Eliminating Vulnerable Authentication Vectors
The moment a fresh Debian or Ubuntu VPS gets a public IP, it starts receiving SSH login attempts within minutes, not days. Automated botnets scan the entire IPv4 space continuously, and password authentication on port 22 is the single most common way production servers get compromised. The fix is not a clever password policy, it is removing passwords from the authentication path entirely. Generate an ed25519 key pair on your workstation, copy the public key to the server's authorized_keys, and only then disable password authentication in sshd_config.
Do this in the correct order or you will lock yourself out. First confirm you can log in with the key from a second terminal session while the original root/password session stays open. Only after that succeeds do you edit /etc/ssh/sshd_config to set PasswordAuthentication no and PermitRootLogin prohibit-password (or no, if you have a dedicated sudo user). Restart sshd with a reload, not a full stop, so an existing connection survives a misconfiguration.
A second, cheaper hardening step is moving SSH off port 22. This does not stop a targeted attacker, but it silences upwards of 95% of automated scanning noise, which keeps your auth logs readable and reduces Fail2ban's workload later. Combine this with disabling root login outright and creating a dedicated deploy user with sudo privileges, so every action is attributable to a named account rather than a shared root session.
Consider certificate-based SSH (an internal CA signing short-lived host and user certificates) once you manage more than a handful of servers, since rotating a leaked key across a dozen boxes is far more error-prone than revoking a certificate's trust anchor once. For a single-VPS setup this is overkill, but it is worth knowing the escalation path exists before you find yourself manually swapping authorized_keys entries across a growing fleet under time pressure.
2. Creating a Least-Privilege Deploy User
Running your application, your CI pipeline, and your interactive shell sessions as root is the fastest way to turn a single mistake, a leaked deploy key, or a dependency vulnerability into a full server compromise. Create a dedicated non-root user with sudo access scoped to what it actually needs, and use that account for every SSH session, GitHub Actions deployment, and Docker Compose invocation from this point forward.
Sudo access should not mean unrestricted root. For a deploy account whose job is to pull code, run docker compose, and restart services, grant NOPASSWD access to a narrow list of commands via a dedicated file in /etc/sudoers.d rather than adding the user to the sudo group wholesale. This keeps automated CI pipelines working without interactive password prompts while still limiting the blast radius if the deploy key is ever leaked.

