Skip to content
security By Peter Mastras 4 June 2026 8 min read

Linux server hardening: a practical checklist for production

A working checklist for hardening a Linux server in production: SSH, firewall, fail2ban, automatic updates, audit logging and more. Built from real environments.

Most Linux servers that get compromised weren’t broken into through a zero-day. They had SSH open to the world with password authentication, a root account you could log into directly, outdated packages with known CVEs, or services running that nobody needed. This checklist covers the practical hardening steps I apply to every production Linux server before it goes live.

These steps apply to Debian and Ubuntu. Most translate directly to RHEL-based distributions with minor command differences.

SSH: the first and most important surface

If your server has a public IP, port 22 is being scanned constantly. The goal is to make SSH unusable for attackers without a key.

Disable password authentication. Edit /etc/ssh/sshd_config:

PasswordAuthentication no
ChallengeResponseAuthentication no
KbdInteractiveAuthentication no

Before doing this, make sure your SSH key is in ~/.ssh/authorized_keys and you can log in with it. Locking yourself out of a cloud server is a bad afternoon.

Disable root login. Direct root SSH is a single-factor compromise away from full control of the machine:

PermitRootLogin no

Use a regular user account and sudo for privileged operations. This adds one layer of separation and means every privileged action shows up under a named user in logs.

Restrict which users can log in. If only two accounts ever need SSH access, say so:

AllowUsers deploy peter

Anything not in that list cannot authenticate, regardless of what keys they have.

Restart SSH after changes. Always test the new config before restarting:

sshd -t        # test config syntax
systemctl restart ssh

Keep your current session open and open a second session to confirm access before closing anything.

On port changing. Changing SSH to a non-standard port does reduce noise in logs, but it is security through obscurity. Anything listening on a public IP will be found by scanners within hours. Fix the real issues first. If you do change the port, update your firewall rules and any deployment scripts before restarting.

fail2ban: block brute-force attempts

fail2ban watches log files and temporarily bans IPs that trigger repeated failures. For SSH it is standard practice:

apt install fail2ban

Create /etc/fail2ban/jail.local with at minimum:

[sshd]
enabled = true
port = ssh
maxretry = 5
bantime = 3600
findtime = 600

This bans any IP that fails 5 times within 10 minutes for one hour. Adjust aggressively for internet-facing servers. I typically set bantime = 86400 on servers that don’t need access from broad IP ranges.

Check the status with fail2ban-client status sshd. You’ll see banned IPs and the hit count. On a freshly exposed server the numbers are sometimes sobering.

UFW firewall: default deny

Ubuntu ships with UFW, which wraps iptables into something manageable. The approach is default deny inbound, allow only what’s needed:

ufw default deny incoming
ufw default allow outgoing
ufw allow ssh
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable

If the server is a mail server, add the relevant SMTP/submission ports. Everything else stays closed.

Review your rules periodically. ufw status numbered shows the full ruleset. Old rules for services that no longer exist are common in long-running servers.

For servers that only need SSH access from a known IP range, restrict it:

ufw allow from 203.0.113.0/24 to any port 22

This eliminates SSH exposure entirely from outside that range.

Automatic security updates

Unpatched packages are the second most common way servers get owned, after weak credentials. On Debian/Ubuntu, unattended-upgrades handles security patches automatically:

apt install unattended-upgrades
dpkg-reconfigure --priority=low unattended-upgrades

Check /etc/apt/apt.conf.d/50unattended-upgrades to confirm the security origin is enabled. By default it is, but verify:

Unattended-Upgrade::Origins-Pattern {
    "origin=Debian,codename=${distro_codename}-security,label=Debian-Security";
    "origin=Ubuntu,codename=${distro_codename}-security,label=Ubuntu-Security";
};

Set automatic reboots if a kernel update requires it. For servers where uptime matters, schedule the reboot window:

Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "03:00";

