Most server breaches happen because someone skipped the basics. You patch the application but forget the SSH port is wide open with password auth. Or you lock down SSH but never configure a firewall. Every missed step is an open door.
This checklist walks through ten hardening steps I've used on production VPS and dedicated servers. They're not exotic. They work because they close the gaps attackers scan for first.
1. Disable root SSH login and use key-based authentication
Password authentication over SSH is the easiest vector for brute-force. Disable it entirely.
Generate an SSH key pair on your local machine if you haven't already:
ssh-keygen -t ed25519 -C "[email protected]"
Copy the public key to your server:
ssh-copy-id user@your_server_ip
Then edit /etc/ssh/sshd_config and set:
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
Restart SSH:
systemctl restart sshd
Test the new connection in a separate terminal before closing your current session. If something breaks, you still have access to fix it.
2. Change the default SSH port
Port 22 gets hammered by automated scanners. Moving SSH to a non-standard port (like 2222 or 49222) drops that noise to nearly zero.
In /etc/ssh/sshd_config, change:
Port 2222
Restart SSH and update your firewall rules to allow the new port. When you reconnect, specify the port:
ssh -p 2222 user@your_server_ip
This isn't security through obscurity alone—it's reducing attack surface. Bots scan port 22. They rarely scan all 65,535 ports.
3. Configure a firewall with UFW or firewalld
A firewall blocks everything except the services you explicitly allow. On Ubuntu and Debian, UFW is the simplest choice. On RHEL-based systems, use firewalld.
UFW example:
ufw default deny incoming
ufw default allow outgoing
ufw allow 2222/tcp # your SSH port
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable
Check the status:
ufw status verbose
firewalld example:
firewall-cmd --permanent --add-service=ssh
firewall-cmd --permanent --add-service=http
firewall-cmd --permanent --add-service=https
firewall-cmd --reload
If you changed the SSH port, add it as a custom rule:
firewall-cmd --permanent --add-port=2222/tcp
firewall-cmd --reload
Don't skip this step. I've seen servers compromised within hours of being online because no firewall was active.
4. Install and configure fail2ban
fail2ban watches your logs and bans IPs that show malicious behavior—repeated failed SSH logins, for example.
Install it:
apt install fail2ban # Debian/Ubuntu
yum install fail2ban # RHEL/CentOS
The default config (/etc/fail2ban/jail.conf) should not be edited directly. Copy it:
cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
In jail.local, enable the SSH jail and set the ban time:
[sshd]
enabled = true
port = 2222
maxretry = 3
bantime = 3600
Restart fail2ban:
systemctl restart fail2ban
Check banned IPs:
fail2ban-client status sshd
In support tickets I handled, fail2ban routinely blocked hundreds of IPs per week on public-facing servers.
5. Enable automatic security updates
Manual patching is unreliable. Automate it.
On Debian/Ubuntu, install unattended-upgrades:
apt install unattended-upgrades
dpkg-reconfigure --priority=low unattended-upgrades
Edit /etc/apt/apt.conf.d/50unattended-upgrades to enable automatic security updates and optionally reboot:
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "03:00";
On RHEL/CentOS, use dnf-automatic:
yum install dnf-automatic
systemctl enable --now dnf-automatic.timer
Edit /etc/dnf/automatic.conf and set:
apply_updates = yes
Automatic updates won't catch everything, but they close the window for known exploits.
6. Disable unused services and remove unnecessary packages
Every running service is a potential entry point. List active services:
systemctl list-units --type=service --state=running
Disable anything you don't need:
systemctl disable --now <service_name>
Common candidates: rpcbind, avahi-daemon, cups, bluetooth.
Remove unused packages:
apt autoremove # Debian/Ubuntu
yum autoremove # RHEL/CentOS
A smaller attack surface is easier to defend.
7. Set up log monitoring with logwatch or journalctl alerts
Logs tell you what's happening. If you're not reading them, you're flying blind.
logwatch emails you a daily summary:
apt install logwatch
Edit /usr/share/logwatch/default.conf/logwatch.conf and set your email:
MailTo = [email protected]
Run it manually to test:
logwatch --detail high --mailto [email protected] --range today
journalctl can filter logs by priority:
journalctl -p err -b
Set up a cron job to email critical errors:
0 8 * * * journalctl -p crit --since "24 hours ago" | mail -s "Critical logs" [email protected]
Log monitoring catches issues before they become incidents.
8. Configure file integrity monitoring with AIDE
AIDE (Advanced Intrusion Detection Environment) hashes your file system and alerts you when critical files change.
Install it:
apt install aide # Debian/Ubuntu
yum install aide # RHEL/CentOS
Initialize the database:
aideinit
On some systems, the command is aide --init. The initial database is written to /var/lib/aide/aide.db.new. Move it:
mv /var/lib/aide/aide.db.new /var/lib/aide/aide.db
Run a check:
aide --check
Schedule daily checks via cron:
0 5 * * * /usr/bin/aide --check | mail -s "AIDE report" [email protected]
If an attacker modifies /etc/passwd or installs a backdoor, AIDE will tell you.
9. Restrict user privileges and use sudo properly
Never run applications as root. Create service accounts with minimal permissions.
For human users, grant sudo access only when needed. Edit /etc/sudoers with visudo:
username ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart nginx
This allows username to restart nginx without a password, but nothing else.
For service accounts, disable login:
useradd -r -s /usr/sbin/nologin serviceuser
The -r flag creates a system account. The -s flag sets the shell to nologin, preventing interactive login.
10. Enable SELinux or AppArmor for mandatory access control
SELinux (on RHEL-based systems) and AppArmor (on Debian-based systems) enforce access policies that limit what processes can do, even if they're compromised.
SELinux:
Check the current mode:
getenforce
If it's Permissive or Disabled, set it to Enforcing in /etc/selinux/config:
SELINUX=enforcing
Reboot for the change to take effect. Review denials in /var/log/audit/audit.log.
AppArmor:
Check the status:
aa-status
Enable profiles for installed services:
aa-enforce /etc/apparmor.d/*
Both tools have a learning curve, but they've stopped privilege escalation attacks cold in environments I've managed.
What about intrusion detection systems?
If you're running multiple servers or handling sensitive data, consider an IDS like Snort or Suricata. They monitor network traffic for suspicious patterns.
For single VPS setups, the steps above are sufficient. Most attacks target the low-hanging fruit: weak SSH, no firewall, outdated packages. Close those gaps first.
Automate the checklist with Ansible or shell scripts
Manual hardening works for one server. For five or fifty, automate it.
Here's a minimal shell script that applies steps 1, 3, 4, and 5:
#!/bin/bash
set -e
# Update system
apt update && apt upgrade -y
# Install packages
apt install -y ufw fail2ban unattended-upgrades
# Configure UFW
ufw default deny incoming
ufw default allow outgoing
ufw allow 2222/tcp
ufw allow 80/tcp
ufw allow 443/tcp
ufw --force enable
# Enable fail2ban
systemctl enable --now fail2ban
# Enable unattended-upgrades
dpkg-reconfigure -plow unattended-upgrades
echo "Hardening complete. Review SSH config and reboot."
For Ansible, write a playbook that applies the same tasks across an inventory. Version-control the playbook so every server starts with the same baseline.
Start with SSH and the firewall
If you implement only two items from this checklist, make them SSH hardening and firewall configuration. Those two steps block the majority of opportunistic attacks.
The rest—fail2ban, automatic updates, log monitoring, file integrity checks—add depth. They catch what slips through the first line of defense.
Hardening isn't a one-time project. It's a baseline. Review it quarterly, automate what you can, and adjust as your infrastructure grows.
![Server Security Hardening Checklist: 10 Steps [2026]](/images/blog/server-security-hardening-checklist-10-steps-2026.jpg)