Skip to content

Server Hardening Checklist

A practical lockdown sequence for a freshly provisioned Linux server. Work through it top to bottom on a new box before it carries any real traffic.

Don't lock yourself out

When changing SSH and firewall settings, keep your current session open and test the new config in a second terminal before closing the first. Confirm key-based login works before you disable passwords.

Tested on

AlmaLinux 9 / RHEL 9 (firewalld, dnf). Debian/Ubuntu equivalents (ufw, apt) are noted inline.

1. Patch the system first

# RHEL/AlmaLinux
sudo dnf upgrade --refresh -y
# Debian/Ubuntu
sudo apt update && sudo apt full-upgrade -y

Enable automatic security updates so the box doesn't drift:

# RHEL/AlmaLinux
sudo dnf install -y dnf-automatic
sudo sed -i 's/^apply_updates = no/apply_updates = yes/' /etc/dnf/automatic.conf
sudo systemctl enable --now dnf-automatic.timer

# Debian/Ubuntu
sudo apt install -y unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades

2. Create a non-root admin user

Never operate day to day as root over SSH.

sudo useradd --create-home --shell /bin/bash deepak
sudo passwd deepak
sudo usermod -aG wheel deepak     # 'sudo' group on Debian/Ubuntu

Set up key-based login for the new user (run on your laptop):

ssh-copy-id deepak@SERVER_IP
# then verify it works in a NEW terminal before continuing:
ssh deepak@SERVER_IP

See User & Permission Management for sudo scoping details.

3. Harden SSH

Edit /etc/ssh/sshd_config (or better, a drop-in in /etc/ssh/sshd_config.d/99-hardening.conf):

# Disable direct root login over SSH
PermitRootLogin no
# Disable password auth entirely — keys only
PasswordAuthentication no
KbdInteractiveAuthentication no
# Disable legacy challenge/response
ChallengeResponseAuthentication no
# Reasonable idle timeout
ClientAliveInterval 300
ClientAliveCountMax 2
# Optional: restrict who can log in
AllowUsers deepak

Validate the config, then reload — not restart, so existing sessions hold:

sudo sshd -t           # syntax check; fix any error before proceeding
sudo systemctl reload sshd

Test before you trust

Open a new SSH session as your admin user now. Only once that succeeds should you close your original root session.

Changing the SSH port?

If you move SSH off 22, you must also open the new port in the firewall (Step 4) and, on RHEL with SELinux enforcing, register it:

sudo semanage port -a -t ssh_port_t -p tcp 2222

4. Configure the firewall

Default-deny inbound, allow only what you serve.

# RHEL/AlmaLinux (firewalld)
sudo systemctl enable --now firewalld
sudo firewall-cmd --permanent --add-service=ssh
sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --reload
sudo firewall-cmd --list-all          # verify

# Debian/Ubuntu (ufw)
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH
sudo ufw allow 80,443/tcp
sudo ufw enable
sudo ufw status verbose

5. Block brute-force with fail2ban

fail2ban watches logs and temporarily bans IPs that fail auth repeatedly.

# RHEL/AlmaLinux (needs EPEL)
sudo dnf install -y epel-release
sudo dnf install -y fail2ban
# Debian/Ubuntu
sudo apt install -y fail2ban

Create /etc/fail2ban/jail.local (never edit jail.conf directly):

[DEFAULT]
bantime  = 1h
findtime = 10m
maxretry = 5
backend  = systemd

[sshd]
enabled = true
sudo systemctl enable --now fail2ban
sudo fail2ban-client status sshd      # see banned IPs and counters

6. Reduce the attack surface

# See what's listening on the network
sudo ss -tulpn

# Disable services you don't need (example)
sudo systemctl disable --now cups avahi-daemon 2>/dev/null || true

Leave SELinux enforcing

On RHEL-family systems, resist the urge to disable SELinux. Check it's enforcing and learn audit2allow instead:

getenforce          # should print: Enforcing
sudo ausearch -m avc -ts recent   # review recent denials

7. Enable auditing and time sync

# Accurate time is a prerequisite for trustworthy logs and TLS
sudo systemctl enable --now chronyd      # chrony on RHEL; systemd-timesyncd on Debian
chronyc tracking

# The audit daemon records security-relevant events
sudo systemctl enable --now auditd

Verification checklist

Run these to confirm the box is locked down:

sudo sshd -T | grep -Ei 'permitrootlogin|passwordauth'   # no / no
sudo firewall-cmd --list-all                              # only intended services
sudo fail2ban-client status sshd                          # jail active
systemctl is-active dnf-automatic.timer chronyd auditd
getenforce                                                # Enforcing
sudo ss -tulpn                                            # no surprise listeners

Done

A fresh server with these steps applied has: keys-only SSH with no root login, a default-deny firewall, automatic patching, brute-force protection, accurate time, and auditing. That's a solid baseline — layer application-specific hardening on top.

Test yourself