This is a judgment call. Some environments need manual approval for reboots. At minimum, the security packages should apply automatically.

User and sudo hygiene

No shared accounts. Every person with access gets their own account. When someone leaves, you disable their account rather than changing a shared password.

Sudo logging. By default, sudo commands are logged to /var/log/auth.log. Make sure log rotation isn’t discarding these too quickly. On servers where compliance matters, increase the retention period in /etc/logrotate.conf.

Audit sudo group membership. On Ubuntu, members of the sudo group have full root access. Check who’s in it:

getent group sudo

Remove any accounts that shouldn’t be there. Developer accounts on a production server should use specific sudo rules for what they actually need, not blanket access.

Lock inactive accounts. passwd -l username locks an account without deleting it. usermod --expiredate 1 username expires it entirely.

Disable unused services

Every running service is an attack surface. Check what’s listening:

ss -tlnp

Compare this against what you expect. Web server, SSH, your application. Anything else needs a reason to be there.

Common things to disable on a server that doesn’t need them:

systemctl disable avahi-daemon
systemctl disable cups
systemctl disable bluetooth

avahi-daemon (mDNS) and cups (printing) are enabled by default on some distributions. Neither belongs on a production server.

Check services that start automatically:

systemctl list-unit-files --state=enabled

Disable anything unfamiliar that has no clear purpose.

SUID/SGID audit

SUID binaries run with the file owner’s permissions, regardless of who executes them. SGID does the same for group. These are necessary for some system utilities (passwd, ping, sudo itself), but unknown SUID binaries in unexpected locations are a red flag:

find / -perm -4000 -type f 2>/dev/null
find / -perm -2000 -type f 2>/dev/null

Save this output on first run and compare it periodically. New SUID binaries appearing on a running server without an explanation warrant investigation.

If you find SUID set on something that doesn’t need it:

chmod u-s /path/to/binary

Audit logging with auditd

auditd records system calls and file access at the kernel level. It is useful for compliance requirements and for forensics after an incident.

apt install auditd audispd-plugins

A basic ruleset covers the most important events. In /etc/audit/rules.d/audit.rules:

# Track authentication
-w /etc/passwd -p wa -k auth
-w /etc/shadow -p wa -k auth
-w /etc/sudoers -p wa -k privilege

# Track sudo usage
-a always,exit -F arch=b64 -S execve -F uid=0 -k root_commands

# Track SSH configuration changes
-w /etc/ssh/sshd_config -p wa -k sshd_config

After adding rules:

augenrules --load
systemctl restart auditd

Query logs with ausearch -k auth or ausearch -k privilege. On a healthy server these logs are quiet. Unexpected writes to /etc/passwd or /etc/sudoers at 3am are not quiet.

ClamAV in hosted environments

On dedicated servers and VPS environments that handle user-uploaded files, or where clients send files inbound, ClamAV provides a baseline for malware scanning. It is not a substitute for proper access controls but it catches known malware in uploads:

apt install clamav clamav-daemon
freshclam

The free ClamAV database is updated several times a day. Configure clamd to run as a daemon and integrate it with your application’s file handling, or run periodic scans on upload directories:

clamscan -r /var/www/uploads --log=/var/log/clamav/uploads.log

For environments without user-generated content, ClamAV adds overhead without much benefit. Apply judgment.

Putting it together

Running through this checklist on a new server takes about 45 minutes. On existing servers that have been running for a while without review, allow longer. The service audit and SUID check often surface things that need investigation.

The checks that matter most, in order: key-only SSH, disabled root login, fail2ban enabled, UFW default deny, and automatic security updates. Everything else reduces the attack surface further but those five cover the majority of real-world compromises.

For a broader view of your security posture, the Security Health Check covers network exposure, authentication configuration, and application-layer issues. The Website Security Scanner checks public-facing infrastructure for common misconfigurations.

Need help with your email infrastructure?

Talk to an engineer