Most breached servers share the same story: default SSH port, password auth still enabled, firewall wide open, and months-old kernel vulnerabilities. The attack surface practically advertises itself.
I've rebuilt too many compromised VPS instances where a single hardening step would have stopped the intrusion. This checklist walks through nine concrete actions that block the most common attack vectors—no fluff, just the commands and config snippets you need to run today.
Step 1: Disable Root Login and Enforce SSH Keys
Password authentication over SSH is a liability. Brute-force bots hammer every publicly routable IP, trying common credentials around the clock.
First, create a non-root user with sudo privileges:
adduser deployuser
usermod -aG sudo deployuser
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 the server:
ssh-copy-id -i ~/.ssh/id_ed25519.pub deployuser@your_server_ip
Test the key login, then edit /etc/ssh/sshd_config:
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
ChallengeResponseAuthentication no
Restart SSH:
systemctl restart sshd
Now only your key can unlock the door. Root is locked out entirely.
Step 2: Change the Default SSH Port
Port 22 attracts automated scanners like moths to a flame. Moving SSH to a non-standard port—say, 2849 or 49222—drops the noise in your auth logs by an order of magnitude.
In /etc/ssh/sshd_config:
Port 2849
Restart SSH, then update your firewall rules (covered next) to allow the new port. Remember to test the new port before closing your current session:
ssh -p 2849 deployuser@your_server_ip
Keep the old session open until you've confirmed the new port works.
Step 3: Configure a Stateful Firewall
A firewall should deny everything by default and allow only the services you explicitly need. UFW makes this straightforward on Debian and Ubuntu systems.
Install and enable UFW:
apt update && apt install ufw
ufw default deny incoming
ufw default allow outgoing
ufw allow 2849/tcp # your SSH port
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable
For RHEL-based systems, use firewalld:
firewall-cmd --permanent --add-port=2849/tcp
firewall-cmd --permanent --add-service=http
firewall-cmd --permanent --add-service=https
firewall-cmd --reload
Check the active rules:
ufw status verbose
# or
firewall-cmd --list-all
If you run a database, bind it to localhost and never expose port 3306 or 5432 to the internet. Applications can connect locally or over a VPN tunnel.
Step 4: Automate Security Updates
Manual patching sounds responsible until you forget for three months and a public exploit drops. Automatic updates for security patches keep the kernel and critical packages current without daily babysitting.
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 reboots if needed:
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "03:00";
For RHEL/CentOS, enable dnf-automatic:
dnf install dnf-automatic
systemctl enable --now dnf-automatic.timer
Check /etc/dnf/automatic.conf and set apply_updates = yes under the [commands] section.
Automatic updates won't catch every edge case, but they close the window on known CVEs faster than any manual schedule.
Step 5: Install and Configure Fail2Ban
Fail2Ban watches your logs for repeated authentication failures and temporarily bans the offending IPs. It's a lightweight intrusion-prevention layer that stops brute-force attempts cold.
Install Fail2Ban:
apt install fail2ban
# or
dnf install fail2ban
Copy the default config:
cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
Edit /etc/fail2ban/jail.local and enable the SSH jail:
[sshd]
enabled = true
port = 2849
logpath = /var/log/auth.log
maxretry = 3
bantime = 3600
Restart Fail2Ban:
systemctl restart fail2ban
systemctl enable fail2ban
Check banned IPs:
fail2ban-client status sshd
In support tickets I handled, the usual culprit was a compromised WordPress admin using the same weak password everywhere. Fail2Ban bought enough time to spot the pattern and lock down the account.
Step 6: Enable AppArmor or SELinux
Mandatory access control systems confine applications to their designated file paths and system calls. Even if an attacker compromises a service, they can't pivot to other parts of the system.
Debian and Ubuntu ship with AppArmor enabled by default. Verify:
aa-status
If it's not active:
systemctl enable apparmor
systemctl start apparmor
RHEL and CentOS use SELinux. Check the mode:
getenforce
If it returns Permissive or Disabled, set it to Enforcing in /etc/selinux/config:
SELINUX=enforcing
Reboot to apply. SELinux denials show up in /var/log/audit/audit.log. Use ausearch and audit2allow to troubleshoot policies that block legitimate operations, but never disable SELinux just because an app complains—fix the policy instead.
Step 7: Harden Kernel Parameters with sysctl
The kernel exposes hundreds of tunables through /etc/sysctl.conf. A few key settings improve network security and reduce the impact of certain DoS attacks.
Add these lines to /etc/sysctl.conf:
# Ignore ICMP ping requests
net.ipv4.icmp_echo_ignore_all = 1
# Disable IP forwarding
net.ipv4.ip_forward = 0
net.ipv6.conf.all.forwarding = 0
# Enable SYN cookies
net.ipv4.tcp_syncookies = 1
# Ignore ICMP redirects
net.ipv4.conf.all.accept_redirects = 0
net.ipv6.conf.all.accept_redirects = 0
# Disable source packet routing
net.ipv4.conf.all.accept_source_route = 0
net.ipv6.conf.all.accept_source_route = 0
# Log martian packets
net.ipv4.conf.all.log_martians = 1
Apply the changes:
sysctl -p
These settings won't stop a determined attacker, but they close off some low-hanging reconnaissance paths and make SYN flood attacks harder to execute.
Step 8: Deploy an Intrusion Detection System
Fail2Ban handles obvious brute-force patterns. For deeper visibility, install an IDS like AIDE or OSSEC to monitor file integrity and detect rootkits.
AIDE (Advanced Intrusion Detection Environment) creates a database of file checksums and alerts you to unexpected changes:
apt install aide
aideinit
mv /var/lib/aide/aide.db.new /var/lib/aide/aide.db
Run a check:
aide --check
Schedule daily checks via cron:
echo "0 5 * * * /usr/bin/aide --check | mail -s 'AIDE Report' [email protected]" | crontab -
OSSEC offers host-based intrusion detection with log analysis, rootkit detection, and alerting. Installation involves compiling from source or using pre-built packages, and the config lives in /var/ossec/etc/ossec.conf. Point it at your web server logs, SSH logs, and system logs to catch anomalies.
Both tools generate noise at first. Tune the rules to ignore benign changes like package updates, and focus alerts on critical paths like /etc/passwd, /etc/shadow, and web root directories.
Step 9: Implement Regular Security Audits
Hardening is not a one-time checklist. Drift happens. Services get added, firewall rules accumulate exceptions, and old user accounts linger.
Schedule a monthly audit:
- List all open ports:
ss -tulnornetstat -tuln - Review active user accounts:
cut -d: -f1 /etc/passwd - Check sudo privileges:
cat /etc/sudoersand files in/etc/sudoers.d/ - Scan for world-writable files:
find / -type f -perm -002 -ls 2>/dev/null - Review cron jobs:
crontab -lfor each user and check/etc/cron.* - Inspect listening services:
systemctl list-units --type=service --state=running - Run a vulnerability scanner: tools like Lynis automate many of these checks
Lynis is a solid open-source auditing tool:
wget -O - https://downloads.cisofy.com/lynis/lynis-3.0.9.tar.gz | tar xz
cd lynis
./lynis audit system
It produces a detailed report with a hardening index and specific recommendations.
What if the server is already live with users?
Rolling out these changes on a production server requires care. SSH changes especially can lock you out if you make a mistake.
Always test SSH config changes in a new session before closing your current one. Keep a console access method available—most VPS providers offer a web-based VNC console or serial console. Stage changes during a maintenance window and announce any brief service interruptions, especially if you're rebooting for kernel updates.
For high-traffic environments, consider blue-green deployments: harden a fresh server, migrate traffic, then decommission the old one. That way you can validate the entire stack before cutting over.
Your First 30 Minutes
Start with SSH hardening and the firewall. Those two steps block the majority of automated attacks you'll see in the wild. Add Fail2Ban next, then schedule the automatic updates.
The rest—AppArmor, sysctl tunables, IDS—can roll out over the following week. Every step tightens the perimeter. None of them require exotic tools or deep kernel knowledge, just a methodical approach and a willingness to lock things down before an incident forces your hand.
Hardening isn't glamorous, but it's the difference between a server that quietly does its job and one that ends up in your 3 a.m. incident log.
