A freshly provisioned Linux server is not a secure one — it's a default one, tuned for compatibility and ease of setup rather than for standing exposed on the internet. Hardening is the deliberate process of closing that gap: reducing what's running, restricting who can reach it and what they can do once they're in, and making sure that if something does go wrong, there's a record of it. This guide walks through that process in the order it actually matters, with the reasoning behind each step, not just the command to run.
This guide is written for a traditional Linux server you administer directly — bare metal or a VM, reachable over SSH, running for months or years rather than minutes. Container images and managed cloud services share some of the same underlying concerns but come with their own separate hardening practices this guide doesn't attempt to cover.
The philosophy: attack surface and defense in depth
Two ideas do most of the work here. Attack surface is everything on a system that could potentially be exploited — every running service, every open port, every installed package, every account that can log in. Hardening is largely the practice of shrinking that surface: uninstalling what you don't use, closing ports nothing listens on, removing accounts nobody needs. Defense in depth is the complementary idea that no single control should be the only thing standing between an attacker and the system — a firewall, a security module, careful permissions, and logging each catch different failure modes, so one misconfiguration doesn't mean total exposure.
Neither idea is about achieving some final, permanently "hardened" state. It's an ongoing posture: new packages get installed, new accounts get added, and without periodic review a hardened server drifts back toward a default one, one convenient exception at a time.
Before you touch anything: don't lock yourself out
The single most common self-inflicted incident in server hardening is losing access to your own machine — disabling password authentication before confirming a key works, enabling a firewall before allowing SSH through it, or restarting sshd with a typo in the config. Two habits prevent almost all of this:
- Keep a second session open while testing any SSH or firewall change. If the new setting breaks something, you still have a working connection to fix it from.
- Have an out-of-band access path — a cloud provider's web console, IPMI, or physical access — for the rare case where both sessions get cut off anyway.
Worth adding a third habit if the server holds anything you can't easily rebuild: take a snapshot or backup before a hardening pass, not just before a risky one. Most of these changes are individually reversible, but reverting five compounding changes made over an hour is slower and more error-prone than restoring a snapshot taken before you started.
With that safety net in place, the actual work goes in five areas: updates, SSH, the firewall, users and permissions, and logging.
1. System updates and patching
This is foundational, not one item among equals — hardening reduces what an attacker can do if they get in and closes off some entry points, but an actively exploited, unpatched vulnerability in exposed software routes around most of it. Start every hardening pass with a full update, and set up automatic security updates so the next one doesn't depend on someone remembering:
# Debian/Ubuntu
sudo apt update && sudo apt full-upgrade -y
sudo apt install -y unattended-upgrades
sudo dpkg-reconfigure --priority=low unattended-upgrades
# Fedora/RHEL
sudo dnf upgrade --refresh -y
sudo dnf install -y dnf-automatic
sudo systemctl enable --now dnf-automatic.timerAlongside updates, remove what you don't use. Every installed package and running service is part of the attack surface whether or not it's ever touched again — a default install often includes software a given server's actual role never needs. Finally, know your distribution's support lifecycle: running a release past its end-of-life date means security patches simply stop arriving, regardless of how well everything else here is configured.
2. SSH hardening
SSH is usually the only remote administrative access point on a server, which makes it both essential and the single highest-value target to harden carefully. The core changes, in /etc/ssh/sshd_config:
PermitRootLogin no
PasswordAuthentication no
MaxAuthTries 3
AllowUsers deploy alice
ClientAliveInterval 300
ClientAliveCountMax 2PermitRootLogin no forces anyone administering the box to authenticate as a named user first, then escalate via sudo — so every privileged action is tied to an identifiable account rather than an anonymous "root did it." PasswordAuthentication no is the single highest-impact change available: it eliminates brute-force password guessing as an attack path entirely, since there's no password to guess. This requires a working key pair first — see SSH Key Generator if you don't have one, and SSH Keys Explained for how key authentication actually works under the hood.
MaxAuthTries limits how many authentication attempts a single connection gets before being dropped, and AllowUsers restricts login to specific named accounts — useful on a server where, say, a database service account exists in /etc/passwd but should never be an SSH login target at all. The ClientAlive* settings close idle sessions automatically, so a forgotten terminal left open on a shared machine doesn't stay authenticated indefinitely.
After editing the file, restart the daemon and verify the change actually took effect before disconnecting your current session:
sudo systemctl restart sshd
sudo sshd -T | grep -iE 'permitrootlogin|passwordauthentication'Changing the default port (22) is worth a brief note since it comes up constantly: it's real but minor. It reduces the sheer volume of automated scanner noise hitting your logs, and does nothing against a targeted attacker who simply scans the host for open ports. Treat it as a convenience, not a security control. For managing all of this — and pairing it with per-host settings like ProxyJump and IdentityFile — see SSH Config Generator and the SSH Config File Guide. For an additional layer beyond keys, PAM modules like Google Authenticator add a second factor to SSH logins — worth considering for anything genuinely high-value, though it adds real operational complexity most small setups don't need.
Minimizing what's actually running
A firewall controls what can reach a service from the network; it does nothing about services that shouldn't be running in the first place. The two questions worth asking on any server are "what's currently running" and "what's currently listening" — they're related but not identical, since a running service might only listen on a local socket nothing external can reach anyway.
systemctl list-units --type=service --state=running
ss -tulnpsystemctl list-units shows every active service; cross-reference that against what the server's actual role requires and disable anything left over from the default install that isn't needed:
sudo systemctl disable --now <unneeded-service>ss -tulnp (or the older netstat -tulnp) shows every port actually listening for connections and which process owns it — the ground truth for what a firewall needs to cover, rather than guessing from a service list alone. A service that only needs to talk to other processes on the same machine should bind to 127.0.0.1 rather than 0.0.0.0 wherever its configuration allows it, so it's unreachable from the network layer entirely regardless of what the firewall does or doesn't allow. If the server doesn't use IPv6 at all, either firewall it with the same care as IPv4 or disable it outright — a common gap is hardening the IPv4 firewall thoroughly while leaving IPv6 wide open on a dual-stack host, effectively leaving a second, unguarded front door.
fail2ban deserves a specific mention alongside SSH rather than only in the logging section, since its default configuration already watches sshd out of the box on most distros. A minimal local override confirms it and tunes the response:
# /etc/fail2ban/jail.local
[sshd]
enabled = true
maxretry = 5
bantime = 3600That bans an offending address for an hour after five failed attempts — adjust bantime upward for a server that sees persistent scanning traffic. This is exactly the kind of automated, always-on response that a one-time hardening pass can't provide by itself: it keeps reacting to new attempts long after you've moved on to other work.
3. Firewall: default-deny, then allow what's needed
The firewall's job is simple to state and easy to get backwards in practice: deny everything incoming by default, then explicitly allow only what this specific server needs to expose. Ubuntu and Debian ship UFW, a friendly frontend over the kernel's packet filter; Fedora and RHEL use firewalld instead. Both do the same job with different syntax:
# UFW (Ubuntu/Debian)
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH
sudo ufw enable
# firewalld (Fedora/RHEL)
sudo systemctl enable --now firewalld
sudo firewall-cmd --permanent --add-service=ssh
sudo firewall-cmd --set-default-zone=public
sudo firewall-cmd --reloadNotice the order: SSH is allowed before the firewall is enabled or reloaded with its new default-deny policy. Getting this backwards — enabling the firewall first with no SSH rule in place — cuts off remote access to the machine immediately, often with no way to fix it except console access. Beyond SSH, allow only the specific ports the server's actual services need — a web server needs 80/443, a database server generally needs nothing open to the public internet at all, only to specific application servers on a private network. And revisit the rule set periodically: firewall rules for a service that was decommissioned six months ago don't remove themselves.
4. User and permissions review
This is where the principle of least privilege gets applied to accounts and files: every account should have exactly the access it needs to do its job, no more. Concretely:
- Lock the root account's password (
sudo passwd -l root) so it can't be used for direct login even ifPermitRootLoginwere ever accidentally re-enabled — administration happens through named accounts with sudo, never a shared root login. - One account per human, never a shared login for a team. Shared accounts make it impossible to know who actually did what during an incident review.
- Enforce password aging for any account that still authenticates with a password (
chage -M 90 -W 7 username— expire after 90 days, warn 7 days ahead). - Enforce minimum password complexity at the PAM layer, so a weak password is rejected at creation time rather than relying on people to choose well voluntarily — the
pam_pwqualitymodule (configured in/etc/security/pwquality.conf) is available on all four distros and can require a minimum length and a mix of character classes:minlen = 12 minclass = 3 - Audit for world-writable files and unexpected SUID/SGID binaries — both are common privilege-escalation vectors when they exist somewhere they shouldn't:
find / -xdev -type f -perm -0002 -not -type l 2>/dev/null
find / -xdev \( -perm -4000 -o -perm -2000 \) -type f 2>/dev/nullReview what these turn up rather than reflexively stripping every result — some SUID binaries (passwd itself, notably) are supposed to have that bit set to function at all. The point is knowing what's there, not zeroing it out blindly. Service accounts that run a specific daemon and never need an interactive shell should be created with one that refuses login (/usr/sbin/nologin) rather than a normal shell they'll never legitimately use. For working out or setting specific permission bits by hand, see Linux Permissions Calculator and chmod Command Generator.
The sudoers file deserves its own care, since a mistake there can be as costly as a mistake in SSH config. Always edit it with visudo, never a plain text editor directly — it validates the syntax before saving, so a typo can't silently leave the file unparsable the next time anyone tries to use sudo at all. Avoid blanket NOPASSWD: ALL entries; if a specific automated task genuinely needs passwordless sudo, scope the exception to that one command rather than granting it unrestricted:
# Narrow: only this one command, passwordless
deploy ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart myapp
# Avoid: unrestricted passwordless root
deploy ALL=(ALL) NOPASSWD: ALLMost distros also log every sudo invocation to /var/log/auth.log (Debian, Ubuntu) or /var/log/secure (Fedora, RHEL) by default — worth confirming rather than assuming, since that log is exactly what answers "who ran what as root, and when" during an incident review.
5. Logging and auditing
Hardening reduces the chance of compromise; logging is how you find out one happened anyway. The two aren't substitutes for each other. At minimum:
# Debian/Ubuntu
sudo apt install -y auditd fail2ban
# Fedora/RHEL
sudo dnf install -y audit fail2ban
sudo systemctl enable --now auditd
sudo systemctl enable --now fail2banauditd (packaged as audit on Fedora/RHEL) provides fine-grained tracking of security-relevant events — file access, privilege escalation, authentication attempts — well beyond what default system logs capture. fail2ban watches authentication logs and automatically firewalls off source addresses after repeated failures, turning a slow brute-force attempt into a quickly-blocked one. Beyond these two:
- Ship logs off the host when you can — a remote syslog target or a managed logging service means an attacker who fully compromises the machine can't also silently delete the evidence by deleting local log files.
- Set a log rotation policy so logs don't silently fill the disk — most distros configure
logrotatereasonably out of the box, but it's worth confirming for any service that logs heavily. - Actually review logs on a schedule. Collecting logs nobody looks at catches nothing; even a lightweight weekly skim of authentication failures and
sudousage catches problems a purely automated system might miss.
Finally, confirm the distribution's mandatory access control layer is actually enforcing, not merely installed — sudo aa-status on Ubuntu/Debian (AppArmor), sestatus on Fedora/RHEL (SELinux). Both confine what a process is allowed to do at the kernel level, independent of standard file permissions — genuinely useful defense in depth specifically for the scenario where a network-facing service gets compromised despite everything else on this list.
Beyond the checklist: additional layers worth knowing about
The five areas above cover the fundamentals every server benefits from. A few additional tools are worth knowing exist, even if they're not universally necessary:
- Kernel network hardening via sysctl — settings like
net.ipv4.tcp_syncookies=1(SYN flood protection),net.ipv4.conf.all.accept_redirects=0(refuse to have routes silently altered by an untrusted ICMP redirect),net.ipv4.conf.all.rp_filter=1(reject packets whose source address couldn't plausibly have arrived on the interface it came in on), andkernel.randomize_va_space=2(address space layout randomization, generally already the default on modern distros) tighten kernel-level network behavior beyond what the firewall alone controls. Settings like these go in/etc/sysctl.d/99-hardening.conf, applied withsudo sysctl --system. - AIDE (Advanced Intrusion Detection Environment) takes a cryptographic snapshot of important system files — binaries, config files, anything you tell it to watch — and later flags any that changed. That's useful for detecting the kind of quiet file tampering that wouldn't necessarily show up in a log at all, at the cost of needing an initial baseline taken on a system you already trust hasn't been compromised.
- Lynis is a security auditing tool that scans a running system end to end and reports a long list of specific hardening suggestions tailored to what it actually finds installed, rather than a generic list — a good way to catch anything a checklist like this one, written before it ever saw your specific server, wouldn't know to mention.
None of these are a substitute for the five fundamentals above; they're what to reach for once those are solidly in place and you want additional depth.
Verifying the work actually took effect
Every change above is worth confirming immediately, rather than assuming a command that ran without an error message did what it was supposed to:
sudo sshd -T | grep -iE 'permitrootlogin|passwordauthentication' # SSH settings actually applied
sudo ufw status verbose # or: sudo firewall-cmd --list-all
ss -tulnp # only the ports you expect are listening
sudo -l -U <username> # what a given account can actually sudo
sudo auditctl -l # active audit rulesIf you have access to a second machine, an external port scan (nmap -Pn your-server-ip) shows what the outside world actually sees — the most honest check of all, since it reflects the real network path rather than the configuration you intended to apply. A firewall rule with a typo, or a cloud provider's separate network-level security group silently overriding what you configured on the host itself, both show up immediately in an external scan and easily slip past every other check on this list.
Distribution differences, summarized
The underlying goals are identical across every major distribution; only the specific tools and package names differ:
- Package manager:
apt(Ubuntu, Debian) vs.dnf(Fedora, RHEL — RHEL also still acceptsyumas an alias). - Firewall: UFW (Ubuntu, Debian) vs. firewalld (Fedora, RHEL).
- Mandatory access control: AppArmor (Ubuntu; Debian supports it too, less uniformly pre-enabled) vs. SELinux, enforcing by default (Fedora, RHEL).
- Audit package name:
auditd(Ubuntu, Debian) vs.audit(Fedora, RHEL) — same underlying daemon, different package name. - Automatic updates:
unattended-upgrades(Ubuntu, Debian) vs.dnf-automatic(Fedora, RHEL).
The Linux Hardening Checklist Generator produces the exact commands for whichever of the four you're running, as a single downloadable Markdown file.
Common mistakes
- Disabling password authentication before confirming key-based login works. Always test the key in a second session before closing the one you know still works.
- Enabling the firewall before allowing SSH through it. Covered above, and worth repeating: this is the single most common way people lock themselves out.
- Treating hardening as a one-time task. New accounts, new packages, and new firewall rules accumulate continuously — schedule a periodic review rather than hardening once at provisioning and never revisiting it.
- Skipping updates because hardening feels more interesting. An unpatched, actively exploited vulnerability bypasses nearly everything else on this list.
FAQ
Where should I actually start if I've never hardened a server before?
System updates first, then SSH, then the firewall — in that order, and never disable password authentication over SSH until you've confirmed key-based login works in a second, still-open session. Everything else (permissions review, logging, auditing) matters, but those three prevent the most common real-world compromises.
Is changing the default SSH port actually worth doing?
It reduces log noise from automated scanners hammering port 22, and nothing more — it doesn't stop a targeted attacker, who will simply port-scan the host. Treat it as a minor convenience, never as a substitute for key-based authentication and a real firewall policy.
Do I need both a security module (SELinux/AppArmor) and a firewall?
Yes — they defend against different things. A firewall controls what can reach the machine over the network; a security module confines what an already-running process is allowed to do on the machine itself, which matters specifically when a network-facing service gets compromised despite the firewall.
How often should this checklist actually be revisited?
At minimum whenever the server's role changes (a new service gets installed, a new admin gets added) and on a recurring schedule regardless — quarterly is a reasonable default for a small team. Firewall rules and user accounts in particular tend to accumulate cruft from changes nobody remembered to clean up.
Is a hardening checklist a substitute for keeping software updated?
No — hardening reduces what an attacker can do if they get in and closes off some ways in, but an unpatched, actively exploited vulnerability in exposed software bypasses most of it. Patching is foundational, not one checklist item among equals.
What's the difference between hardening and monitoring?
Hardening reduces the chance of a successful compromise in the first place (fewer open doors, less privilege available if one is used). Monitoring — logging, auditing, intrusion detection — is how you find out a compromise happened anyway despite hardening. A server needs both; neither substitutes for the other.
Try it yourself
Linux Hardening Checklist Generator turns everything above into a downloadable, distro-specific Markdown checklist — pick Ubuntu, Debian, Fedora, or RHEL and get the exact commands for each section. Pair it with SSH Key Generator and SSH Config Generator for the SSH side, and Linux Permissions Calculator for the permissions review.