VPS Hardening Reduces Attack Surface
A fresh VPS arrives with default configurations that assume nothing about your threat model. Best VPS hardening practices treat the server as hostile territory: disable what you don't need, authenticate everything, log everything, and assume breach.
This isn't paranoia—it's the mental model that separates a compromised box from one that stays yours. The goal is to make exploitation expensive enough that attackers move to easier targets.
The Hardening Model
Think of VPS hardening in three layers:
- Network perimeter: Control who can reach your server and on which ports.
- System surface: Remove or disable services, libraries, and users that add no value.
- Runtime behavior: Monitor and log what actually happens; detect deviations early.
Each layer compounds. A misconfigured firewall is a problem; a misconfigured firewall plus unnecessary SSH versions running is a bigger problem.
Layer 1: Network Perimeter
Firewall rules come first. Most VPS providers offer a software or cloud firewall; use it. Allow only the ports your application needs—typically 22 (SSH), 80 (HTTP), 443 (HTTPS)—and nothing else.
Gotcha: Don't forget to allow outbound traffic if your app makes external API calls. Deny by default, allow by exception.
SSH access is your lifeline and your vulnerability. Change the default port 22 to something else (e.g., 2222). This stops automated scanners and credential-stuffing bots from hitting your SSH daemon. Use Port 2222 in /etc/ssh/sshd_config, then reload: systemctl restart sshd. Test the new port in a separate terminal before closing your current session.
Gotcha: Firewall rules must also allow your new SSH port, or you'll lock yourself out.
Disable password authentication entirely. SSH keys are cryptographically stronger and immune to brute force. Generate a key pair on your local machine:
ssh-keygen -t ed25519 -C "your-email@example.com"
Copy the public key to ~/.ssh/authorized_keys on the VPS (typically done during provisioning). Then set PasswordAuthentication no in /etc/ssh/sshd_config and reload. Verify you can still log in with your key before restarting SSH.
Disable root login over SSH. Set PermitRootLogin no in /etc/ssh/sshd_config. Create a non-root user with sudo privileges instead:
useradd -m -s /bin/bash deploy
usermod -aG sudo deploy
Gotcha: Always test sudo access before disabling root login, or you'll need console access to recover.
Layer 2: System Surface
Remove unnecessary packages. Start with a minimal base image (Ubuntu Server, Debian, CentOS Minimal). Don't install X11, development tools, or language runtimes you won't use. Each package is a potential attack vector.
Gotcha: Some packages have dependencies you might not expect. Use apt-cache depends <package> to see the tree before uninstalling.
Disable unused services. Check what's running: systemctl list-units --type=service --state=running. Services like Avahi (mDNS), cups (printing), or bluetooth have no place on a headless server. Disable them:
systemctl disable avahi-daemon
systemctl stop avahi-daemon
Set restrictive file permissions. SSH keys should be 600 (read/write owner only); SSH config directory should be 700. Home directories should not be world-readable:
chmod 700 /home/deploy
chmod 600 /home/deploy/.ssh/authorized_keys
Use a host firewall too. The cloud firewall is a perimeter; ufw (or firewalld) is a local policy engine. Enable it:
ufw default deny incoming
ufw default allow outgoing
ufw allow 2222/tcp
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable
Gotcha: Enable ufw before setting rules, or you'll block yourself if something goes wrong.
Layer 3: Runtime Behavior
Enable system logging. Rsyslog is usually installed; check /var/log/auth.log (SSH attempts) and /var/log/syslog (system events). Ensure logs are preserved:
ls -la /var/log/auth.log
If log files are missing or truncated, an attacker may have covered their tracks.
Monitor failed login attempts. Failed SSH attempts should be rare if you're using keys. A spike suggests reconnaissance:
grep "Failed password" /var/log/auth.log | wc -l
If this number is large, someone is trying to brute force you. Confirm that password auth is disabled and consider rate-limiting at the firewall level. The same discipline applies when you're on the move—staying cautious on public wifi is just as important as locking down the server itself.
Set up log rotation. Logs consume disk space over time. Configure logrotate to compress and archive old logs:
ls /etc/logrotate.d/
Most distributions ship with sensible defaults, but verify /etc/logrotate.d/rsyslog exists and includes your auth logs.
Use fail2ban for repeated offenses. Fail2ban reads logs and temporarily blocks IPs that exceed a threshold of failed attempts:
apt install fail2ban
systemctl enable fail2ban
Create /etc/fail2ban/jail.local with:
[sshd]
enabled = true
port = 2222
maxretry = 5
findtime = 600
bantime = 3600
This blocks any IP with 5 failed login attempts in 10 minutes for 1 hour.
Enable automatic security updates. Patches close known vulnerabilities. On Ubuntu/Debian:
apt install unattended-upgrades
dpkg-reconfigure -plow unattended-upgrades
Gotcha: Automatic updates can restart services or the kernel without warning. Schedule them during maintenance windows if your app is production-critical.
When This Breaks
Locked out of SSH: You changed the port or disabled password auth but can't log in with your key. Use your VPS provider's console/recovery mode to fix /etc/ssh/sshd_config or add your key to authorized_keys.
Firewall blocks legitimate traffic: You allowed port 443 but your app also needs outbound DNS (port 53). Add the missing rule and test.
Logs fill the disk: If log rotation isn't working, old logs accumulate and /var runs out of space. Clear archived logs manually or increase rotation frequency.
fail2ban blocks you: If you're working from a dynamic IP and fail a login multiple times, fail2ban will block you. Whitelist your IP in jail.local or use the console to unban yourself: fail2ban-client set sshd unbanip <your-ip>.
Trade-Offs
Hardening costs convenience. SSH keys are stronger than passwords but require backup and management. Disabling services reduces attack surface but means you can't quickly enable them later without re-provisioning. Fail2ban can block legitimate traffic if misconfigured. Automatic updates improve security but can introduce regressions.
The trade-off is worth it: a compromised VPS costs far more in recovery time and reputation damage than the effort to harden it upfront. If you're also managing build automation, Taskfile is worth a look as a modern alternative for scripting your hardening steps consistently across environments.
One-Line Takeaway
Harden your VPS by controlling network access (firewall + SSH hardening), minimizing system surface (remove unnecessary packages and services), and detecting anomalies (logging + fail2ban).