How to Secure & Harden Your VPS: Complete Guide for 2026

One-sentence verdict: An unhardened VPS gets scanned within minutes of going live — following this checklist takes about 30 minutes and blocks 99% of automated attacks before they start.

Who This Guide Is For

If you’re running a large enterprise infrastructure with compliance requirements (PCI-DSS, SOC 2), you’ll need additional measures beyond this guide — but this remains a solid starting point.

Why VPS Hardening Matters

A freshly provisioned VPS with a public IP starts receiving automated attacks within 2–15 minutes. Here’s what happens if you do nothing:

ThreatTimelineImpact
SSH brute-force botsWithin 5 minutesRoot access compromise
Port scannersWithin 15 minutesService enumeration
Vulnerability scannersWithin 1 hourExploit attempts on known CVEs
Cryptomining malwareWithin 24 hoursCPU hijacked, bandwidth drained
Botnet recruitmentWithin 48 hoursYour IP blacklisted, used for DDoS

These are not hypothetical — honeypot research consistently shows thousands of login attempts per day on exposed SSH ports.

Security Hardening Checklist (Quick Reference)

StepTimeImpactDifficulty
Create non-root user2 minHighEasy
Disable root SSH login1 minHighEasy
SSH key authentication only5 minCriticalEasy
Change SSH port1 minMediumEasy
Configure UFW firewall5 minCriticalEasy
Install Fail2Ban5 minHighEasy
Enable automatic updates3 minHighEasy
Set up log monitoring5 minMediumModerate
Configure 2FA for SSH5 minHighModerate
Install rootkit scanner3 minMediumEasy

Total time: ~35 minutes for a fully hardened server.

Step 1: Create a Non-Root User with Sudo Access

Never run services as root. Create a dedicated user immediately after first login:

# Log in as root (first time only)
ssh root@your-server-ip

# Create a new user
adduser deploy

# Add to sudo group
usermod -aG sudo deploy

# Verify sudo works
su - deploy
sudo whoami  # Should output: root

Why this matters: If any service running as root gets compromised, the attacker owns everything. A non-root user with sudo limits the blast radius.

Step 2: Set Up SSH Key Authentication

Passwords are brute-forceable. SSH keys are not (with proper key length).

On your local machine:

# Generate an Ed25519 key (most secure, recommended)
ssh-keygen -t ed25519 -C "your-email@example.com"

# Or RSA 4096 if Ed25519 isn't supported
ssh-keygen -t rsa -b 4096 -C "your-email@example.com"

# Copy the public key to your server
ssh-copy-id deploy@your-server-ip

Verify you can log in with the key before proceeding:

ssh deploy@your-server-ip
# Should log in without asking for a password

Step 3: Lock Down SSH Configuration

Edit the SSH daemon config:

sudo nano /etc/ssh/sshd_config

Apply these settings:

# Disable root login entirely
PermitRootLogin no

# Disable password authentication (key only)
PasswordAuthentication no

# Disable empty passwords
PermitEmptyPasswords no

# Change default port (pick something between 10000-65535)
Port 2222

# Limit login attempts
MaxAuthTries 3

# Disconnect idle sessions after 5 minutes
ClientAliveInterval 300
ClientAliveCountMax 2

# Only allow your user
AllowUsers deploy

# Disable X11 forwarding (unless you need it)
X11Forwarding no

# Use only SSH protocol 2
Protocol 2

Restart SSH:

sudo systemctl restart sshd

⚠️ Critical: Keep your current SSH session open and test the new config in a separate terminal before closing it. If something is wrong, you can still fix it from the open session.

# Test from a new terminal
ssh -p 2222 deploy@your-server-ip

Step 4: Configure UFW Firewall

UFW (Uncomplicated Firewall) provides a simple interface to iptables:

# Install UFW (usually pre-installed on Ubuntu)
sudo apt install ufw -y

# Default policy: deny all incoming, allow all outgoing
sudo ufw default deny incoming
sudo ufw default allow outgoing

# Allow your custom SSH port
sudo ufw allow 2222/tcp comment 'SSH'

# Allow web traffic (if running a web server)
sudo ufw allow 80/tcp comment 'HTTP'
sudo ufw allow 443/tcp comment 'HTTPS'

