A brand new Linux server is a liability until you've spent half an hour on it. Automated scanners find a fresh public IP within minutes — I've watched SSH brute-force attempts start on a box that had been reachable for under ten minutes.
This is what I do between "the provider says it's ready" and "I'm willing to put customer traffic on it". It's ordered by how much risk each step removes per minute spent.
Before you start: open a second SSH session and leave it connected. Every time you change SSH or firewall configuration, test the change in a new connection while the old one is still alive. If you lock yourself out, that session is the difference between a fix and a console ticket.
1. Patch first, everything else second
Provider images are frequently weeks or months old. The most likely way into your server right now is a vulnerability that already has a patch sitting in the repository.
# RHEL / Rocky / Alma
dnf update -y && dnf install -y epel-release
# Debian / Ubuntu
apt update && apt full-upgrade -y
# Reboot if the kernel changed — check first
needs-restarting -r # RHEL family
[ -f /var/run/reboot-required ] && echo "reboot needed" # Debian family
Then make sure security updates keep applying without you:
# RHEL family
dnf install -y dnf-automatic
sed -i 's/^apply_updates =.*/apply_updates = yes/' /etc/dnf/automatic.conf
sed -i 's/^upgrade_type =.*/upgrade_type = security/' /etc/dnf/automatic.conf
systemctl enable --now dnf-automatic.timer
# Debian family
apt install -y unattended-upgrades
dpkg-reconfigure -plow unattended-upgrades
Security updates only. Unattended full upgrades on a production web server will eventually restart PHP or MySQL at a time you didn't choose. Restrict automation to the security repository and handle feature upgrades in a window.
2. A real user, and no more root logins
Working as root all day means every mistake is maximally destructive and nothing in the audit log tells you which human did it. Create a named account with sudo, put your key on it, and confirm it works before you disable root.
adduser alan
usermod -aG wheel alan # 'sudo' group on Debian/Ubuntu
mkdir -p /home/alan/.ssh
cp /root/.ssh/authorized_keys /home/alan/.ssh/ # or paste your public key
chown -R alan:alan /home/alan/.ssh
chmod 700 /home/alan/.ssh
chmod 600 /home/alan/.ssh/authorized_keys
Now — in a new terminal — log in as that user and run sudo -v. Only when
that works do you continue.
3. Lock down SSH
SSH is the front door. These four settings remove the overwhelming majority of opportunistic attacks, because almost all of it is automated password guessing.
# No root logins over the network
PermitRootLogin no
# Keys only — this is the single highest-value line here
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
# Don't let unauthenticated sessions linger
LoginGraceTime 30
MaxAuthTries 3
MaxSessions 5
# Only these accounts may log in at all
AllowUsers alan
# Disable features you almost certainly don't use
X11Forwarding no
AllowAgentForwarding no
PermitEmptyPasswords no
sshd -t && systemctl reload sshd # -t refuses to reload a broken config
On changing the SSH port: it cuts log noise dramatically, which has real value when you're reading logs by hand. It is not a security control — a port scan finds it in seconds. Move it for the quiet, not for the safety.
4. Default-deny firewall
The rule is simple: deny everything inbound, then open only what a real user needs. If you can't name who connects to a port, it shouldn't be open.
systemctl enable --now firewalld
firewall-cmd --set-default-zone=drop
firewall-cmd --permanent --add-service=ssh
firewall-cmd --permanent --add-service=http
firewall-cmd --permanent --add-service=https
firewall-cmd --reload
firewall-cmd --list-all
ufw default deny incoming
ufw default allow outgoing
ufw allow OpenSSH
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable
ufw status verbose
On hosting servers I generally use CSF instead — it wraps iptables with connection tracking, and its LFD daemon handles login-failure blocking, port-scan detection and process tracking in one place.
5. Ban the persistent guessers
Key-only SSH already defeats password attacks, but fail2ban stops the noise and covers the other services — mail, FTP, panel logins — where passwords still exist.
[DEFAULT]
bantime = 1h
findtime = 10m
maxretry = 4
# Never lock yourself out from the office or a jump host
ignoreip = 127.0.0.1/8 ::1 203.0.113.0/24
[sshd]
enabled = true
[postfix-sasl]
enabled = true
Set ignoreip before you enable it, not after you've banned yourself. Everyone does
this exactly once.
6. Kernel and network parameters
A handful of sysctl values close off spoofing and redirect tricks that have no
legitimate use on a normal web server.
# Reject spoofed source addresses
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1
# Ignore ICMP redirects and source routing
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.all.accept_source_route = 0
net.ipv6.conf.all.accept_redirects = 0
# Log packets with impossible addresses
net.ipv4.conf.all.log_martians = 1
# SYN flood resistance
net.ipv4.tcp_syncookies = 1
# Restrict kernel pointer exposure and dmesg
kernel.kptr_restrict = 2
kernel.dmesg_restrict = 1
# Protect against symlink/hardlink races in shared /tmp
fs.protected_symlinks = 1
fs.protected_hardlinks = 1
sysctl --system
7. Shrink the attack surface
Every listening service is a door. Find out what's actually listening — the answer on a default image is usually "more than you expected".
# Listening sockets with owning process
ss -tulpn
# Services set to start at boot
systemctl list-unit-files --state=enabled --type=service
# Disable what you can't justify
systemctl disable --now rpcbind avahi-daemon cups 2>/dev/null
Then tighten the filesystem. Mount options on /tmp and /dev/shm block a
common escalation path: dropping a binary in a world-writable directory and executing it.
/dev/shm /dev/shm tmpfs defaults,nodev,nosuid,noexec 0 0
/tmp /tmp tmpfs defaults,nodev,nosuid,noexec 0 0
Test noexec on /tmp before you rely on it. Some package managers and installers legitimately execute from /tmp and will fail with confusing errors. Verify updates still work before you consider it done.
8. Logging you'll be glad of later
During an incident, the question is always "when did this start, and what else did they touch?". You can only answer it if you were logging beforehand.
# /etc/audit/rules.d/hardening.rules
-w /etc/passwd -p wa -k identity
-w /etc/shadow -p wa -k identity
-w /etc/sudoers -p wa -k privilege
-w /etc/ssh/sshd_config -p wa -k sshd
-w /var/log/lastlog -p wa -k logins
-a always,exit -F arch=b64 -S execve -F euid=0 -k root-commands
Just as important: ship logs off the box. Local logs are the first thing an attacker edits. Even a nightly rsync to somewhere they don't have credentials for is vastly better than nothing.
9. Backups, and a restore you have actually tested
Hardening reduces the chance of a bad day. Backups decide how bad the bad day is. The rule I hold to: a backup nobody has restored is a rumour.
- Backups go somewhere the server cannot reach with its own credentials — otherwise ransomware encrypts them too.
- Restore one, on a schedule, into an isolated environment. Automate the check; a human will stop doing it by week three.
- Know your actual RPO and RTO — how much data can you lose, and how long may recovery take? If you can't answer, you don't have a backup strategy, you have backup files.
10. Verify what you've done
# Confirm password auth is genuinely off
ssh -o PreferredAuthentications=password -o PubkeyAuthentication=no user@host
# expected: "Permission denied (publickey)"
# Confirm the firewall from outside the box
nmap -Pn -p- your.server.ip
# General audit with a scoring tool
lynis audit system
Thirty minutes of this removes the entire class of opportunistic, automated attack that makes up most of what actually hits a server. It won't stop a determined attacker with a zero-day in your application — nothing at this layer will. But it means that when something does happen, it was interesting, and you have the logs to reconstruct it.