Most breached servers I've cleaned up shared a pattern: default SSH ports, root login enabled, no firewall rules, and months-old kernel versions. The attack surface was wide open.
Hardening a production server doesn't require exotic tools or paranoid overkill. It requires nine specific controls applied consistently. These steps block the automated scans that probe every IP block and the credential stuffing bots that try common passwords every few seconds.
1. Disable Root SSH and Use Key Authentication
Password authentication over SSH is the weakest link. Brute-force attacks against root accounts run constantly.
First, create a non-root user with sudo privileges:
adduser deploy
usermod -aG sudo deploy
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 deploy@your_server_ip
Test the key login before you lock out password authentication. Once confirmed, edit /etc/ssh/sshd_config:
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
ChallengeResponseAuthentication no
Restart SSH:
systemctl restart sshd
Every support ticket I've handled involving a compromised server had password auth enabled. Keys eliminate that entire class of attack.
2. Change the Default SSH Port
This won't stop a determined attacker, but it drops automated scan noise to near zero. Most bots scan port 22 and move on.
Pick a port above 1024 and below 65535. Edit /etc/ssh/sshd_config:
Port 2289
If you run a firewall (you should), open the new port before restarting SSH:
ufw allow 2289/tcp
systemctl restart sshd
Update your local SSH config (~/.ssh/config) so you don't forget:
Host myserver
HostName your_server_ip
Port 2289
User deploy
IdentityFile ~/.ssh/id_ed25519
Now you connect with ssh myserver.
3. Configure a Firewall with Default-Deny Rules
An unconfigured firewall allows everything. You want the opposite: block everything, then open only the ports your services need.
On Ubuntu/Debian, use ufw:
ufw default deny incoming
ufw default allow outgoing
ufw allow 2289/tcp # SSH
ufw allow 80/tcp # HTTP
ufw allow 443/tcp # HTTPS
ufw enable
On CentOS/RHEL, use firewalld:
firewall-cmd --set-default-zone=drop
firewall-cmd --permanent --add-port=2289/tcp
firewall-cmd --permanent --add-service=http
firewall-cmd --permanent --add-service=https
firewall-cmd --reload
Verify what's open:
ufw status verbose
# or
firewall-cmd --list-all
A simple firewall blocks more attacks than any intrusion detection system you'll configure later.
4. Keep the Kernel and Packages Updated
Unpatched software is the second most common entry point after weak SSH configs. Exploits for known vulnerabilities appear within days of disclosure.
Enable automatic security updates on Debian/Ubuntu:
apt install unattended-upgrades
dpkg-reconfigure -plow unattended-upgrades
Edit /etc/apt/apt.conf.d/50unattended-upgrades to ensure security updates apply automatically:
Unattended-Upgrade::Allowed-Origins {
"${distro_id}:${distro_codename}-security";
};
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "03:00";
For CentOS/RHEL, use dnf-automatic:
dnf install dnf-automatic
systemctl enable --now dnf-automatic.timer
Edit /etc/dnf/automatic.conf:
apply_updates = yes
Manual updates still matter for custom-compiled software and kernel modules. Schedule a monthly review.
5. Disable Unused Services and Remove Unnecessary Packages
Every running service is a potential target. The smaller your attack surface, the fewer things can break.
List active services:
systemctl list-units --type=service --state=running
Disable anything you don't recognize or need:
systemctl disable --now avahi-daemon
systemctl disable --now cups
Remove packages you didn't explicitly install:
apt autoremove
I've seen servers running print daemons and Bluetooth managers in data centers. Disable them.
6. Configure Fail2Ban to Block Brute-Force Attempts
Fail2Ban monitors logs and bans IPs that show malicious behavior—repeated login failures, scanner signatures, exploit attempts.
Install and enable:
apt install fail2ban
systemctl enable --now fail2ban
Create /etc/fail2ban/jail.local:
[DEFAULT]
bantime = 3600
findtime = 600
maxretry = 5
[sshd]
enabled = true
port = 2289
logpath = /var/log/auth.log
Restart Fail2Ban:
systemctl restart fail2ban
Check banned IPs:
fail2ban-client status sshd
Fail2Ban won't stop sophisticated attackers, but it cuts down noise and catches the lazy automation.
7. Harden Kernel Parameters with sysctl
The kernel has dozens of network and security knobs. A few matter for general hardening.
Edit /etc/sysctl.conf or create /etc/sysctl.d/99-security.conf:
# Prevent IP spoofing
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1
# Ignore ICMP redirects
net.ipv4.conf.all.accept_redirects = 0
net.ipv6.conf.all.accept_redirects = 0
# Ignore send redirects
net.ipv4.conf.all.send_redirects = 0
# Disable source packet routing
net.ipv4.conf.all.accept_source_route = 0
net.ipv6.conf.all.accept_source_route = 0
# Log suspicious packets
net.ipv4.conf.all.log_martians = 1
# Protect against SYN flood attacks
net.ipv4.tcp_syncookies = 1
Apply changes:
sysctl -p
These settings close network-layer weaknesses that most admins never think about.
So what if the firewall and SSH hardening are done but you're still flying blind?
8. Set Up Centralized Logging and Monitoring
You can't detect compromise if you're not watching. Logs tell you when someone tried a password spray, when a service crashed, when disk space filled up.
For small setups, configure rsyslog to forward logs to a separate server or use a lightweight agent. For production clusters, use the ELK stack or a managed service.
At minimum, enable auditd to log system calls:
apt install auditd
systemctl enable --now auditd
Add rules in /etc/audit/rules.d/audit.rules:
-w /etc/passwd -p wa -k identity
-w /etc/shadow -p wa -k identity
-w /etc/sudoers -p wa -k actions
-w /var/log/auth.log -p wa -k auth
Reload rules:
auditctl -R /etc/audit/rules.d/audit.rules
Search audit logs:
ausearch -k identity
Pair logging with uptime monitoring. A simple cron job that pings a dead-man switch URL or a Prometheus exporter both work. If the server goes dark, you need to know within minutes, not hours.
9. Limit User Privileges and Use sudo Sparingly
Don't run application services as root. Don't give every user sudo access. Least privilege isn't paranoia; it's damage control when something does go wrong.
Create service users with no login shell:
useradd -r -s /usr/sbin/nologin nginx
Run your web server, database, and app under dedicated accounts. If Nginx gets compromised, the attacker lands in a limited shell with no sudo.
For sudo, use /etc/sudoers.d/ to grant specific commands instead of blanket ALL=(ALL:ALL) ALL:
deploy ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart nginx
Edit with visudo to catch syntax errors.
I've seen incidents where a developer account with full sudo had credentials leaked in a GitHub commit. Limiting privilege would have stopped lateral movement.
Daily Security Checklist for Production Servers
Once hardening is done, routine checks keep you ahead of drift:
- Check for failed login attempts:
grep "Failed password" /var/log/auth.log | tail -20 - Review sudo usage:
grep sudo /var/log/auth.log | tail -20 - Confirm firewall is active:
ufw statusorfirewall-cmd --state - Check disk space:
df -h(full disks hide logs and cause silent failures) - Verify updates:
apt list --upgradableordnf check-update - Review running processes:
ps aux | grep -v "\[" | head -20
Script these checks or pipe them into a monitoring dashboard.
What About Web Application Firewalls and Intrusion Detection?
A WAF like ModSecurity or a cloud-based solution adds a layer between attackers and your application. It's worth setting up if you run public web services, but it's not a substitute for the foundational controls above.
IDS tools like OSSEC or Wazuh watch for anomalies and known attack signatures. They generate noise until you tune the rules, and they require time to operate. Start with the nine steps here first. Add IDS once you've closed the obvious gaps.
Where to Start If You're Already Running Servers
If you manage live infrastructure and haven't implemented these controls, start with SSH hardening and firewall rules today. Those two changes block the majority of opportunistic attacks.
Schedule the rest over the next two weeks: automated updates, Fail2Ban, kernel tuning, logging, and privilege separation. Test each step in a staging environment first if you have one, but don't let perfect block progress. A partially hardened server is better than an open one.
The attack patterns evolving in production environments don't wait for perfect configurations. Apply these nine steps, monitor what changes, and iterate.
![Server Security Best Practices: 9 Steps to Lock Down [2026]](/images/blog/server-security-best-practices-9-steps-to-lock-down-2026-2.jpg)