# Enable the firewall
sudo ufw enable

# Verify rules
sudo ufw status verbose

Important: Always allow your SSH port before enabling UFW, or you’ll lock yourself out.

Firewall Rules for Common Self-Hosted Services

ServicePortUFW Command
SSH (custom)2222sudo ufw allow 2222/tcp
HTTP80sudo ufw allow 80/tcp
HTTPS443sudo ufw allow 443/tcp
WireGuard51820sudo ufw allow 51820/udp
PostgreSQL (local only)5432Don’t expose — use SSH tunnel
Docker API2376Don’t expose — bind to localhost

Step 5: Install and Configure Fail2Ban

Fail2Ban monitors log files and bans IPs that show malicious behavior:

# Install
sudo apt install fail2ban -y

# Create local config (never edit jail.conf directly)
sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
sudo nano /etc/fail2ban/jail.local

Add or modify these settings:

[DEFAULT]
bantime = 3600        # Ban for 1 hour
findtime = 600        # Within a 10-minute window
maxretry = 3          # After 3 failed attempts
banaction = ufw       # Use UFW for banning

[sshd]
enabled = true
port = 2222           # Match your SSH port
filter = sshd
logpath = /var/log/auth.log
maxretry = 3
bantime = 86400       # Ban SSH attackers for 24 hours

Start Fail2Ban:

sudo systemctl enable fail2ban
sudo systemctl start fail2ban

# Check status
sudo fail2ban-client status sshd

Step 6: Enable Automatic Security Updates

Don’t let known vulnerabilities sit unpatched:

# Install unattended-upgrades
sudo apt install unattended-upgrades apt-listchanges -y

# Enable automatic security updates
sudo dpkg-reconfigure -plow unattended-upgrades

Edit /etc/apt/apt.conf.d/50unattended-upgrades to configure what gets updated:

Unattended-Upgrade::Allowed-Origins {
    "${distro_id}:${distro_codename}-security";
    "${distro_id}ESMApps:${distro_codename}-apps-security";
};

// Auto-reboot if needed (at 4 AM)
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "04:00";

// Email notifications (optional)
Unattended-Upgrade::Mail "your-email@example.com";

Step 7: Set Up Basic Intrusion Detection

Install rkhunter (Rootkit Hunter)

sudo apt install rkhunter -y

# Update the database
sudo rkhunter --update

# Run a scan
sudo rkhunter --check --skip-keypress

# Set up daily scans via cron
echo '0 3 * * * root /usr/bin/rkhunter --check --skip-keypress --report-warnings-only' | sudo tee /etc/cron.d/rkhunter

Monitor File Changes with AIDE

sudo apt install aide -y

# Initialize the database
sudo aideinit

# Copy the new database
sudo cp /var/lib/aide/aide.db.new /var/lib/aide/aide.db

# Run a check
sudo aide --check

Step 8: Secure Shared Memory and Kernel Parameters

Edit /etc/fstab to secure shared memory:

echo 'tmpfs /run/shm tmpfs defaults,noexec,nosuid 0 0' | sudo tee -a /etc/fstab

Harden kernel parameters in /etc/sysctl.conf:

sudo tee -a /etc/sysctl.conf << 'EOF'
# Prevent IP spoofing
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1

# Disable source routing
net.ipv4.conf.all.accept_source_route = 0
net.ipv6.conf.all.accept_source_route = 0

# Ignore ICMP broadcast requests
net.ipv4.icmp_echo_ignore_broadcasts = 1

# Disable ICMP redirects
net.ipv4.conf.all.accept_redirects = 0
net.ipv6.conf.all.accept_redirects = 0
net.ipv4.conf.all.send_redirects = 0

# Log suspicious packets
net.ipv4.conf.all.log_martians = 1
net.ipv4.conf.default.log_martians = 1

# Disable IPv6 if not needed
net.ipv6.conf.all.disable_ipv6 = 1
net.ipv6.conf.default.disable_ipv6 = 1
EOF

# Apply changes
sudo sysctl -p

Install Logwatch for daily security summaries:

sudo apt install logwatch -y

