Complete Technical Walkthrough
How to Secure a VPS Server: A Step-by-Step Hardening Guide
Every critical configuration you need to lock down a fresh Linux VPS — from first login to firewall
📋 What’s in this guide
- Why VPS Security Is Your Responsibility
- First Login: The Critical First 10 Minutes
- Create a Non-Root User with Sudo
- Harden SSH: Keys, Ports & Restrictions
- Configure the Firewall (UFW)
- Automatic Security Updates
- Fail2Ban: Block Brute-Force Attacks
- Secure Shared Memory & Parameters
- Rootkit & Intrusion Detection
- Hardening Nginx & Apache
- Database Security Basics
- Ongoing Maintenance & Auditing
- Complete VPS Hardening Checklist
A freshly provisioned VPS is a wide-open door. The moment it goes online with a public IP address, automated bots begin probing it — scanning for open ports, trying default credentials on SSH, testing for known software vulnerabilities. This isn’t paranoia; it’s routine background noise on the internet. Within hours of first boot, your server’s auth logs will show thousands of failed login attempts from IP addresses around the world.
Unlike shared hosting, where your provider handles OS-level security, a VPS is your responsibility from the ground up. The host secures the physical hardware and the hypervisor. Everything inside your virtual machine — the operating system, the services, the firewall, the user accounts — is yours to configure and maintain. Most VPS providers hand you root access and a blank Linux install. What you do with it determines whether your server stays secure.
This guide walks through every meaningful hardening step for a fresh Ubuntu or Debian VPS, in the order you should apply them. The commands are tested and production-ready. By the end, your server will be significantly more resistant to the attacks that compromise the majority of poorly-configured VPS instances.
All commands in this guide are written for Ubuntu 22.04 LTS and Ubuntu 24.04 LTS, and are compatible with Debian 11/12. Commands for CentOS/AlmaLinux/RHEL differ primarily in the package manager (dnf instead of apt) and firewall tooling (firewalld instead of UFW) — the concepts are identical, but the syntax varies. Notes are included where key differences apply.
1. Why VPS Security Is Your Responsibility
The shared responsibility model of VPS hosting means your provider secures the infrastructure, but you own everything inside your VM. Understanding what you’re actually defending against is the first step to hardening effectively.
The Threat Landscape for a New VPS
| Attack Type | Severity | How Common | Primary Defense |
|---|---|---|---|
| SSH brute-force | Critical | Extremely common — starts within hours of provisioning | SSH key auth + Fail2Ban |
| Outdated package exploits | Critical | Very common — unpatched servers are low-hanging fruit | Automatic security updates |
| Root login compromise | Critical | Common — default root SSH is targeted heavily | Disable root SSH login |
| Open port exploitation | High | Common — scanners find unnecessary open services | Firewall (UFW) + port audit |
| Web app vulnerabilities | High | Very common for WordPress/PHP sites | Web server hardening + WAF |
| Cryptomining malware | High | Increasingly common after any successful breach | Rootkit scanning + monitoring |
| Privilege escalation | Medium | Targeted — follows initial low-privilege compromise | Minimal sudo access + kernel updates |
The good news: the vast majority of successful VPS compromises exploit entirely preventable misconfigurations. Servers with password-based SSH, no firewall, and unpatched packages are breached constantly — not because of sophisticated attacks, but because they present no meaningful resistance to automated scanners. Every step in this guide removes a layer of that easy access.
Security hardening is dramatically easier to apply to a fresh server than to one already running production workloads. Apply these steps immediately after provisioning, before installing your web server, database, or application. Retrofitting security onto a running server risks breaking things and often gets skipped or done incompletely. Do it first.
2. First Login: The Critical First 10 Minutes
When you first receive your VPS credentials, the server is running as root with a password that was emailed to you — often a weak auto-generated string. Your first session has one priority: get the system updated and set the stage for everything that follows.
Step 1: Connect and Update Everything
Log in via SSH as root using the credentials from your provider:
$ ssh root@your_server_ip
The very first thing you do on any new server is update all installed packages. Freshly provisioned VPS images are often weeks or months old and ship with known vulnerabilities already patched upstream:
# Update the package list and upgrade all installed packages $ apt update && apt upgrade -y # Remove packages no longer needed $ apt autoremove -y # Reboot if a kernel update was installed $ reboot
Step 2: Set the Correct Timezone
Accurate timestamps are essential for reading logs and correlating security events. Set your timezone before anything else generates log entries:
$ timedatectl set-timezone America/New_York # Replace with your timezone — list options with: timedatectl list-timezones $ timedatectl status
Step 3: Set a Strong Root Password
Even though we’ll disable root SSH login shortly, set a strong root password now as a local access fallback (used via your host’s console/VNC if SSH becomes inaccessible):
$ passwd root # Enter a long, random password and store it in your password manager
3. Create a Non-Root User with Sudo Access
Running everything as root is one of the most dangerous habits in server administration. If any process running as root is compromised, the attacker has complete, unrestricted control of your server. The solution is a separate user account that can request elevated privileges via sudo only when needed — and has that privilege logged.
Create the User and Add to the sudo Group
# Create a new user — replace "deploy" with your preferred username $ adduser deploy # You'll be prompted to set a password and fill in optional user details # Add the new user to the sudo group $ usermod -aG sudo deploy # Verify the user has sudo access $ su - deploy deploy@server:~$ sudo whoami # Should output: root
Copy SSH Keys to the New User
If you connected as root using an SSH key, copy that key to your new user so you can log in as them without a password. Do this before locking down SSH:
# Copy root's authorized_keys to the new user $ rsync --archive --chown=deploy:deploy ~/.ssh /home/deploy
Now open a new terminal window and test that you can log in as your new user before proceeding. Never close your existing root session until you’ve confirmed the new login works — otherwise you risk locking yourself out.
$ ssh deploy@your_server_ip # If this works, your new user is configured correctly
When making SSH configuration changes, always verify the new setup works in a separate terminal before closing your current session. A single misconfiguration in sshd_config — a typo, a missing authorized_keys file, wrong permissions — can lock you out of your server entirely. Your only recovery option would be your host’s emergency console or a server rebuild.
4. Harden SSH: Keys, Ports & Restrictions
SSH is the primary entry point to your server and therefore the primary target of automated attacks. Hardening your SSH configuration is the single highest-impact security step you can take. We’re making four key changes: disabling root login, disabling password authentication, optionally changing the default port, and restricting which users can connect.
Generate an SSH Key Pair (If You Haven’t Already)
If you’re currently logging in with a password, generate a key pair on your local machine first:
# Generate a modern Ed25519 key pair (preferred over RSA) $ ssh-keygen -t ed25519 -C "[email protected]" # Copy your public key to the server $ ssh-copy-id deploy@your_server_ip # Test key-based login before changing SSH config $ ssh deploy@your_server_ip
Edit the SSH Daemon Configuration
$ sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak $ sudo nano /etc/ssh/sshd_config
Find and set the following directives. Add them if they don’t exist; update them if they do:
# Disable root login entirely PermitRootLogin no # Disable password authentication — keys only PasswordAuthentication no ChallengeResponseAuthentication no UsePAM no # Restrict SSH to your non-root user only AllowUsers deploy # Optional but recommended: change default port (reduces log noise) # Pick any unused port between 1024–65535 Port 2222 # Reduce login grace time and limit auth attempts LoginGraceTime 30 MaxAuthTries 3 MaxSessions 5 # Disable unused authentication methods X11Forwarding no PermitEmptyPasswords no IgnoreRhosts yes
Save the file, then test the configuration for syntax errors before restarting:
# Validate config — must show no errors before restarting $ sudo sshd -t # Restart SSH service $ sudo systemctl restart sshd # Test from a NEW terminal before closing this session $ ssh -p 2222 deploy@your_server_ip
Changing SSH from port 22 to a custom port won’t stop a determined attacker — full port scans find it quickly. But it eliminates the vast majority of automated brute-force bots that only target port 22, and dramatically reduces auth log noise, making real intrusion attempts much easier to spot. It’s a low-effort measure worth doing, but it’s not a substitute for key-based authentication.
5. Configure the Firewall (UFW)
A firewall controls which network traffic reaches your server. The principle is simple: deny everything by default, then explicitly allow only the ports and protocols your server actually needs. UFW (Uncomplicated Firewall) is the standard tool for this on Ubuntu and Debian — it’s a user-friendly interface to iptables.
Basic UFW Setup
# Install UFW if not already present $ sudo apt install ufw -y # Set default policies: deny all incoming, allow all outgoing $ sudo ufw default deny incoming $ sudo ufw default allow outgoing # Allow your SSH port — critical: do this before enabling UFW # If you changed SSH to a custom port, use that port number here $ sudo ufw allow 2222/tcp # Allow HTTP and HTTPS if running a web server $ sudo ufw allow 80/tcp $ sudo ufw allow 443/tcp # Enable UFW — confirm with 'y' when prompted $ sudo ufw enable # Verify the rules are active $ sudo ufw status verbose
Common UFW Rules Reference
| Service | UFW Command | Notes |
|---|---|---|
| SSH (default port) | sudo ufw allow 22/tcp | Only if you kept port 22 |
| SSH (custom port) | sudo ufw allow 2222/tcp | Match your sshd_config Port setting |
| HTTP | sudo ufw allow 80/tcp | Required for web server + Let’s Encrypt |
| HTTPS | sudo ufw allow 443/tcp | Required for SSL traffic |
| MySQL (local only) | sudo ufw deny 3306 | Never expose MySQL to the internet |
| PostgreSQL (local only) | sudo ufw deny 5432 | Never expose PostgreSQL to the internet |
| Allow specific IP | sudo ufw allow from 203.0.113.5 | Allow all traffic from a trusted IP |
| Rate-limit SSH | sudo ufw limit 2222/tcp | Blocks IPs with >6 connections in 30s |
MySQL (3306), PostgreSQL (5432), Redis (6379), and MongoDB (27017) should never be accessible from the public internet. Always keep these ports blocked by default. If your application and database are on the same server, they communicate over localhost and never need to be publicly accessible. If they’re on separate servers, use a private network or an SSH tunnel — not a public firewall rule.
UFW Rate Limiting for SSH
UFW has a built-in rate-limiting feature that automatically blocks IPs making more than 6 connection attempts within 30 seconds. Enable it on your SSH port as an additional layer against brute force:
# Enable rate limiting on your SSH port $ sudo ufw limit 2222/tcp comment 'Rate limit SSH'
6. Automatic Security Updates
Software vulnerabilities are discovered constantly. The difference between a patched and unpatched server can be a matter of hours when a critical CVE is published and exploit code becomes public. Unattended Upgrades automatically installs security updates on Ubuntu and Debian without requiring manual intervention.
# Install unattended-upgrades $ sudo apt install unattended-upgrades apt-listchanges -y # Configure automatic updates $ sudo dpkg-reconfigure --priority=low unattended-upgrades # Select "Yes" to enable automatic security updates
For more granular control, edit the configuration directly:
$ sudo nano /etc/apt/apt.conf.d/50unattended-upgradesThe key settings to verify or add:
// Automatically remove unused kernel packages Unattended-Upgrade::Remove-Unused-Kernel-Packages "true"; // Remove unused dependencies automatically Unattended-Upgrade::Remove-Unused-Dependencies "true"; // Automatically reboot if required (e.g. after kernel update) // Set to "false" if you prefer to control reboots manually Unattended-Upgrade::Automatic-Reboot "true"; Unattended-Upgrade::Automatic-Reboot-Time "03:00"; // Email notifications on upgrade failures Unattended-Upgrade::Mail "[email protected]"; Unattended-Upgrade::MailReport "on-change";
Setting Automatic-Reboot to “true” means your server will restart automatically after kernel updates — typically at the time you specify. For most solo VPS owners running websites, this is the right call: staying on an unpatched kernel is worse than a scheduled 2am reboot. If you’re running a service with strict uptime requirements, set it to “false” and handle kernel reboots manually during a maintenance window.
7. Fail2Ban: Block Brute-Force Attacks
Even with key-based SSH auth, automated bots will hammer your SSH port relentlessly. Fail2Ban monitors log files for patterns of failed authentication attempts and automatically bans offending IP addresses using firewall rules — typically after a configurable number of failures within a defined time window.
Install and Configure Fail2Ban
$ sudo apt install fail2ban -y # Create a local config file — never edit jail.conf directly # (it gets overwritten on package updates) $ sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local $ sudo nano /etc/fail2ban/jail.local
Find the [DEFAULT] section and set these values:
# Ban IPs for 1 hour after triggering the threshold bantime = 3600 # Time window in which failures are counted (10 minutes) findtime = 600 # Number of failures before a ban is issued maxretry = 5 # Use UFW as the ban action banaction = ufw
Then configure the SSH jail — scroll down to find the [sshd] section:
[sshd] enabled = true port = 2222 # Match your actual SSH port filter = sshd logpath = /var/log/auth.log maxretry = 3 # Stricter than default for SSH
$ sudo systemctl enable fail2ban $ sudo systemctl restart fail2ban # Check status of all active jails $ sudo fail2ban-client status # Check specifically the SSH jail $ sudo fail2ban-client status sshd # Unban an IP if you accidentally lock yourself out $ sudo fail2ban-client set sshd unbanip YOUR_IP
Fail2Ban ships with pre-built jails for Nginx, Apache, Postfix, and many other services. If you’re running a web server, enable the nginx-http-auth and nginx-botsearch jails to automatically ban IPs that probe for vulnerable scripts, attempt HTTP auth brute-force, or trigger repeated 404 errors from scanning tools. The same concept — detect, count, ban — applies across every service that writes to a log file.
8. Secure Shared Memory & Kernel Parameters
Two additional system-level hardening steps that are quick to apply and meaningfully reduce your attack surface: securing the shared memory filesystem and tuning kernel network parameters via sysctl.
Secure /run/shm (Shared Memory)
The shared memory filesystem can be exploited to run malicious code. Mounting it with restrictive options prevents execution of programs stored there:
$ sudo nano /etc/fstabAdd this line at the end of the file:
tmpfs /run/shm tmpfs defaults,noexec,nosuid,nodev 0 0Harden Kernel Network Parameters (sysctl)
The Linux kernel exposes many security-relevant settings through the sysctl interface. The following settings harden network behavior against common attack techniques including IP spoofing, ICMP redirects, and SYN flood attacks:
$ sudo nano /etc/sysctl.d/99-hardening.conf# Disable IP source routing (prevents IP spoofing) net.ipv4.conf.all.accept_source_route = 0 net.ipv6.conf.all.accept_source_route = 0 # Disable ICMP redirects (prevents routing table manipulation) net.ipv4.conf.all.accept_redirects = 0 net.ipv6.conf.all.accept_redirects = 0 net.ipv4.conf.all.send_redirects = 0 # Enable SYN flood protection (SYN cookies) net.ipv4.tcp_syncookies = 1 # Enable reverse path filtering (drops spoofed packets) net.ipv4.conf.all.rp_filter = 1 # Ignore ICMP broadcast requests (smurf attack protection) net.ipv4.icmp_echo_ignore_broadcasts = 1 # Log martian packets (packets with impossible source addresses) net.ipv4.conf.all.log_martians = 1 # Disable IPv6 if you don't use it net.ipv6.conf.all.disable_ipv6 = 1 net.ipv6.conf.default.disable_ipv6 = 1
$ sudo sysctl -p /etc/sysctl.d/99-hardening.conf9. Rootkit Detection & Intrusion Monitoring
Once your preventive hardening is in place, you need tools that detect if something has gotten through despite those protections. Rootkit scanners check for signs of compromise — hidden processes, modified system binaries, suspicious kernel modules. Audit daemons log privileged actions for forensic review.
rkhunter — Rootkit Hunter
$ sudo apt install rkhunter -y # Update the rkhunter database $ sudo rkhunter --update # Populate the initial file property baseline # Run this immediately after a clean install, before running services $ sudo rkhunter --propupd # Run a full scan $ sudo rkhunter --check --sk # --sk skips keypress prompts for automated runs
Chkrootkit — Second Opinion Scanner
$ sudo apt install chkrootkit -y $ sudo chkrootkit
Auditd — System Call Auditing
The Linux Audit Daemon logs system calls, file access, and user actions to a tamper-evident log. It’s essential for forensics if you ever need to determine how a compromise occurred:
$ sudo apt install auditd audispd-plugins -y $ sudo systemctl enable auditd $ sudo systemctl start auditd # View recent audit events $ sudo aureport --summary # Check for failed login attempts $ sudo aureport --auth --failed
Schedule Automated Scans
Add a weekly rkhunter scan to cron so you’re notified automatically if anything changes:
$ sudo crontab -e# Run rkhunter every Sunday at 2am, email results 0 2 * * 0 /usr/bin/rkhunter --check --sk --report-warnings-only | mail -s "rkhunter report: $(hostname)" [email protected]
10. Web Server Hardening (Nginx & Apache)
If your VPS is serving websites, your web server is an externally-facing service that needs its own hardening layer. Default Nginx and Apache configurations are designed for compatibility, not security — they expose version information, allow potentially dangerous HTTP methods, and don’t enforce security headers that protect your visitors.
Nginx Hardening
# Hide Nginx version from response headers server_tokens off; # Prevent clickjacking attacks add_header X-Frame-Options "SAMEORIGIN" always; # Prevent MIME-type sniffing add_header X-Content-Type-Options "nosniff" always; # Enable XSS protection in older browsers add_header X-XSS-Protection "1; mode=block" always; # Enforce HTTPS for 1 year (only add once SSL is confirmed working) add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; # Control referrer information sent to other sites add_header Referrer-Policy "strict-origin-when-cross-origin" always; # Limit allowed HTTP methods per server block if ($request_method !~ ^(GET|HEAD|POST)$) { return 405; }
Apache Hardening
# Hide Apache version and OS info ServerTokens Prod ServerSignature Off # Disable directory listing Options -Indexes # Disable .htaccess overrides where not needed (improves performance + security) <Directory /> AllowOverride None </Directory> # Add security headers Header always set X-Frame-Options "SAMEORIGIN" Header always set X-Content-Type-Options "nosniff" Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
After applying these headers, verify them at securityheaders.com — paste your domain URL and it gives you an instant grade and shows exactly which headers are present or missing. Aim for an A or A+ rating. This is also a useful tool to bookmark for ongoing audits, since headers can disappear after web server or WordPress updates.
11. Database Security Basics
If your VPS runs MySQL or MariaDB, a few essential steps dramatically reduce the risk of database compromise. The default MySQL installation leaves several insecure defaults in place that need to be addressed immediately.
Run the MySQL Secure Installation Script
$ sudo mysql_secure_installationThis interactive script will walk you through:
- Setting a strong root password (or validating the existing one)
- Removing anonymous user accounts
- Disabling remote root login
- Removing the test database
- Reloading privilege tables
Answer Yes to all prompts.
Use Dedicated Database Users Per Application
Never use the MySQL root user for application database connections. Create a separate user with the minimum permissions needed for each application:
-- Create a dedicated database and user for your application CREATE DATABASE myapp_db; CREATE USER 'myapp_user'@'localhost' IDENTIFIED BY 'strong_password_here'; GRANT SELECT, INSERT, UPDATE, DELETE ON myapp_db.* TO 'myapp_user'@'localhost'; FLUSH PRIVILEGES; -- Note: 'localhost' binding means the user can ONLY connect locally -- Never use '%' (any host) unless you absolutely need remote DB access
Bind MySQL to Localhost Only
Verify that MySQL is not listening on a public network interface:
$ sudo nano /etc/mysql/mysql.conf.d/mysqld.cnf # Ensure this line is present and not commented out: bind-address = 127.0.0.1 $ sudo systemctl restart mysql # Verify MySQL is only listening on localhost $ ss -tulnp | grep 3306 # Should show 127.0.0.1:3306, not 0.0.0.0:3306
12. Ongoing Maintenance & Security Auditing
Hardening a server isn’t a one-time task — it’s the beginning of an ongoing security posture. New vulnerabilities are discovered constantly, configurations drift over time as software is installed and updated, and threat patterns evolve. Here’s what ongoing VPS security maintenance looks like in practice.
Weekly Checks
- Review auth logs for anomalies:
sudo grep "Failed password" /var/log/auth.log | tail -50 - Check Fail2Ban status:
sudo fail2ban-client status sshd - Review currently open ports:
sudo ss -tulnp— compare against what you expect - Check for users with unexpected sudo access:
getent group sudo
Monthly Checks
- Run manual rkhunter scan:
sudo rkhunter --check --sk - Review installed packages for anything unexpected:
dpkg --get-selections | less - Audit running services:
sudo systemctl list-units --type=service --state=running— disable anything you don’t need - Verify UFW rules are still correct:
sudo ufw status numbered - Check disk usage for unexpected growth:
df -h && du -sh /var/log/*
Useful One-Liners for Security Monitoring
# Show all currently listening ports and their processes $ sudo ss -tulnp # Show last 20 successful logins $ last -20 # Show all users currently logged in $ who # Check for SUID/SGID files that shouldn't be there $ find / -perm /6000 -type f 2>/dev/null # List all cron jobs for all users $ for user in $(cut -f1 -d: /etc/passwd); do crontab -u $user -l 2>/dev/null; done # Check for world-writable files (potential attack vectors) $ find / -xdev -type f -perm -0002 2>/dev/null
For production servers handling sensitive data or significant traffic, consider adding a cloud-level firewall (DigitalOcean Firewall, Linode Cloud Firewall, Hetzner Firewall) in front of your VPS firewall. These block malicious traffic before it reaches your server at all — completely transparent to your applications. Cloudflare’s proxy adds WAF and DDoS protection on top of that for HTTP/HTTPS traffic. Defense in depth means multiple layers, not just one well-configured UFW.
13. Complete VPS Hardening Checklist
Use this checklist for every new VPS you provision. Complete the steps in order — some steps depend on previous ones being in place.
Immediate (First 30 Minutes)
- Connected to server via SSH as root and confirmed access
- Ran
apt update && apt upgrade -yand rebooted if kernel was updated - Set correct timezone with
timedatectl - Set a strong root password stored in password manager
- Created a non-root sudo user (e.g.
deploy) - Copied SSH public key to new user’s
~/.ssh/authorized_keys - Tested login as new user in a separate terminal window
SSH Hardening
- Generated Ed25519 SSH key pair on local machine
- Set
PermitRootLogin noin sshd_config - Set
PasswordAuthentication noin sshd_config - Set
AllowUsers deploy(or your username) in sshd_config - Changed SSH port from 22 to custom port in sshd_config
- Ran
sudo sshd -tto validate config — no errors - Restarted sshd and confirmed login works on new port
Firewall
- Installed and enabled UFW
- Default policies set: deny incoming, allow outgoing
- SSH port allowed before enabling UFW
- HTTP (80) and HTTPS (443) allowed if running web server
- Database ports (3306, 5432, 6379) confirmed blocked from public
- Rate limiting enabled on SSH port:
sudo ufw limit [port]/tcp - Confirmed active rules with
sudo ufw status verbose
System Hardening
- Unattended-upgrades installed and configured with email notifications
- Fail2Ban installed with SSH jail enabled and custom port set
- Shared memory secured in /etc/fstab
- sysctl network hardening parameters applied via /etc/sysctl.d/99-hardening.conf
- rkhunter and chkrootkit installed; initial baseline set with
--propupd - Auditd installed and running
- Weekly rkhunter cron job configured
Web Server & Database (If Applicable)
- Web server version hidden from response headers (
server_tokens off) - Security headers configured: X-Frame-Options, X-Content-Type-Options, HSTS
- Unnecessary HTTP methods restricted
- mysql_secure_installation completed with Yes to all prompts
- Dedicated per-application database users created (no root in app configs)
- MySQL bound to 127.0.0.1 only — confirmed with
ss -tulnp | grep 3306 - Security headers graded A or A+ at securityheaders.com
A Hardened Server Is a
Reliable Server.
Most VPS compromises aren’t sophisticated — they exploit weak defaults that take less than an hour to fix. Password SSH, no firewall, unpatched packages, a root login that accepts anyone with the right credentials. Every step in this guide removes one of those easy footholds, and the compounding effect of all of them together makes your server a much harder target than the vast majority of machines on the internet.
The checklist at the end isn’t just a summary — it’s a repeatable process. Save it. Run through it every time you provision a new server. Apply it to existing servers in your fleet that haven’t been hardened. Security posture is built through consistent habits, not heroic one-time efforts.
Monitor your logs. Keep your packages updated. Review your firewall rules when you install new software. The configuration you set today will drift unless you maintain it — and a brief monthly audit is all it takes to catch the drift before it becomes a vulnerability.
Lock it down once.
Maintain it always.