Production servers sit in the crosshairs every day. Automated scanners hit SSH ports, bots hunt for weak credentials, and exploit toolkits test every open service. The question isn't whether someone will try to break in — it's whether your defenses hold when they do.
I've seen too many compromised servers that followed outdated guides or skipped foundational steps. This post walks through nine practical hardening measures that address the attack patterns I see in real support tickets. No fluff, just the configuration changes and checks that matter.
1. Disable Root SSH Login Immediately
Letting root log in over SSH is handing attackers half the credentials they need. Every brute-force script on the internet tries root first.
Edit your SSH daemon config:
sudo nano /etc/ssh/sshd_config
Find or add this line:
PermitRootLogin no
Restart SSH to apply:
sudo systemctl restart sshd
Before you do this, make sure you have a working sudo user account and can authenticate with it. Test the sudo user's login in a second terminal session before closing your root session. I've locked myself out by skipping that check.
Once root login is off, attackers need to guess both a valid username and its password. That's a much steeper hill.
2. Move SSH to a Non-Standard Port
Port 22 gets hammered by every script kiddie's scanner. Moving SSH to a high port (above 1024, below 65535) cuts automated noise by ninety percent or more.
In /etc/ssh/sshd_config, change:
Port 22
to something like:
Port 49222
Restart SSH, then update your firewall rules to allow the new port and block 22. If you're behind a hardware firewall or security group, update those rules too.
Log in using the new port to confirm:
ssh -p 49222 [email protected]
This isn't security by obscurity if you layer it with real hardening. It's reducing attack surface by filtering out the low-effort scans that eat log space and CPU cycles.
3. Enforce SSH Key Authentication Only
Passwords can be brute-forced. Keys can't, not in any reasonable timeframe.
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 -p 49222 [email protected]
Test key-based login, then disable password authentication in /etc/ssh/sshd_config:
PasswordAuthentication no
ChallengeResponseAuthentication no
Restart SSH. From this point forward, only clients with the matching private key can authenticate. Store that private key securely — if you lose it, you'll need console access to recover.
4. Configure a Firewall and Default-Deny Everything
An open server is an invitation. Close every port except the ones you actually need.
On most modern Linux distributions, ufw (Uncomplicated Firewall) handles this cleanly:
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 49222/tcp # your SSH port
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
Check the active rules:
sudo ufw status verbose
If you're running a mail server, database, or other services, open only the specific ports those services require and restrict them by source IP when possible. Every open port is a potential entry point.
For more granular control, use iptables directly or a configuration management tool that templates firewall rules. The principle is the same: deny by default, allow explicitly.
5. Keep the System Patched
Unpatched systems run known vulnerabilities. Exploit code for those vulnerabilities is often public and trivial to use.
Enable automatic security updates on Debian-based systems:
sudo apt install unattended-upgrades
sudo dpkg-reconfigure --priority=low unattended-upgrades
On RHEL-based systems, configure dnf-automatic:
sudo dnf install dnf-automatic
sudo systemctl enable --now dnf-automatic.timer
Check /etc/dnf/automatic.conf and set apply_updates = yes under the [commands] section if you want full automation.
Automatic updates do carry a small risk of breaking something, especially on complex application stacks. Weigh that against the risk of running weeks behind on kernel and library patches. In production, I lean toward automation for security updates and manual control for major version upgrades.
6. Harden Sudo and Limit Privilege Escalation
Not every user needs full sudo. Not every sudo command should run without a password prompt.
Edit the sudoers file safely:
sudo visudo
For users who need limited sudo, grant specific commands instead of ALL:
username ALL=(ALL) /usr/bin/systemctl restart nginx, /usr/bin/systemctl status nginx
Disable password-less sudo for most users. If you see a line like:
%wheel ALL=(ALL) NOPASSWD: ALL
replace it with:
%wheel ALL=(ALL) ALL
Requiring a password doesn't stop a determined attacker who's already in, but it does slow down automated post-exploit scripts and adds a layer of intent.
Also review /etc/security/access.conf and PAM configurations to restrict who can even attempt su or sudo.
7. Disable Unused Services and Remove Unnecessary Packages
Every running service is a potential attack vector. Every installed package is potential exploit surface.
List active services:
sudo systemctl list-units --type=service --state=running
Disable anything you don't recognize or don't need:
sudo systemctl disable --now servicename
Remove packages you're not using:
sudo apt autoremove
sudo apt purge package-name
I've seen servers running Exim, Postfix, and sendmail simultaneously because no one cleaned up after switching mail transfer agents. Each one was listening, each one had configuration files, and each one could have been an entry point.
Minimalism isn't just philosophically satisfying. It's practical risk reduction.
So what about logging and intrusion detection?
Hardening your server is half the job. Knowing when someone's probing it is the other half.
8. Set Up Fail2Ban to Block Brute-Force Attempts
fail2ban watches your logs and bans IPs that show malicious patterns — repeated login failures, vulnerability scans, exploit attempts.
Install it:
sudo apt install fail2ban
Copy the default config so your changes survive updates:
sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
Edit /etc/fail2ban/jail.local and enable the SSH jail:
[sshd]
enabled = true
port = 49222
maxretry = 3
bantime = 3600
Restart fail2ban:
sudo systemctl restart fail2ban
Check banned IPs:
sudo fail2ban-client status sshd
You can add jails for other services — Apache, Nginx, Postfix, anything that logs authentication attempts or suspicious requests. In support tickets I handled, the usual culprit for lockouts was admins forgetting their own password and tripping the ban. Whitelist your own management IPs in the config to avoid that.
9. Enable and Monitor Auditd for File Integrity
auditd records system calls and file access. It's the black box recorder for your server.
Install the audit daemon:
sudo apt install auditd
Add a rule to watch sensitive files, like /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
Make those rules persistent by adding them to /etc/audit/rules.d/audit.rules.
Search the audit log for triggered watches:
sudo ausearch -k passwd_changes
Audit logs grow fast. Rotate them and ship them to a centralized log server if you're managing multiple systems. If someone compromises your server, the first thing they'll do is tamper with local logs. Off-box logging preserves the evidence.
What to Check First
Hardening a server isn't a one-time task. It's a baseline. Come back to this list every few months and verify the settings are still active — services drift, updates revert configs, and new team members sometimes undo changes they don't understand.
Start with SSH and firewall. Those two steps block the majority of opportunistic attacks. Then layer in key-based auth, patching, and logging. The goal is defense in depth: multiple barriers so that compromising one control doesn't hand over the whole system.
Your server's uptime depends on it.
![Server Security Best Practices: 9 Steps to Lock Down [2026]](/images/blog/server-security-best-practices-9-steps-to-lock-down-2026.jpg)