Most hardening checklists tell you what to do. Few explain what breaks when you do it wrong. In support tickets I handled, the pattern was always the same: someone followed a tutorial, locked themselves out, or left a bigger hole than they started with.
Here are the twelve mistakes I see over and over, along with the fix that actually works.
Mistake 1: Changing the SSH port without updating the firewall
You edit /etc/ssh/sshd_config and set Port 2222. Restart SSH. Now you can't connect.
The usual culprit: your firewall still only allows port 22. Before you restart SSH, update your firewall rules—whether that's iptables, ufw, or firewalld. For ufw:
sudo ufw allow 2222/tcp
sudo ufw reload
Only then restart SSH:
sudo systemctl restart sshd
Test the new port in a second terminal before closing your original session. If it fails, you still have a way back in.
Mistake 2: Disabling root login before creating a sudo user
You set PermitRootLogin no in sshd_config and restart SSH. Now nobody can log in.
Always create a non-root user with sudo privileges first:
adduser deploy
usermod -aG sudo deploy # Debian/Ubuntu
# OR
usermod -aG wheel deploy # CentOS/RHEL
Test that deploy can sudo. Then disable root login.
Keep one root session open while you test the new user. That safety net has saved me more times than I can count.
Mistake 3: Forgetting to set a password expiration policy
You enforce strong passwords but never expire them. Accounts with old, potentially leaked credentials sit unchanged for years.
Set a maximum password age in /etc/login.defs:
PASS_MAX_DAYS 90
PASS_MIN_DAYS 1
PASS_WARN_AGE 7
For existing users:
sudo chage -M 90 -m 1 -W 7 username
Check current settings with chage -l username. Service accounts that need static passwords should be explicitly set to never expire, not left at the default.
Mistake 4: Blocking all ICMP instead of rate-limiting it
You drop all ICMP packets to "harden" the server. Now you can't troubleshoot network issues and Path MTU Discovery breaks.
Rate-limit instead:
sudo iptables -A INPUT -p icmp --icmp-type echo-request -m limit --limit 1/s --limit-burst 5 -j ACCEPT
sudo iptables -A INPUT -p icmp --icmp-type echo-request -j DROP
Always allow ICMP types 3 (Destination Unreachable) and 4 (Source Quench)—they're essential for TCP to work properly.
Mistake 5: Installing fail2ban without customizing the jail settings
Default fail2ban installs often watch too few services or use ban times that are too short. An attacker hitting SSH twice an hour will never trip the default threshold.
Edit /etc/fail2ban/jail.local:
[DEFAULT]
bantime = 3600
findtime = 600
maxretry = 3
[sshd]
enabled = true
port = 2222
logpath = /var/log/auth.log
Restart fail2ban and check the status:
sudo systemctl restart fail2ban
sudo fail2ban-client status sshd
Monitor your logs in the first week. If legitimate users get banned, adjust maxretry or whitelist their IPs in ignoreip.
Mistake 6: Running chmod 777 on directories to fix permission errors
Something breaks. You run chmod -R 777 /var/www to make it work. Now any user can write to your web root.
Find the correct owner and set proper 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 {} \;
If the web server needs to write to specific directories (uploads, cache), set those to 775 or use ACLs. Never make everything world-writable.
So what if your firewall rules look correct but traffic still gets through?
Check the rule order. Firewalls process rules top-to-bottom and stop at the first match.
If you have this:
sudo iptables -A INPUT -j ACCEPT
sudo iptables -A INPUT -p tcp --dport 22 -j DROP
The DROP rule never runs. List your rules:
sudo iptables -L INPUT -v -n --line-numbers
Delete bad rules by line number:
sudo iptables -D INPUT 1
Insert restrictive rules at the top, permissive ones at the bottom. Always end with a default DROP policy:
sudo iptables -P INPUT DROP
Mistake 7: Leaving default service ports open
MySQL listens on 0.0.0.0:3306. Your database is exposed to the internet even though only localhost needs access.
Edit /etc/mysql/mysql.conf.d/mysqld.cnf (or equivalent):
bind-address = 127.0.0.1
Restart MySQL and verify:
sudo systemctl restart mysql
sudo netstat -tulpn | grep 3306
You should see 127.0.0.1:3306, not 0.0.0.0:3306. Same principle applies to Redis, Memcached, PostgreSQL, and any other backend service.
Mistake 8: Not testing SELinux policies before enforcing them
You enable SELinux in enforcing mode. Half your services stop working and you have no idea why.
Start in permissive mode:
sudo setenforce 0
Run your application and check for denials:
sudo ausearch -m avc -ts recent
Use audit2allow to generate policies for legitimate denials:
sudo ausearch -m avc -ts recent | audit2allow -M mypolicy
sudo semodule -i mypolicy.pp
Once everything works in permissive mode, switch to enforcing:
sudo setenforce 1
Make it permanent in /etc/selinux/config.
Mistake 9: Updating everything at once without a snapshot
You run apt upgrade -y or yum update -y on a production server. A kernel update breaks a driver and the server won't boot.
Take a snapshot first if you're on a VPS. On bare metal, at minimum:
sudo dpkg --get-selections > ~/package-list-$(date +%F).txt
Update in stages: security patches first, then kernel, then application packages. Reboot between kernel updates. If you're managing multiple servers, test updates on one before rolling them out.
Mistake 10: Using the same SSH key everywhere
You generate one SSH key and copy it to every server. One compromised workstation means every server is compromised.
Generate unique keys per client or per role:
ssh-keygen -t ed25519 -f ~/.ssh/id_production_servers -C "production access"
Use SSH config to map keys to hosts:
Host prod-*
IdentityFile ~/.ssh/id_production_servers
User deploy
Host dev-*
IdentityFile ~/.ssh/id_dev_servers
User devuser
Rotate keys periodically. Remove old authorized keys from ~/.ssh/authorized_keys when people leave or roles change.
Mistake 11: Ignoring log rotation and filling up the disk
Your server stops responding. df -h shows /var at 100%. A single application log has grown to 80 GB.
Check that logrotate is configured:
ls /etc/logrotate.d/
For custom applications, create a config like /etc/logrotate.d/myapp:
/var/log/myapp/*.log {
daily
rotate 7
compress
delaycompress
missingok
notifexist
create 0640 appuser appgroup
}
Test it:
sudo logrotate -d /etc/logrotate.d/myapp
sudo logrotate -f /etc/logrotate.d/myapp
Set up monitoring alerts for disk usage. Catching it at 80% is better than discovering it at 100%.
Mistake 12: Hardening the server but not the applications
Your OS is locked down. SSH uses keys, the firewall is tight, and updates are automatic. Then WordPress gets compromised because you're running outdated plugins.
Server hardening is the foundation, not the finish line. Keep application dependencies updated:
wp plugin update --all # WordPress
composer update # PHP
npm audit fix # Node.js
sudo systemctl disable apache2
sudo systemctl stop apache2
Run web application firewalls (ModSecurity, Cloudflare WAF) and monitor application logs for suspicious patterns. A hardened server running vulnerable code is still a compromised server.
What to audit after hardening
Once you've fixed these twelve mistakes, verify the changes actually stuck:
- Run
ss -tulpnand confirm only expected ports are listening. - Check
sudo iptables -L -v -norsudo ufw status verboseto confirm firewall rules. - Test SSH access from an external IP using your new configuration.
- Verify
fail2ban-client statusshows active jails. - Scan your server with
lynisornmapfrom another machine to find anything you missed.
Document what you changed and why. Six months from now you won't remember, and the next admin needs a map.
What's the most common mistake after hardening?
Locking yourself out during the SSH config changes. Always test with a second session open.
Should I use a non-standard SSH port?
It cuts down log noise from automated scanners, but it's security through obscurity. Do it after you've implemented key-based auth, not instead of it.
How often should I rotate SSH keys?
Yearly for production systems, or immediately if an authorized user leaves or a workstation is compromised.
Can I automate these hardening steps?
Yes. Ansible, Puppet, and Chef all have hardening roles. Test them in staging first—automation makes mistakes faster.
What if SELinux breaks my application and I can't figure out the policy?
Switch back to permissive mode, collect detailed audit logs, and consult the application's documentation. Some software simply doesn't support SELinux well.
Start with the highest-impact fixes
You don't need to implement all twelve fixes at once. Start with SSH (mistakes 1, 2, and 10), then firewall rules (4 and 7), then monitoring (5 and 11). Each layer adds defense.
The goal isn't a perfect server. It's a server that's hard enough to break that attackers move on to easier targets.
