When you spin up a fresh VPS or dedicated server, it's exposed to the internet immediately. Port scanners, brute-force bots, and vulnerability probes start knocking within minutes. Hardening means locking down the default setup so automated attacks hit walls instead of open doors.
This checklist walks through twelve foundational steps in the order you'd apply them on day one. Each step defines the term, explains why it matters, and shows the actual commands. No prior security experience required.
1. Update the system packages
What it means: Operating systems ship security patches and bug fixes as package updates. A fresh server image might be weeks or months old.
Why it matters: Unpatched software is the easiest entry point. Automated exploits scan for known CVEs in outdated packages.
How to do it:
On Debian/Ubuntu:
sudo apt update
sudo apt upgrade -y
sudo apt autoremove -y
On CentOS/AlmaLinux/Rocky:
sudo dnf update -y
Reboot if kernel updates were installed:
sudo reboot
Set a reminder to run updates weekly. Automated unattended-upgrades packages exist, but for a first server it's safer to apply them manually until you understand what breaks.
2. Create a non-root user with sudo privileges
What it means: The root account has unrestricted power. Running commands as root means one typo can delete everything. A non-root user with sudo can escalate only when needed.
Why it matters: If an attacker compromises a service running as root, they own the entire machine. If they compromise a limited user, damage is contained.
How to do it:
adduser deploy
usermod -aG sudo deploy
On RHEL-based systems, replace sudo with wheel:
usermod -aG wheel deploy
Test the new account before you lock out root:
su - deploy
sudo whoami
If it prints root, sudo is working. From now on, log in as deploy and use sudo for admin tasks.
3. Disable root login over SSH
What it means: SSH is the remote shell protocol. By default, root can log in directly. Disabling root login forces attackers to guess both a username and a password.
Why it matters: Brute-force scripts assume root exists. Two-factor authentication (username + password) is harder to crack than password alone.
How to do it:
Open the SSH config:
sudo nano /etc/ssh/sshd_config
Find or add:
PermitRootLogin no
Restart SSH:
sudo systemctl restart sshd
Test from another terminal window before closing your current session. If you lock yourself out, cloud providers offer a console.
4. Use SSH keys instead of passwords
What it means: SSH can authenticate with a key pair (public + private) instead of a password. The private key stays on your laptop; the public key goes on the server.
Why it matters: Passwords can be brute-forced. A 4096-bit RSA key cannot.
How to do it:
On your local machine (not the server), generate a key:
ssh-keygen -t rsa -b 4096 -C "[email protected]"
Press Enter to accept the default path. Set a passphrase when prompted (optional but recommended).
Copy the public key to the server:
ssh-copy-id deploy@your-server-ip
Log in again. If it doesn't ask for the server password (only the key passphrase, if you set one), keys are working.
Now disable password authentication entirely:
sudo nano /etc/ssh/sshd_config
Set:
PasswordAuthentication no
PubkeyAuthentication yes
Restart SSH:
sudo systemctl restart sshd
Keep a backup of your private key in a password manager. Lose it and you're locked out.
5. Change the default SSH port
What it means: SSH listens on port 22 by default. Moving it to a non-standard port (like 2200) stops low-effort bots that only scan 22.
Why it matters: Port 22 gets thousands of login attempts per day. A different port cuts noise by 99%.
How to do it:
sudo nano /etc/ssh/sshd_config
Change:
Port 2200
If you're running a firewall (next step), open the new port first:
sudo ufw allow 2200/tcp
Restart SSH:
sudo systemctl restart sshd
Reconnect with:
ssh -p 2200 deploy@your-server-ip
This is security by obscurity, not real protection. But it reduces log spam and saves CPU cycles filtering garbage traffic.
6. Configure a firewall with UFW or firewalld
What it means: A firewall blocks incoming connections except those you explicitly allow. UFW (Uncomplicated Firewall) is the Debian/Ubuntu tool; firewalld is the RHEL/CentOS equivalent.
Why it matters: Open ports are open doors. If you're running a web server, only ports 80, 443, and your SSH port should be reachable.
How to do it (UFW on Ubuntu):
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 2200/tcp # your SSH port
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
Check status:
sudo ufw status verbose
How to do it (firewalld on CentOS):
sudo firewall-cmd --permanent --add-port=2200/tcp
sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --reload
If you add a service later (like MySQL or Redis), remember to open its port only to trusted IPs, not the entire internet.
7. Install and configure Fail2Ban
What it means: Fail2Ban watches log files for repeated failed login attempts and temporarily bans the offending IP by adding a firewall rule.
Why it matters: Even with SSH keys, some services (FTP, webmail, WordPress login) still use passwords. Fail2Ban stops brute-force attempts.
How to do it:
sudo apt install fail2ban -y
Copy the default config:
sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
Edit the local copy:
sudo nano /etc/fail2ban/jail.local
Under [sshd], set:
enabled = true
port = 2200
maxretry = 3
bantime = 3600
Restart Fail2Ban:
sudo systemctl restart fail2ban
sudo systemctl enable fail2ban
Check active jails:
sudo fail2ban-client status
You'll see banned IPs in your firewall rules and in /var/log/fail2ban.log.
8. Disable unused services and remove unnecessary packages
What it means: Default server images often include services you'll never use (Bluetooth daemons, print servers, GUI components). Each running service is a potential vulnerability.
Why it matters: The smaller your attack surface, the fewer things can break or be exploited.
How to do it:
List active services:
systemctl list-units --type=service --state=running
Disable services you don't recognize or need:
sudo systemctl disable bluetooth.service
sudo systemctl stop bluetooth.service
Remove orphaned packages:
sudo apt autoremove -y
On a web server, you typically need SSH, a web server (Nginx or Apache), a database (MySQL or PostgreSQL), and maybe PHP-FPM. Everything else is optional.
9. Set up automatic security updates (carefully)
What it means: The unattended-upgrades package applies security patches automatically without manual intervention.
Why it matters: Zero-day exploits appear without warning. Automatic patching closes the window between disclosure and your next manual update.
How to do it:
sudo apt install unattended-upgrades -y
sudo dpkg-reconfigure -plow unattended-upgrades
Edit the config:
sudo nano /etc/apt/apt.conf.d/50unattended-upgrades
Enable only security updates by uncommenting:
"${distro_id}:${distro_codename}-security";
Comment out:
// "${distro_id}:${distro_codename}-updates";
This avoids surprise breakage from feature updates. Security patches are safer to auto-apply.
Test the setup:
sudo unattended-upgrades --dry-run --debug
Logs go to /var/log/unattended-upgrades/.
10. Configure file permissions and disable unused SUID binaries
What it means: SUID (Set User ID) binaries run with the permissions of their owner, not the user executing them. A SUID root binary can be exploited for privilege escalation.
Why it matters: Attackers look for SUID binaries to escape restricted shells or gain root access.
How to do it:
Find all SUID binaries:
find / -perm -4000 -type f 2>/dev/null
Common ones like sudo, passwd, and mount are necessary. Rare ones like arping or traceroute can often be stripped:
sudo chmod u-s /usr/bin/arping
For application files, set restrictive permissions:
sudo chown -R www-data:www-data /var/www/html
sudo find /var/www/html -type d -exec chmod 755 {} \;
sudo find /var/www/html -type f -exec chmod 644 {} \;
Never set 777 on production files.
11. Enable and configure auditd for logging
What it means: auditd is the Linux auditing daemon. It records system calls, file access, and commands run by users.
Why it matters: When something goes wrong (or someone breaks in), logs tell you what happened. Auditd captures events the regular syslog misses.
How to do it:
sudo apt install auditd -y
sudo systemctl enable auditd
sudo systemctl start auditd
Add a rule to watch the /etc/passwd file:
sudo auditctl -w /etc/passwd -p wa -k passwd_changes
Make rules persistent:
sudo nano /etc/audit/rules.d/audit.rules
Add:
-w /etc/passwd -p wa -k passwd_changes
-w /etc/shadow -p wa -k shadow_changes
-w /var/log/auth.log -p wa -k auth_log_changes
Reload:
sudo augenrules --load
Search logs:
sudo ausearch -k passwd_changes
Audit logs go to /var/log/audit/audit.log.
12. Set up a basic intrusion detection system with AIDE
What it means: AIDE (Advanced Intrusion Detection Environment) creates a baseline snapshot of your filesystem. Later scans detect if files were modified, added, or deleted.
Why it matters: Rootkits and backdoors modify system binaries. AIDE catches unauthorized changes.
How to do it:
sudo apt install aide -y
Initialize the database (takes a few minutes):
sudo aideinit
Move the database into place:
sudo mv /var/lib/aide/aide.db.new /var/lib/aide/aide.db
Run a check:
sudo aide --check
If you make legitimate changes (install software, edit configs), update the baseline:
sudo aide --update
sudo mv /var/lib/aide/aide.db.new /var/lib/aide/aide.db
Schedule weekly scans with cron:
sudo crontab -e
Add:
0 3 * * 0 /usr/bin/aide --check | mail -s "AIDE Report" [email protected]
You'll get emailed reports of any filesystem changes.
So what's the order of operations?
Apply these steps in sequence on a fresh server:
- Update packages, reboot if needed
- Create non-root user
- Set up SSH keys
- Disable root login and password auth
- Change SSH port
- Configure firewall
- Install Fail2Ban
- Disable unused services
- Enable automatic security updates
- Lock down file permissions
- Start auditd
- Initialize AIDE
Each step builds on the last. You wouldn't disable password auth before adding SSH keys, or enable the firewall before opening your SSH port.
What to check first when something goes wrong
If you can't connect after hardening, SSH with verbose output to see where it fails:
ssh -vvv -p 2200 deploy@your-server-ip
Check your firewall rules:
sudo ufw status numbered
Check Fail2Ban:
sudo fail2ban-client status sshd
Read the auth log:
sudo tail -f /var/log/auth.log
Ninety percent of lockouts come from forgetting to allow the new SSH port in the firewall or fat-fingering the port number in sshd_config. The other ten percent are Fail2Ban bans you triggered yourself. Both are reversible from your provider's web console.
Hardening isn't a one-time task. New vulnerabilities appear, software changes, and your own requirements shift. Set a calendar reminder to review these twelve steps every few months. The fifteen minutes you spend re-checking configs will save hours recovering from a breach.
