How to Secure a VPS Server

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

📖 ~5,800 words 🐧 Linux / Ubuntu / Debian ⚡ Updated 2026

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.

🐧
Distro Note

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 TypeSeverityHow CommonPrimary Defense
SSH brute-forceCriticalExtremely common — starts within hours of provisioningSSH key auth + Fail2Ban
Outdated package exploitsCriticalVery common — unpatched servers are low-hanging fruitAutomatic security updates
Root login compromiseCriticalCommon — default root SSH is targeted heavilyDisable root SSH login
Open port exploitationHighCommon — scanners find unnecessary open servicesFirewall (UFW) + port audit
Web app vulnerabilitiesHighVery common for WordPress/PHP sitesWeb server hardening + WAF
Cryptomining malwareHighIncreasingly common after any successful breachRootkit scanning + monitoring
Privilege escalationMediumTargeted — follows initial low-privilege compromiseMinimal 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.

⚠️
Do This Before You Put Anything Else on the Server

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:

Terminal
$ 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:

Run as root — Ubuntu / Debian
# 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:

Terminal
$ 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):

Terminal
$ 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

Terminal — run as root
# 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:

Terminal — run as root
# 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.

New terminal window — test before proceeding
$ ssh deploy@your_server_ip
# If this works, your new user is configured correctly
⚠️
Always Test in a New Window First

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:

Your local machine — not the server
# 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

Terminal — on your server
$ 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:

/etc/ssh/sshd_config — key settings
# 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:

Terminal
# 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
💡
Is Changing the SSH Port Worth It?

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

Terminal
# 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

ServiceUFW CommandNotes
SSH (default port)sudo ufw allow 22/tcpOnly if you kept port 22
SSH (custom port)sudo ufw allow 2222/tcpMatch your sshd_config Port setting
HTTPsudo ufw allow 80/tcpRequired for web server + Let’s Encrypt
HTTPSsudo ufw allow 443/tcpRequired for SSL traffic
MySQL (local only)sudo ufw deny 3306Never expose MySQL to the internet
PostgreSQL (local only)sudo ufw deny 5432Never expose PostgreSQL to the internet
Allow specific IPsudo ufw allow from 203.0.113.5Allow all traffic from a trusted IP
Rate-limit SSHsudo ufw limit 2222/tcpBlocks IPs with >6 connections in 30s
⚠️
Never Expose Databases to the Internet

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:

Terminal
# 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.

Terminal
# 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:

Terminal
$ sudo nano /etc/apt/apt.conf.d/50unattended-upgrades

The key settings to verify or add:

/etc/apt/apt.conf.d/50unattended-upgrades
// 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";
Automatic Reboot: Trade-off to Know

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

Terminal
$ 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:

/etc/fail2ban/jail.local — [DEFAULT] section
# 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:

/etc/fail2ban/jail.local — [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
Terminal — enable and verify
$ 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
💡
Extend Fail2Ban Beyond SSH

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:

Terminal
$ sudo nano /etc/fstab

Add this line at the end of the file:

/etc/fstab — add this line
tmpfs /run/shm tmpfs defaults,noexec,nosuid,nodev 0 0

Harden 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:

Terminal
$ sudo nano /etc/sysctl.d/99-hardening.conf
/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
Terminal — apply the settings
$ sudo sysctl -p /etc/sysctl.d/99-hardening.conf

9. 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

Terminal
$ 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

Terminal
$ 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:

Terminal
$ 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:

Terminal
$ sudo crontab -e
crontab — add this line
# 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

/etc/nginx/nginx.conf — security additions in the http{} block
# 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

/etc/apache2/conf-available/security.conf
# 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"
🔍
Check Your Security Headers

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

Terminal
$ sudo mysql_secure_installation

This 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:

MySQL shell — run as root
-- 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:

Terminal
$ 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

Useful monitoring commands
# 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
🛡️
Consider a Managed Firewall or Security Service

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 -y and 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 no in sshd_config
  • Set PasswordAuthentication no in 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 -t to 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.