The Ultimate VPS Hardening Guide: Secure Your Server Like a Pro
A no-nonsense, step-by-step guide to securing your VPS from scratch. Learn SSH hardening, firewall setup, fail2ban, kernel tweaks, and more. Written for developers who want their servers bulletproof.
I've been there. You spin up a fresh VPS, deploy your app, and think you're done. Then you check your auth logs a week later and see thousands of failed SSH attempts from IPs you've never heard of. Bots are hammering your server 24/7, and you realize your "production" server is basically an open door.
This guide is everything I wish someone had told me when I started managing my own servers. No fluff, no theory—just practical steps that actually work.
Why Should You Care About Server Security?
Here's the reality: the moment your VPS gets a public IP, it's under attack. Automated bots scan the entire internet constantly, looking for:
- Default SSH ports with weak passwords
- Unpatched software with known vulnerabilities
- Misconfigured services exposing sensitive data
I've seen servers compromised within hours of deployment. Cryptominers, botnets, ransomware—the threats are real. But here's the good news: basic hardening stops 99% of automated attacks.
Let's get your server locked down.
Before We Start: The Golden Rules
Before touching anything on a production server, burn these rules into your brain:
- Always keep a backup SSH session open when modifying SSH or firewall configs
- Test changes before committing—one typo can lock you out
- Have your VPS provider's console access bookmarked—it's your emergency backdoor
- Document everything—future you will thank present you
This guide assumes you're running Ubuntu/Debian. Most commands work on other distros with minor tweaks.
Step 1: Update Everything Immediately
The first thing you do on any fresh VPS. No exceptions.
sudo apt update && sudo apt upgrade -y
This pulls the latest package lists and upgrades all installed software. Unpatched systems are the #1 cause of server compromises—known vulnerabilities get exploited within hours of disclosure.
Enable Automatic Security Updates
You won't always be around to manually patch your server. Let's automate it:
sudo apt install unattended-upgrades -y
sudo dpkg-reconfigure --priority=low unattended-upgrades
Select "Yes" when prompted. Your server will now automatically install security patches without you lifting a finger.
Want more control? Edit the config:
sudo nano /etc/apt/apt.conf.d/50unattended-upgrades
I recommend enabling these options:
Unattended-Upgrade::Remove-Unused-Dependencies "true";
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "03:00";
This removes orphaned packages and reboots at 3 AM if a kernel update requires it. Yes, automatic reboots sound scary, but an unpatched kernel is scarier.
Step 2: Create a Non-Root Admin User
Using root for everything is like driving without a seatbelt. One wrong command and you've nuked your system.
Create a dedicated admin user:
sudo adduser admin
sudo usermod -aG sudo admin
Replace admin with whatever username you prefer. You'll be prompted to set a password—make it strong (12+ characters, mix of everything).
Now test it. Open a new terminal (keep your current session open!) and try:
ssh admin@your-server-ip
Once logged in, verify sudo works:
sudo whoami
If it outputs root, you're good. If this fails, do not proceed. Fix it first, or you'll lock yourself out.
Step 3: Set Up SSH Key Authentication
Passwords are amateur hour. They can be brute-forced, phished, or guessed. SSH keys use cryptographic key pairs that are virtually impossible to crack.
Generate Your Key Pair (On Your Local Machine)
Run this on your laptop/desktop, not on the server:
ssh-keygen -t ed25519 -a 100 -f ~/.ssh/vps_key -C "your-email@example.com"
What this does:
-t ed25519— Uses the Ed25519 algorithm (more secure and faster than RSA)-a 100— 100 rounds of key derivation (makes brute-forcing harder)-f ~/.ssh/vps_key— Saves as "vps_key" in your .ssh folder-C "email"— Adds a comment to identify the key
When prompted for a passphrase, set one. Even if someone steals your key file, they can't use it without the passphrase.
Copy Your Public Key to the Server
ssh-copy-id -i ~/.ssh/vps_key.pub admin@your-server-ip
You'll enter your password one last time. After this, test key-based login:
ssh -i ~/.ssh/vps_key admin@your-server-ip
If you logged in without being asked for the user password (only the key passphrase if you set one), it's working.
Pro Tip: Simplify Your SSH Config
Add this to ~/.ssh/config on your local machine:
Host myvps
HostName your-server-ip
User admin
Port 22
IdentityFile ~/.ssh/vps_key
Now you can connect with just ssh myvps. Much cleaner.
Step 4: Harden the SSH Daemon
This is where we lock things down. SSH keys are working, so now we disable password authentication entirely.
Critical: Keep a backup session open! If you mess this up, you'll still have access to fix it.
Edit the SSH config:
sudo nano /etc/ssh/sshd_config
Find and modify these settings (uncomment by removing the # if needed):
# Disable root login completely
PermitRootLogin no
# Disable password authentication (keys only)
PasswordAuthentication no
# Enable public key authentication
PubkeyAuthentication yes
# Don't allow empty passwords
PermitEmptyPasswords no
# Use SSH Protocol 2 only
Protocol 2
# Limit authentication attempts
MaxAuthTries 3
# Time allowed to authenticate (seconds)
LoginGraceTime 20
# Only allow specific users
AllowUsers admin
# Disable unnecessary features
X11Forwarding no
AllowTcpForwarding no
AllowAgentForwarding no
What Each Setting Does
| Setting | Purpose |
|---|---|
PermitRootLogin no |
Attackers must compromise a regular user first |
PasswordAuthentication no |
Eliminates brute-force attacks entirely |
MaxAuthTries 3 |
Disconnects after 3 failed attempts |
LoginGraceTime 20 |
Closes connection if auth takes too long |
AllowUsers admin |
Only specified users can SSH in |
Optional: Change the SSH Port
This reduces log noise from automated scanners (they mostly target port 22):
Port 2222
Not a real security measure—security through obscurity—but it cuts down on noise significantly.
Validate and Apply
Before restarting SSH, check for syntax errors:
sudo sshd -t
No output means it's valid. Now restart:
sudo systemctl restart sshd
Test in a new terminal before closing your current session:
ssh -i ~/.ssh/vps_key admin@your-server-ip
# Or if you changed the port:
ssh -i ~/.ssh/vps_key -p 2222 admin@your-server-ip
Only close your backup session after confirming this works.
Step 5: Configure the Firewall (UFW)
A firewall controls what traffic can reach your server. The principle: deny everything by default, then explicitly allow only what you need.
Install and Configure UFW
sudo apt install ufw -y
# Block all incoming by default
sudo ufw default deny incoming
# Allow all outgoing (your server can reach the internet)
sudo ufw default allow outgoing
Allow SSH (Do This Before Enabling!)
# Default port:
sudo ufw allow 22/tcp
# Or if you changed to port 2222:
sudo ufw allow 2222/tcp
Allow Web Traffic (If Running a Web Server)
sudo ufw allow 80/tcp # HTTP
sudo ufw allow 443/tcp # HTTPS
Enable the Firewall
sudo ufw enable
You'll see a warning about disrupting SSH—type 'y' (you already allowed SSH above).
Verify Your Rules
sudo ufw status verbose
You should see something like:
Status: active
Default: deny (incoming), allow (outgoing)
To Action From
-- ------ ----
22/tcp ALLOW IN Anywhere
80/tcp ALLOW IN Anywhere
443/tcp ALLOW IN Anywhere
If a service isn't explicitly allowed, it's dead from outside. That's exactly what we want.
Step 6: Install Fail2ban
Fail2ban monitors your logs for suspicious activity (like repeated failed logins) and automatically bans offending IPs. It's your automated security guard.
sudo apt install fail2ban -y
Configure SSH Protection
Never edit the main config directly—create a local override:
sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
sudo nano /etc/fail2ban/jail.local
Find the [sshd] section and configure it:
[sshd]
enabled = true
port = 22
filter = sshd
logpath = /var/log/auth.log
maxretry = 3
findtime = 10m
bantime = 24h
ignoreip = 127.0.0.1/8 ::1
What this does:
- maxretry = 3 — Ban after 3 failed attempts
- findtime = 10m — Count failures within 10 minutes
- bantime = 24h — Ban for 24 hours
- ignoreip — Never ban localhost (add your home IP if it's static)
If you changed your SSH port, update port = 2222.
Start Fail2ban
sudo systemctl restart fail2ban
sudo systemctl enable fail2ban
Check Status
sudo fail2ban-client status sshd
You'll see currently banned IPs and statistics. Give it a day and you'll be amazed how many IPs get banned.
Step 7: Remove Unnecessary Services
Every running service is a potential attack vector. A minimal server has fewer vulnerabilities.
See What's Listening
sudo ss -tulpn
This shows all services listening for network connections. You might be surprised what's running.
Remove Common Junk
These packages are often pre-installed but rarely needed:
sudo apt purge telnet ftp xinetd rpcbind rsh-server -y
sudo apt autoremove -y
Be careful here. Don't remove packages you're not sure about. If you're running a web server, don't remove nginx or apache2.
Step 8: Enable AppArmor
AppArmor is Ubuntu's Mandatory Access Control system. It restricts what programs can do, even if they're compromised.
# Check if it's running
sudo aa-status
# Install additional profiles
sudo apt install apparmor-profiles apparmor-utils -y
# Enforce all profiles
sudo aa-enforce /etc/apparmor.d/*
If a confined program gets exploited, AppArmor limits the damage it can do.
Step 9: Kernel Hardening
The Linux kernel has tunable parameters that can prevent various network attacks. Create a security config:
sudo nano /etc/sysctl.d/99-security.conf
Add these settings:
# Disable IP forwarding (unless this is a router)
net.ipv4.ip_forward = 0
# Ignore ICMP redirects (prevent MITM attacks)
net.ipv4.conf.all.accept_redirects = 0
net.ipv6.conf.all.accept_redirects = 0
# Don't send ICMP redirects
net.ipv4.conf.all.send_redirects = 0
# Enable SYN flood protection
net.ipv4.tcp_syncookies = 1
# Log suspicious packets
net.ipv4.conf.all.log_martians = 1
# Ignore broadcast ICMP requests
net.ipv4.icmp_echo_ignore_broadcasts = 1
# Disable source routing
net.ipv4.conf.all.accept_source_route = 0
net.ipv6.conf.all.accept_source_route = 0
# Enable reverse path filtering (prevents IP spoofing)
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1
Apply the settings:
sudo sysctl -p /etc/sysctl.d/99-security.conf
Step 10: Secure Shared Memory
Shared memory can be exploited to run malicious code. Lock it down:
echo "tmpfs /run/shm tmpfs defaults,noexec,nosuid 0 0" | sudo tee -a /etc/fstab
sudo mount -o remount /run/shm
This prevents executable code in shared memory—a common exploit vector.
Step 11: Set Proper File Permissions
# Secure cron directories
sudo chmod 700 /etc/cron.d /etc/cron.daily /etc/cron.hourly /etc/cron.monthly /etc/cron.weekly
# Secure your SSH directory
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys
# Restrict sensitive system files
sudo chmod 644 /etc/passwd
sudo chmod 600 /etc/shadow
Step 12: Enable Persistent Logging
Logs are your forensic evidence. Without them, you won't know when or how an attack occurred.
# Create journal directory for persistent logs
sudo mkdir -p /var/log/journal
sudo systemd-tmpfiles --create --prefix /var/log/journal
sudo systemctl restart systemd-journald
Essential Log Commands
# View recent errors
sudo journalctl -p err -b
# View SSH logs
sudo journalctl -u ssh
# Watch auth log in real-time
sudo tail -f /var/log/auth.log
# View failed login attempts
sudo grep "Failed password" /var/log/auth.log
# View successful logins
sudo last
Step 13: Set Up Backups
A secure server without backups is still at risk. Ransomware, hardware failure, or accidental deletion can destroy everything.
sudo apt install rsync -y
# Create backup directory
sudo mkdir -p /backups
# Backup important directories
sudo rsync -avz --delete /etc /home /var/www /backups/server-$(date +%F)/
Sync to Remote Storage
Local backups aren't enough. Copy them off-server:
rsync -avz -e ssh /backups/ user@backup-server:~/vps-backups/
Critical rule: Backups stored only on the same server are NOT real backups. Always have an off-site copy.
Step 14: Run a Security Audit
Lynis scans your system and gives you a hardening score with specific recommendations:
sudo apt install lynis -y
sudo lynis audit system
The audit takes a few minutes. At the end, you'll see a hardening index (score out of 100) and actionable suggestions. Aim for 80+.
Run this monthly and after any major system changes.
Step 15: Verify Your Work
After all that hardening, let's verify it worked. From your local machine:
nmap -Pn your-server-ip
You should only see the ports you explicitly allowed:
PORT STATE SERVICE
22/tcp open ssh (or your custom port)
80/tcp open http (only if web server)
443/tcp open https (only if web server)
If you see unexpected open ports, investigate immediately. Use sudo ss -tulpn on the server to identify what's listening.
Bonus: Two-Factor Authentication for SSH
Want to go even further? Add 2FA so even if someone gets your private key, they still need your phone:
sudo apt install libpam-google-authenticator -y
google-authenticator
Follow the prompts and scan the QR code with your authenticator app.
Edit PAM config:
sudo nano /etc/pam.d/sshd
Add at the end:
auth required pam_google_authenticator.so
Update SSH config:
ChallengeResponseAuthentication yes
AuthenticationMethods publickey,keyboard-interactive
Restart SSH:
sudo systemctl restart sshd
Now you need both your SSH key AND a 2FA code to log in. That's serious security.
The Pre-Production Checklist
Before going live, verify everything:
- System fully updated with automatic security patches
- Non-root admin user created and tested
- SSH key authentication working
- Password authentication disabled
- Root login disabled
- Firewall enabled with only necessary ports
- Fail2ban protecting SSH
- Unnecessary services removed
- AppArmor profiles enforced
- Kernel hardening applied
- Persistent logging enabled
- Backup strategy implemented
- External port scan shows only expected services
- VPS provider console access bookmarked
Emergency Recovery
Locked yourself out? Don't panic.
Option 1: VPS Provider Console
- Log into your provider's dashboard (DigitalOcean, Linode, Vultr, etc.)
- Access the web console (VNC/KVM)—this bypasses SSH
- Log in and fix your config
Option 2: Recovery Mode
- Reboot into recovery/rescue mode via provider dashboard
- Mount your filesystem
- Fix
/etc/ssh/sshd_config - Reboot normally
Quick Fixes via Console
# Re-enable password auth temporarily
sudo sed -i 's/PasswordAuthentication no/PasswordAuthentication yes/' /etc/ssh/sshd_config
sudo systemctl restart sshd
# Disable firewall temporarily
sudo ufw disable
Final Thoughts
Server security isn't a one-time thing—it's an ongoing process. But these steps will stop the vast majority of automated attacks and make your server significantly harder to compromise.
The key takeaways:
- Update religiously — Unpatched software is the #1 attack vector
- SSH keys only — Passwords are a liability
- Firewall everything — If it's not explicitly allowed, it's blocked
- Monitor your logs — You can't defend against what you can't see
- Backup off-site — Because disasters happen
Your VPS is now hardened. Go build something awesome on it.
Have questions or found something I missed? Reach out—I'm always looking to improve this guide.