Server Hardening Checklist for Production Linux Systems
The baseline
Every production server I touch gets the same treatment before anything else runs on it. This isn't theoretical — it's the exact process I follow when spinning up new infrastructure.
Here's the checklist.
1. SSH hardening
The defaults are not good enough. Change them immediately.
# /etc/ssh/sshd_config
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
MaxAuthTries 3
ClientAliveInterval 300
ClientAliveCountMax 2
AllowUsers deploy
Key-only authentication. No root login. Short timeouts. This alone prevents the majority of brute-force attacks.
2. Firewall — deny by default
UFW makes this straightforward:
ufw default deny incoming
ufw default allow outgoing
ufw allow from <management-ip> to any port 22
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable
The critical part: SSH access is only allowed from known management IPs. Everything else is dropped.
3. Fail2Ban
For the attempts that do get through:
# /etc/fail2ban/jail.local
[sshd]
enabled = true
port = ssh
filter = sshd
maxretry = 3
bantime = 3600
findtime = 600
Three failed attempts and you're banned for an hour. Simple and effective.
4. Automated updates
Unattended security updates are non-negotiable:
apt install unattended-upgrades
dpkg-reconfigure -plow unattended-upgrades
Configure it to only apply security patches automatically. Feature updates should still go through your normal deployment pipeline.
5. Container security
If you're running Docker, the defaults need work too:
- Run containers as non-root users
- Use read-only filesystems where possible
- Limit container capabilities with
--cap-drop ALL - Scan images with
trivybefore deploying
docker run --read-only --cap-drop ALL --cap-add NET_BIND_SERVICE \
--user 1000:1000 your-app:latest
6. Monitoring
A hardened server that nobody watches is still a liability. At minimum:
- System logs — centralized with
journaldorrsyslog - File integrity — monitor
/etc,/usr/binwith AIDE orosquery - Network traffic — watch for unexpected outbound connections
I use a simple Python script that checks for new listening ports and alerts via webhook if anything unexpected appears.
The principle
Security isn't a product you install. It's a process of reducing attack surface, detecting anomalies, and responding quickly. Every item on this list reduces the window an attacker has to work with.
Start with the basics. Get them right. Then build from there.