Linux Server Security Checklist
A step-by-step Linux server hardening checklist: secure SSH, firewall rules, automatic updates, service auditing, fail2ban, and intrusion detection for any internet-facing server.
Before you start
- ▸Root or sudo access on a freshly installed server
- ▸A local SSH key pair already generated (ssh-keygen -t ed25519)
- ▸Basic familiarity with a terminal text editor such as nano or vim
Every server exposed to the public internet is probed within minutes of going online. This checklist covers the essential hardening steps you should complete before a server handles real traffic: locking down SSH, configuring a firewall, enforcing automatic updates, auditing running services, and setting up basic intrusion detection. Work through these in order on a fresh install; retrofitting a live server follows the same steps but requires more care around service restarts.
1. Create a Non-Root Admin User
Logging in directly as root over SSH is unnecessary and dangerous. Create a dedicated admin account, add it to the appropriate sudo group, and you will disable root SSH login in a later step.
Debian/Ubuntu
adduser deploy
usermod -aG sudo deploy
Fedora / RHEL / Rocky
useradd -m deploy
usermod -aG wheel deploy
Arch
useradd -m -G wheel deploy
passwd deploy
Verify you can sudo as the new user before closing your root session.
2. Harden SSH
SSH is the primary attack surface on most servers. Three changes make an enormous difference: key-only authentication, disabling root login, and changing the default port (optional, but cuts automated noise).
Install your public key
# Run this from your LOCAL machine, replacing the IP and user
ssh-copy-id -i ~/.ssh/id_ed25519.pub [email protected]
Edit /etc/ssh/sshd_config
Set these directives explicitly. They may already exist — search before appending.
Port 2222 # optional: non-default port
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
AuthorizationKeysFile .ssh/authorized_keys
X11Forwarding no
AllowTcpForwarding no
MaxAuthTries 3
LoginGraceTime 30
ClientAliveInterval 300
ClientAliveCountMax 2
Restart and verify
sshd -t # syntax check before restarting
systemctl restart sshd
Do not close your current session until you have confirmed a new session connects successfully with the key.
3. Configure the Firewall
Allow only the ports your services actually need. Below are the common defaults: SSH (adjust if you changed the port), HTTP, and HTTPS.
UFW (Debian/Ubuntu)
ufw default deny incoming
ufw default allow outgoing
ufw allow 2222/tcp # your SSH port
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable
ufw status verbose
firewalld (Fedora / RHEL / Rocky)
systemctl enable --now firewalld
firewall-cmd --permanent --remove-service=ssh # removes port 22
firewall-cmd --permanent --add-port=2222/tcp
firewall-cmd --permanent --add-service=http
firewall-cmd --permanent --add-service=https
firewall-cmd --reload
firewall-cmd --list-all
nftables (Arch / any distro)
cat /etc/nftables.conf # inspect existing rules before editing
systemctl enable --now nftables
Edit /etc/nftables.conf directly to add your allowed ports, then systemctl reload nftables.
4. Enable Automatic Security Updates
Unpatched vulnerabilities are the most common root cause of server compromise. Automate security-only updates; review major version upgrades manually.
Debian/Ubuntu — unattended-upgrades
apt install -y unattended-upgrades
dpkg-reconfigure -plow unattended-upgrades
Verify the service is active:
systemctl is-enabled unattended-upgrades
Fedora / RHEL / Rocky — dnf-automatic
dnf install -y dnf-automatic
# Edit /etc/dnf/automatic.conf: set upgrade_type = security
systemctl enable --now dnf-automatic-install.timer
Arch
Arch ships rolling releases with no concept of security-only updates. Use a systemd timer to run pacman -Syu regularly, and review the Arch Security Advisories at security.archlinux.org.
5. Disable Unused Services
Every listening daemon is an attack surface. List what is running, stop anything you don't recognise or need, and disable it so it won't restart on boot.
systemctl list-units --type=service --state=running
# Example: disable the Avahi mDNS daemon (rarely needed on servers)
systemctl stop avahi-daemon
systemctl disable avahi-daemon
systemctl mask avahi-daemon # prevents re-enabling by package updates
Also audit listening network sockets:
ss -tlnp
Every entry in the output should be a service you deliberately chose to expose. If you see 0.0.0.0 on a port you didn't intend to open, investigate immediately.
6. Install fail2ban for Brute-Force Protection
Even with key-only SSH, fail2ban blocks repeated failed login attempts and can protect other services like Nginx, Postfix, or WordPress.
Debian/Ubuntu
apt install -y fail2ban
Fedora / RHEL / Rocky
dnf install -y fail2ban
Configure a local jail override
Never edit /etc/fail2ban/jail.conf directly; it gets overwritten on upgrade. Create a local override instead:
cat > /etc/fail2ban/jail.local <<'EOF'
[DEFAULT]
bantime = 1h
findtime = 10m
maxretry = 5
[sshd]
enabled = true
port = 2222
EOF
systemctl enable --now fail2ban
fail2ban-client status sshd
7. Kernel and Sysctl Hardening
A handful of kernel parameters meaningfully reduce the attack surface against network-based exploits. Add them to a drop-in file so they survive package updates to /etc/sysctl.d/.
cat > /etc/sysctl.d/99-hardening.conf <<'EOF'
# Disable IP forwarding (unless this is a router/container host)
net.ipv4.ip_forward = 0
net.ipv6.conf.all.forwarding = 0
# Protect against SYN flood
net.ipv4.tcp_syncookies = 1
# Ignore ICMP redirects
net.ipv4.conf.all.accept_redirects = 0
net.ipv6.conf.all.accept_redirects = 0
net.ipv4.conf.all.send_redirects = 0
# Ignore broadcast pings
net.ipv4.icmp_echo_ignore_broadcasts = 1
# Randomise virtual address space
kernel.randomize_va_space = 2
EOF
sysctl --system
If this server runs containers or acts as a router, keep ip_forward enabled; disabling it will break routing.
8. Audit File Permissions and SUID Binaries
SUID/SGID binaries run with elevated privileges regardless of which user executes them. Know what you have.
find / -xdev -perm /4000 -type f 2>/dev/null # SUID binaries
find / -xdev -perm /2000 -type f 2>/dev/null # SGID binaries
Compare the output against a known-good baseline. If you see anything unexpected — especially in /tmp, /var, or home directories — investigate. You can remove the SUID bit from binaries you don't need:
chmod u-s /usr/bin/some-binary
9. Install an Intrusion Detection Baseline
AIDE (Advanced Intrusion Detection Environment) records cryptographic hashes of important system files. Run it after hardening and store the database somewhere off-server; compare it periodically to detect unexpected changes.
Debian/Ubuntu
apt install -y aide
aide --config=/etc/aide/aide.conf --init
mv /var/lib/aide/aide.db.new /var/lib/aide/aide.db
Fedora / RHEL / Rocky
dnf install -y aide
aide --init
mv /var/lib/aide/aide.db.new.gz /var/lib/aide/aide.db.gz
Schedule regular checks with a systemd timer or cron, and have the output emailed to you or shipped to a log aggregator.
Verification
After completing the checklist, run a quick external scan against your own server. nmap from a separate machine gives you an attacker's-eye view:
# From a different machine
nmap -sV -p- --open 203.0.113.10
The only open ports should be the ones you explicitly allowed. A clean result here is not a guarantee of security, but an obvious misconfiguration will show up immediately.
Troubleshooting
- Locked out of SSH after changing the port or disabling passwords: You will need console access (cloud provider serial console or physical KVM). Always verify new SSH settings in a second terminal before logging out.
- UFW/firewalld blocking a service unexpectedly: Run
journalctl -xeand look forUFW BLOCKorREJECTkernel log entries to identify the offending port or protocol. - fail2ban not banning IPs: Check
fail2ban-client status sshdand inspect/var/log/fail2ban.log. A common issue is a mismatched log path or backend; setbackend = systemdinjail.localif your distro uses journald without traditional log files. - sysctl settings not persisting after reboot: Confirm the file is in
/etc/sysctl.d/and ends in.conf. Runsysctl --systemagain and verify withsysctl net.ipv4.tcp_syncookies.
Frequently asked questions
- Do I need to do all of these steps every time I provision a server?
- Yes, or better: bake them into a provisioning script or configuration management tool (Ansible, Terraform, cloud-init) so every server starts hardened without manual effort.
- Is changing the SSH port from 22 actually worth it?
- It does not improve real security against a targeted attacker, but it eliminates virtually all automated credential-stuffing noise in your logs, which makes genuine intrusion attempts easier to spot.
- Should I disable IPv6 if I'm not using it?
- Disabling it entirely can break some system functionality and is not necessary. Instead, make sure your firewall rules cover both IPv4 and IPv6 — ufw and firewalld do this by default.
- How does fail2ban work when I've already disabled password authentication on SSH?
- It still provides value by banning IPs that probe for valid usernames, attempt expired-key handshakes, or attack other services like Nginx, Postfix, or a web application on the same host.
- What should I do after AIDE reports a changed file?
- Determine whether the change was expected (a package update, a config edit you made) or unexplained. Unexplained changes to binaries, libraries, or cron jobs should be treated as a potential compromise and investigated before re-updating the baseline.
Related guides
Manage Secrets with Ansible Vault
Encrypt Ansible secrets with AES-256 using ansible-vault: encrypt files and inline vars, automate with password files, and isolate group-level secrets with vault IDs.
AppArmor Explained
Learn how AppArmor profiles work, how to switch between enforce and complain mode, create new profiles, and diagnose access denials on Ubuntu, Debian, and Arch.
Apply CIS Benchmarks with OpenSCAP
Use OpenSCAP and scap-security-guide to evaluate, report on, and remediate Linux systems against CIS Benchmarks — covering install, eval, and automation.
How to Audit a Linux System with auditd
Set up auditd on Linux to track file access, syscalls, and privilege use. Covers persistent rules, file watches, ausearch, and aureport across major distros.