Most server compromises I've seen in support tickets trace back to the same handful of weak points: default SSH settings, missing firewall rules, outdated packages, or users running everything as root. The attack surface hasn't changed much—attackers still scan for open ports, try common passwords, and exploit unpatched services. What has changed is the speed and scale of automated scans hitting every public IP within hours of provisioning.
This guide covers the hardening steps that actually stop those common attacks. We'll walk through SSH configuration, firewall rules, user permissions, package updates, and a few extras that close the gaps most admins overlook.
Lock down SSH first
SSH is your front door. It's also the first thing bots try when they find your server.
Disable root login immediately. Edit /etc/ssh/sshd_config and set:
PermitRootLogin no
Create a non-root user with sudo privileges instead. That way you can still do admin work, but attackers can't brute-force the root account directly. Add the user to the sudo or wheel group depending on your distro:
useradd -m -G sudo deployuser
passwd deployuser
Switch to SSH key authentication and disable password login entirely. Generate a 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 deployuser@your_server_ip
Then turn off password authentication in sshd_config:
PasswordAuthentication no
ChallengeResponseAuthentication no
Change the default SSH port from 22 to something higher—say, 2222 or 49222. Yes, security-through-obscurity isn't a substitute for real hardening, but it does cut down log noise from script-kiddie scans by 90%. Update the Port line:
Port 2222
Restart SSH after every config change:
systemctl restart sshd
Before you close your current session, open a second SSH connection in a new terminal to confirm the new settings work. That way if you locked yourself out, you can still fix it.
Configure a firewall (and keep it simple)
An unconfigured firewall is as bad as no firewall. I've logged into fresh VPS instances that had every port wide open to the internet.
Use ufw on Ubuntu/Debian or firewalld on RHEL-based systems. Both are front-ends that make iptables rules easier to manage. Here's a basic ufw setup:
ufw default deny incoming
ufw default allow outgoing
ufw allow 2222/tcp # your custom SSH port
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable
That blocks everything inbound except SSH, HTTP, and HTTPS. If you run a mail server, database, or other services, open only the ports those services need and restrict them to specific IPs when possible:
ufw allow from 203.0.113.50 to any port 3306 # MySQL from your app server
Check active rules:
ufw status numbered
For firewalld, the equivalent commands look like this:
firewall-cmd --permanent --remove-service=ssh
firewall-cmd --permanent --add-port=2222/tcp
firewall-cmd --permanent --add-service=http
firewall-cmd --permanent --add-service=https
firewall-cmd --reload
Don't leave FTP (port 21), Telnet (23), or MySQL (3306) open to the world. FTP and Telnet should be replaced entirely; MySQL should only listen on localhost or a private network.
Keep software updated (automate it)
Unpatched software is the easiest entry point. Attackers know which CVEs exist and they scan for vulnerable versions constantly.
Enable automatic security updates on Ubuntu/Debian:
apt install unattended-upgrades
dpkg-reconfigure -plow unattended-upgrades
Edit /etc/apt/apt.conf.d/50unattended-upgrades to make sure security updates are enabled:
Unattended-Upgrade::Allowed-Origins {
"${distro_id}:${distro_codename}-security";
};
On RHEL/CentOS/AlmaLinux, use dnf-automatic:
dnf install dnf-automatic
systemctl enable --now dnf-automatic.timer
Configure /etc/dnf/automatic.conf to apply updates automatically:
apply_updates = yes
Check for available updates manually every few weeks anyway, especially for the kernel and critical services like OpenSSH or Apache. Automatic updates handle most things, but some packages need a reboot or service restart to take effect.
Restrict user permissions and use sudo properly
Running applications as root gives an attacker full system access the moment they exploit that app. Create dedicated service accounts with minimal permissions for each service.
For example, if you run a Node.js app, create a user that can only write to the app directory:
useradd -r -s /bin/false nodeapp
chown -R nodeapp:nodeapp /var/www/myapp
The -r flag makes it a system account (no home directory), and -s /bin/false prevents interactive login.
When you do need root privileges, use sudo and log everything. Edit /etc/sudoers with visudo and add:
Defaults logfile="/var/log/sudo.log"
Review that log periodically for unexpected commands. Limit sudo access to specific commands if a user only needs to restart a service:
deployuser ALL=(ALL) NOPASSWD: /bin/systemctl restart myapp
Never add NOPASSWD: ALL for any user—it defeats the purpose of separating root from regular accounts.
Disable unused services and remove unnecessary packages
Every running service is another potential entry point. List active services:
systemctl list-units --type=service --state=running
Disable anything you don't recognize or don't use:
systemctl disable --now cups.service # print server on a web host?
Common candidates for removal: Avahi (zeroconf), rpcbind (NFS-related), bluetooth, and any desktop packages on a headless server.
Remove packages you're not using:
apt autoremove
apt purge <package-name>
The smaller your installed package list, the smaller your attack surface.
Harden kernel parameters with sysctl
A few kernel tweaks can block network-level attacks like SYN floods and IP spoofing. Edit /etc/sysctl.conf or create a file in /etc/sysctl.d/:
# Prevent SYN flood attacks
net.ipv4.tcp_syncookies = 1
# Ignore ICMP redirects
net.ipv4.conf.all.accept_redirects = 0
net.ipv6.conf.all.accept_redirects = 0
# Ignore source-routed packets
net.ipv4.conf.all.accept_source_route = 0
# Enable reverse path filtering
net.ipv4.conf.all.rp_filter = 1
# Log martian packets
net.ipv4.conf.all.log_martians = 1
Apply the changes:
sysctl -p
These settings won't stop application-layer attacks, but they do reduce your exposure to low-level network abuse.
Set up Fail2Ban to block brute-force attempts
Even with SSH keys and a non-standard port, bots will try to connect. Fail2Ban watches your logs and bans IPs that repeatedly fail authentication.
Install it:
apt install fail2ban
Create a local config file so updates don't overwrite your settings:
cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
Edit /etc/fail2ban/jail.local and enable the SSH jail:
[sshd]
enabled = true
port = 2222
maxretry = 3
bantime = 3600
Restart Fail2Ban:
systemctl restart fail2ban
Check banned IPs:
fail2ban-client status sshd
You can add jails for other services too—Apache, Nginx, Postfix—but SSH is the highest-value target.
Enable and review logging
Logs are how you detect attacks in progress and trace what happened after a breach. Make sure logging is enabled for SSH, firewall events, and authentication attempts.
Key log files:
/var/log/auth.log(Debian/Ubuntu) or/var/log/secure(RHEL) for SSH and sudo/var/log/ufw.logor/var/log/firewalldfor firewall denials/var/log/fail2ban.logfor ban actions
Set up a simple daily check. Grep for failed login attempts:
grep "Failed password" /var/log/auth.log | tail -20
Or count authentication failures by IP:
grep "Failed password" /var/log/auth.log | awk '{print $(NF-3)}' | sort | uniq -c | sort -nr
For centralized logging across multiple servers, consider shipping logs to a syslog server or using a service like Papertrail, but even local logs catch 90% of issues if you actually look at them.
Use a configuration management tool or a checklist
Hardening one server manually is manageable. Hardening ten consistently is not.
If you manage more than a handful of servers, use Ansible, Puppet, or even a shell script to apply the same baseline config everywhere. That way you don't forget to disable root login on server number seven or skip Fail2Ban on a staging box that later goes live.
An Ansible playbook for SSH hardening might look like:
- name: Harden SSH
hosts: all
become: yes
tasks:
- name: Disable root login
lineinfile:
path: /etc/ssh/sshd_config
regexp: '^PermitRootLogin'
line: 'PermitRootLogin no'
notify: restart ssh
- name: Disable password authentication
lineinfile:
path: /etc/ssh/sshd_config
regexp: '^PasswordAuthentication'
line: 'PasswordAuthentication no'
notify: restart ssh
handlers:
- name: restart ssh
service:
name: sshd
state: restarted
If you're not ready for automation, keep a hardening checklist in a README and tick it off for each new server.
What about intrusion detection?
Fail2Ban handles reactive bans based on log patterns, but it won't catch a zero-day or an attacker who already has valid credentials. For deeper monitoring, install an intrusion detection system like AIDE (Advanced Intrusion Detection Environment).
AIDE creates a database of file checksums and alerts you when system files change unexpectedly:
apt install aide
aideinit
mv /var/lib/aide/aide.db.new /var/lib/aide/aide.db
Run checks periodically:
aide --check
You can also use rkhunter or chkrootkit to scan for known rootkits, though false positives are common. Schedule weekly scans and review the output:
rkhunter --check --sk
These tools aren't foolproof—an advanced attacker can hide from them—but they catch careless compromises and give you a heads-up when something weird happens.
Secure your applications too
Server hardening only protects the OS layer. If your web app has an SQL injection vulnerability or your WordPress install is three years out of date, all the firewall rules in the world won't help.
Keep application dependencies updated. For PHP apps, run composer update regularly. For Node.js, npm audit fix. For Python, pin versions in requirements.txt and review them.
Use a web application firewall like ModSecurity (for Apache/Nginx) to filter malicious requests at the application layer. Enable it with the OWASP Core Rule Set:
apt install libapache2-mod-security2
cp /etc/modsecurity/modsecurity.conf-recommended /etc/modsecurity/modsecurity.conf
Set SecRuleEngine to On in the config, then restart Apache.
For WordPress specifically, keep core, themes, and plugins updated; remove unused plugins; and use a security plugin like Wordfence or Sucuri to monitor file changes and block brute-force login attempts.
Where to start today
If you're staring at a fresh server or an old one you've been meaning to harden, tackle these in order:
- Disable root SSH login and set up key authentication.
- Configure a firewall with only the ports you need.
- Enable automatic security updates.
- Install and configure Fail2Ban.
- Review running services and disable the ones you don't use.
That covers the highest-impact steps and blocks the vast majority of opportunistic attacks. Everything else—kernel hardening, intrusion detection, application firewalls—builds on that foundation. You don't need to do it all at once, but you do need to do the basics before you put a server on the public internet.
![Server Security Best Practices [Solved] for 2026](/images/blog/server-security-best-practices-solved-for-2026.jpg)