I've rebuilt compromised servers more times than I care to count. The pattern is always the same: an unused service listening on all interfaces, a kernel months out of date, or firewall rules that might as well be a welcome mat. Most breaches don't need zero-days—they walk through unlocked doors.
This checklist covers the eight hardening steps that actually stop attackers. I've arranged them by impact, not alphabetically, because your time matters.
Step 1: Disable and remove unused services
Every running service is attack surface. SSH, your web server, and maybe a database belong on a typical hosting machine. Everything else is risk without reward.
List what's listening right now:
sudo ss -tlnp
You'll see a PID and program name for each listener. Common culprits on fresh installs include rpcbind, cups, avahi-daemon, and postfix (if you route mail elsewhere).
Stop and disable anything you don't recognize or need:
sudo systemctl stop cups
sudo systemctl disable cups
sudo systemctl mask cups
Masking prevents accidental re-enable. For older init systems, chkconfig <service> off does the job.
On cPanel servers, check EasyApache profiles and uninstall unused PHP handlers or Apache modules. A mod_status endpoint left world-readable has leaked internal IPs in tickets I've handled.
Step 2: Patch the kernel and packages
Privilege-escalation exploits live in unpatched kernels. Dirty COW, Stack Clash, and their cousins all had patches available months before widespread exploitation.
Update everything:
# Debian/Ubuntu
sudo apt update && sudo apt upgrade -y
# RHEL/CentOS/AlmaLinux
sudo dnf update -y
Reboot if the kernel updated. Check your running kernel against the installed version:
uname -r
rpm -q kernel # or dpkg -l | grep linux-image
If they don't match, you're running old code. Schedule a maintenance window and reboot.
For production, configure automatic security updates. On Ubuntu:
sudo apt install unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades
Edit /etc/apt/apt.conf.d/50unattended-upgrades to enable automatic reboots during your maintenance window if needed.
Step 3: Harden SSH access
SSH is your front door. Most brute-force attempts target it.
Edit /etc/ssh/sshd_config and make these changes:
Port 2222 # Non-standard port
PermitRootLogin no # Force sudo instead
PasswordAuthentication no # Keys only
PubkeyAuthentication yes
UsePAM yes
AllowUsers deploy adminuser # Whitelist specific users
ClientAliveInterval 300
ClientAliveCountMax 2
MaxAuthTries 3
MaxSessions 2
Restart SSH carefully (test your key auth in a second session first):
sudo sshd -t # Test config syntax
sudo systemctl restart sshd
Install and configure Fail2Ban to block repeated auth failures:
sudo apt install fail2ban # or dnf
sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
Edit /etc/fail2ban/jail.local under [sshd]:
enabled = true
port = 2222
maxretry = 3
bantime = 3600
Fail2Ban watches auth logs and updates iptables automatically. Check banned IPs with sudo fail2ban-client status sshd.
Step 4: Configure a stateful firewall
Firewall rules that allow ESTABLISHED,RELATED and then open individual ports are the baseline. Default-deny everything else.
Using iptables directly:
# Flush existing rules
sudo iptables -F
sudo iptables -X
# Default policies
sudo iptables -P INPUT DROP
sudo iptables -P FORWARD DROP
sudo iptables -P OUTPUT ACCEPT
# Allow loopback
sudo iptables -A INPUT -i lo -j ACCEPT
# Allow established connections
sudo iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
# Allow SSH (adjust port)
sudo iptables -A INPUT -p tcp --dport 2222 -j ACCEPT
# Allow HTTP/HTTPS
sudo iptables -A INPUT -p tcp --dport 80 -j ACCEPT
sudo iptables -A INPUT -p tcp --dport 443 -j ACCEPT
# Save rules
sudo netfilter-persistent save # Debian/Ubuntu
# or
sudo service iptables save # RHEL/CentOS
For firewalld on RHEL-based systems:
sudo firewall-cmd --set-default-zone=drop
sudo firewall-cmd --zone=public --add-service=ssh --permanent
sudo firewall-cmd --zone=public --add-service=http --permanent
sudo firewall-cmd --zone=public --add-service=https --permanent
sudo firewall-cmd --reload
To stop lateral movement after a compromise, block outbound connections from web application users:
# Prevent www-data from initiating outbound connections
sudo iptables -A OUTPUT -m owner --uid-owner www-data -m conntrack --ctstate NEW -j DROP
sudo iptables -I OUTPUT -m owner --uid-owner www-data -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
This lets the web server respond to inbound requests but blocks a compromised PHP script from phoning home or scanning internal networks.
So what about application-layer controls?
Firewalls stop network traffic. They don't inspect file permissions or user privileges.
Step 5: Apply the principle of least privilege
Run services as dedicated users with minimal permissions. Never run application code as root.
For a typical LAMP stack:
# Apache/Nginx runs as www-data or nginx
ps aux | grep -E 'apache|nginx' | head -n 3
# MySQL/MariaDB runs as mysql
ps aux | grep mysql | head -n 1
If you see root owning these processes, your config is wrong. Check the user directive in /etc/nginx/nginx.conf or the User directive in Apache's config.
For application files:
# Web root owned by a deploy user, readable by www-data
sudo chown -R deploy:www-data /var/www/html
sudo find /var/www/html -type d -exec chmod 750 {} \;
sudo find /var/www/html -type f -exec chmod 640 {} \;
WordPress and similar apps need write access to uploads and cache directories only. Lock down everything else.
Disable sudo for application users entirely:
sudo deluser www-data sudo # Debian/Ubuntu
Audit your sudoers file:
sudo visudo
Remove any NOPASSWD entries unless you have a specific automation need. Each one is a privilege-escalation shortcut.
Step 6: Enable and configure auditd
You can't investigate what you didn't log. auditd records system calls, file access, and privilege changes.
Install it:
sudo apt install auditd audispd-plugins # or dnf
sudo systemctl enable auditd
sudo systemctl start auditd
Add rules to watch sensitive files and commands:
# Watch /etc/passwd and /etc/shadow
sudo auditctl -w /etc/passwd -p wa -k passwd_changes
sudo auditctl -w /etc/shadow -p wa -k shadow_changes
# Watch sudo usage
sudo auditctl -w /usr/bin/sudo -p x -k sudo_exec
# Watch sshd config
sudo auditctl -w /etc/ssh/sshd_config -p wa -k sshd_config_changes
Make these permanent by adding them to /etc/audit/rules.d/hardening.rules, then:
sudo augenrules --load
Search audit logs with ausearch:
sudo ausearch -k passwd_changes
sudo ausearch -k sudo_exec -ts today
In support tickets I handled, audit logs caught an attacker adding a user to /etc/passwd hours after initial compromise. Without auditd, we would've missed the timeline entirely.
Step 7: Restrict kernel parameters with sysctl
The Linux kernel exposes tunables through /proc/sys that affect network stack behavior and process isolation.
Edit /etc/sysctl.conf or create /etc/sysctl.d/99-hardening.conf:
# Disable IP forwarding (unless you're a router)
net.ipv4.ip_forward = 0
# Ignore ICMP redirects
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.default.accept_redirects = 0
net.ipv6.conf.all.accept_redirects = 0
net.ipv6.conf.default.accept_redirects = 0
# Ignore source-routed packets
net.ipv4.conf.all.accept_source_route = 0
net.ipv4.conf.default.accept_source_route = 0
# Enable SYN cookies (SYN flood protection)
net.ipv4.tcp_syncookies = 1
# Log martians (packets with impossible source IPs)
net.ipv4.conf.all.log_martians = 1
# Restrict dmesg access to root
kernel.dmesg_restrict = 1
# Restrict kernel pointer exposure
kernel.kptr_restrict = 2
# Disable core dumps for SUID programs
fs.suid_dumpable = 0
Apply the changes:
sudo sysctl -p
These settings close common network-based attacks and limit information leakage to unprivileged users.
Step 8: Implement file integrity monitoring
File integrity monitoring (FIM) detects unauthorized changes to system binaries, configs, and application code.
AIDE (Advanced Intrusion Detection Environment) is the standard tool:
sudo apt install aide # or dnf
Initialize the database (this takes a few minutes):
sudo aideinit
sudo mv /var/lib/aide/aide.db.new /var/lib/aide/aide.db
Run a check:
sudo aide --check
Schedule daily checks via cron:
sudo crontab -e
Add:
0 2 * * * /usr/bin/aide --check | mail -s "AIDE Report" [email protected]
After legitimate changes (a kernel update, Apache config edit), update the baseline:
sudo aide --update
sudo mv /var/lib/aide/aide.db.new /var/lib/aide/aide.db
False positives are common at first. Tune /etc/aide/aide.conf to exclude noisy directories like /var/log or /tmp.
What to check first
When you inherit a server or audit your own setup, prioritize these steps in order:
- Patch the kernel and packages (Step 2)—this closes known exploits immediately.
- Harden SSH (Step 3)—stop the most common attack vector.
- Configure the firewall (Step 4)—reduce your network exposure to only necessary ports.
- Disable unused services (Step 1)—remove attack surface.
The remaining steps—privilege management, logging, sysctl tuning, and FIM—layer defense in depth. They slow down an attacker who breaches the perimeter and give you visibility into what happened.
Hardening isn't a one-time checklist. New vulnerabilities appear, software updates introduce changes, and configurations drift. Schedule a quarterly review of these eight steps. Check your firewall rules, audit your service list, and verify your patches are current. The usual culprit in breaches isn't a sophisticated attack—it's a server that someone forgot to maintain.
Frequently asked questions
How often should I update my server?
Security patches as soon as they're released. Full system updates weekly or monthly depending on your change control process. Configure automatic security updates if you can't commit to manual checks.
Can I use UFW instead of iptables?
Yes. UFW is a simpler frontend for iptables and works well for straightforward rules. For advanced stateful filtering or per-user OUTPUT rules, you'll need iptables or nftables directly.
What if I need a service that listens on all interfaces?
Bind it to localhost only and use an SSH tunnel or VPN for remote access. For example, MySQL should listen on 127.0.0.1:3306 unless you have a specific multi-server architecture.
Does moving SSH to a non-standard port actually help?
It dramatically reduces automated brute-force noise in your logs. It's not security through obscurity—you're still using key auth and Fail2Ban—but it cuts bot traffic by over ninety percent.
Should I disable IPv6 if I'm not using it?
Yes, or configure your firewall to handle it. Unmanaged IPv6 interfaces bypass IPv4-only firewall rules and create an unmonitored path into your server.
![Server Security Hardening Checklist: 8 Steps [2026]](/images/blog/server-security-hardening-checklist-8-steps-2026.jpg)