# Run a test report
sudo logwatch --detail Med --range today --output stdout

# Configure daily email reports
sudo tee /etc/cron.daily/00logwatch << 'EOF'
#!/bin/bash
/usr/sbin/logwatch --output mail --mailto your-email@example.com --detail high
EOF
sudo chmod +x /etc/cron.daily/00logwatch

Step 10: Docker-Specific Security (If Using Containers)

Docker bypasses UFW by default — this catches many people off guard:

# Prevent Docker from manipulating iptables directly
sudo tee /etc/docker/daemon.json << 'EOF'
{
  "iptables": false,
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  }
}
EOF

sudo systemctl restart docker

Best practices for Docker security:

VPS Provider Security Features Comparison

ProviderFree FirewallDDoS ProtectionBackups2FA DashboardPrivate Networking
Hetzner✅ Cloud FirewallBasic€2.88/mo (20%)
DigitalOcean✅ Cloud FirewallFree basic$2.40/mo (20%)✅ VPC
Vultr✅ Firewall GroupsFree basic$1/mo per VPS✅ VPC 2.0
Linode (Akamai)✅ Cloud FirewallFree basic$2.50/mo (20%)✅ VLAN
Contabo❌ (OS-level only)BasicSnapshots onlyLimited
RackNerd❌ (OS-level only)Free SolusVMManual only

Recommendation: Use provider-level firewalls as an additional layer on top of UFW — they filter traffic before it reaches your server, reducing load.

Use CaseMin RAMKey Security MeasuresMonthly Cost
Personal blog/portfolio1 GBSSH keys + UFW + Fail2Ban + auto-updates$4–$6
SaaS app with user data2 GBAll above + AIDE + encrypted backups + WAF$8–$12
Self-hosted AI tools4 GB+All above + Docker hardening + resource limits$12–$24
E-commerce store4 GB+All above + PCI compliance measures + CDN$20–$40

Common Mistakes to Avoid

MistakeRiskFix
Running everything as rootTotal compromise if any service is exploitedUse non-root user + sudo
Leaving SSH on port 22 with password authThousands of brute-force attempts dailyKey-only auth + custom port
Not enabling a firewallAll ports exposed to the internetUFW deny-all + explicit allows
Exposing database portsDirect database access from anywhereBind to localhost, use SSH tunnels
Docker binding to 0.0.0.0Bypasses UFW, exposes services directlyBind to 127.0.0.1 + reverse proxy
No backupsRansomware or disk failure = total lossAutomated off-server backups
Ignoring updatesKnown CVEs exploited within hoursUnattended-upgrades for security patches

Risk Warnings

⚠️ Locking yourself out: Always test SSH changes in a new terminal while keeping the old session open. If you lose access, you’ll need provider console/VNC access to fix it.

⚠️ Breaking services: Overly restrictive firewall rules can break applications. Test after each change.

⚠️ False security: Hardening reduces attack surface but doesn’t make you invulnerable. Keep backups, monitor logs, and stay updated on CVEs affecting your stack.

⚠️ Auto-updates rebooting production: If uptime is critical, disable automatic reboots and schedule maintenance windows manually.

What to Do Next

  1. Set up automated backups — Use provider snapshots + off-site backups (BorgBackup to a second VPS or Backblaze B2).
  2. Install a reverse proxy — Caddy or Nginx Proxy Manager for automatic HTTPS on all your services.
  3. Add uptime monitoring — Use UptimeRobot (free) or self-hosted Uptime Kuma to catch outages.
  4. Review logs weekly — Check Fail2Ban bans, auth.log, and any service-specific logs.
  5. Harden individual applications — Each service (WordPress, Nextcloud, databases) has its own security best practices.

Conclusion

Securing a VPS isn’t optional — it’s the first thing you should do after provisioning. The 30-minute investment in this guide protects against the vast majority of automated attacks. The steps are cumulative: each layer makes exploitation significantly harder.

Start with the basics (non-root user, SSH keys, firewall, Fail2Ban), then add monitoring and intrusion detection as your setup grows. The cheapest VPS at $4/month becomes a liability if it’s compromised and used to attack others — hardening is cheaper than incident response.