Skip to content

Server Hardening Guide

Hardening reduces a server’s attack surface. The highest-impact controls are few and well-known; most breaches exploit the basics (exposed services, weak SSH, unpatched software), not exotic flaws. This guide is opinionated about what matters most for typical self-hosted boxes, using CIS benchmarks as a framework rather than gospel.

This expands on the checklist in the Server Guides index into an actionable, ordered guide.

The CIS Benchmarks are consensus hardening baselines per OS, with Level 1 (sensible, low-friction) and Level 2 (stricter, higher-impact) profiles. Scan with CIS-CAT or OpenSCAP to measure against them — but treat them as a guide, not a mandate; some controls are compliance theater for a single-purpose host. Start at Level 1 and adopt Level 2 controls that fit your threat model (see Threat Modeling).

SSH is the front door — lock it first:

/etc/ssh/sshd_config.d/hardening.conf
PermitRootLogin no
PasswordAuthentication no # key-only auth
KbdInteractiveAuthentication no
AllowGroups ssh-users # only members of this group may log in
ClientAliveInterval 300
ClientAliveCountMax 2

Use SSH keys (ideally hardware-backed), disable password and root login, and restrict who can log in. Changing the port cuts log noise but is obscurity, not security — don’t rely on it. sshd -t before reloading, and keep a second session open so a typo can’t lock you out.

Allow only what you serve. With UFW:

Terminal window
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp # SSH (allow BEFORE enabling, or you lock yourself out)
sudo ufw allow 443/tcp
sudo ufw enable

nftables is the modern lower-level alternative when you need finer control. Don’t forget IPv6 rules.

fail2ban bans IPs after repeated auth failures, blunting brute-force and credential-stuffing:

/etc/fail2ban/jail.local
[sshd]
enabled = true
maxretry = 4
bantime = 1h
ignoreip = 127.0.0.1/8 10.0.0.0/24 # never ban yourself

Tighten network and kernel behavior:

/etc/sysctl.d/99-hardening.conf
net.ipv4.tcp_syncookies = 1
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.all.accept_source_route = 0
net.ipv4.conf.all.rp_filter = 1
kernel.kptr_restrict = 2
fs.protected_hardlinks = 1
fs.protected_symlinks = 1

Apply with sudo sysctl --system.

Unpatched software is the most common entry point. Automate security updates:

Terminal window
# Debian/Ubuntu
sudo apt install -y unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades
# RHEL/Fedora: dnf-automatic

Auto-apply security patches; review feature/major upgrades manually. Pair with CVE awareness — see Tracking CVEs and Vulnerabilities.

Keep AppArmor (Debian/Ubuntu) or SELinux (RHEL/Fedora) in enforcing mode — they confine a compromised service to what it’s allowed to touch. Don’t disable them to make an app “work”; write/adjust a profile instead.

auditd records security-relevant events (privileged commands, auth, file changes) for detection and forensics:

Terminal window
sudo apt install -y auditd
sudo auditctl -w /etc/passwd -p wa -k identity # watch a sensitive file
sudo ausearch -k identity # query the events

Forward logs off-box so an attacker can’t erase them, and feed them into detection — see Blue Team Tools. Periodic rootkit scans (rkhunter, chkrootkit) add a backstop, with the caveat that they detect known rootkits only.