The average internet-facing Linux server receives hundreds of SSH brute-force attempts daily. Within minutes of provisioning a fresh DigitalOcean Droplet, bots are already scanning port 22. I learned this the hard way when an early VPS at Commsult Indonesia was compromised through a weak root password within 48 hours of being online. Since then, I run the same hardening checklist on every new server before deploying any application. This guide documents exactly what I do and why each step matters.
SSH is the front door of every Linux server and the highest-value target for attackers. The CIS Benchmark for Ubuntu recommends disabling root login, disabling password authentication, and using SSH key pairs as the minimum baseline. SSH keys are cryptographically immune to brute-force — a 2048-bit RSA key has more possible combinations than atoms in the observable universe. Password authentication against a determined attacker with a distributed botnet is a losing game.
Edit /etc/ssh/sshd_config with these critical settings: PermitRootLogin no, PasswordAuthentication no, MaxAuthTries 3, PubkeyAuthentication yes, X11Forwarding no, AllowTcpForwarding no (unless needed), and change the port from 22 to a non-standard port (e.g., 2222) to reduce automated scan noise. After making changes, always test the new config with sshd -t before reloading — a broken sshd config with no active session will lock you out of your own server.
UFW (Uncomplicated Firewall) is the right tool for straightforward VPS firewall management. Start with a default-deny-all-incoming policy, then whitelist only what you need: your SSH port, HTTP (80), and HTTPS (443). Never open database ports (5432, 3306, 6379) to the public internet — databases should only be reachable on the loopback interface or a private network. On DigitalOcean, combine UFW with the Cloud Firewall for defense in depth.
┌─────────────────────────────────────────────────────┐
│ LINUX SERVER HARDENING LAYERS │
├─────────────────────────────────────────────────────┤
│ [1] Cloud Firewall — Block before it hits VPS │
│ [2] UFW — Host-level packet filter │
│ [3] SSH Hardening — Key-only, non-standard port │
│ [4] Fail2ban — Auto-ban brute-forcers │
│ [5] Unattended-upgrades — Auto security patches │
│ [6] Kernel sysctl — SYN cookies, ASLR │
│ [7] Audit (Lynis) — Verify what you hardened │
└─────────────────────────────────────────────────────┘From my experience hardening servers in Jakarta with limited admin bandwidth, set up UFW before changing your SSH port. The sequence matters: add the new SSH port rule (ufw allow 2222/tcp), enable UFW, then change sshd_config port and reload SSH. Doing it out of order has locked me out twice — you end up opening a DigitalOcean console session to fix it, which is embarrassing and time-consuming.
Unpatched packages are responsible for a significant portion of server compromises. On Ubuntu/Debian, unattended-upgrades handles automatic security updates without requiring manual intervention. Install it, enable automatic reboots during a low-traffic window, and configure email notifications for reboot events. The Qualys Threat Research Unit found that 60% of breaches involved a vulnerability for which a patch was available but not applied.
Beyond SSH and firewall, apply kernel-level hardening via /etc/sysctl.conf: disable IP forwarding (unless this is a router), enable SYN cookies to prevent SYN flood attacks, set kernel.randomize_va_space=2 for ASLR, and restrict core dumps. Install and configure fail2ban for automated IP banning on repeated auth failures. Remove unused packages and disable unnecessary services with systemctl disable to reduce attack surface.
# SSH hardening — /etc/ssh/sshd_config
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
MaxAuthTries 3
X11Forwarding no
AllowTcpForwarding no
Port 2222
# UFW setup
ufw default deny incoming
ufw default allow outgoing
ufw allow 2222/tcp # SSH (new port)
ufw allow 80/tcp # HTTP
ufw allow 443/tcp # HTTPS
ufw enable
# Kernel hardening — /etc/sysctl.conf
net.ipv4.tcp_syncookies = 1
kernel.randomize_va_space = 2
net.ipv4.conf.all.rp_filter = 1
fs.suid_dumpable = 0Fail2ban is the minimum viable intrusion detection for a VPS. It monitors log files and bans IPs exceeding failure thresholds. Beyond fail2ban, consider installing OSSEC or Wazuh for file integrity monitoring — they alert you when critical system files change, which is a strong signal of compromise. For compliance-sensitive environments, auditd provides kernel-level audit logging of system calls, file access, and user actions.
I once applied CIS Level 2 hardening to a production NestJS server without testing it first — it disabled a kernel feature that the Node.js runtime relied on for performance, causing intermittent crashes under load. Always test hardening changes in a staging environment that mirrors production. Run CIS-CAT or Lynis after applying changes to audit what you actually hardened versus what you think you hardened. The difference can be significant.
Never run application workloads as root. Create a dedicated system user for each service (e.g., www-data for Nginx, nestjs for your Node.js app) with minimal permissions. Use sudo for privilege escalation and configure /etc/sudoers carefully — log all sudo commands with visudo's NOPASSWD disabled. Audit existing users regularly with getent passwd and remove any that should not exist. On fresh VPS instances, disable or delete the default ubuntu or debian user if you have set up your own admin user.
I maintain a bash script that runs all these steps idempotently — I can run it on a fresh server or re-run it on an existing one without breaking anything. It sets SSH config, configures UFW, installs and configures fail2ban, enables unattended-upgrades, applies sysctl hardening, and removes unnecessary packages. The script lives in our private GitLab repository and is the first thing that runs after spinning up any new Commsult Indonesia infrastructure. Automating the checklist eliminates human error and ensures consistency across every environment.