VPS DeploymentIntermediate 14 min Lesson 28 of 30

Server Hardening

Consolidate the security work: SSH, firewall, updates, least privilege, fail2ban, file integrity and knowing what is exposed.

VPS Deployment · Lesson 28 of 30
0/30 done(0%)

What is it? #

Hardening is applying a set of measures that, together, make a server considerably harder to compromise.

This lesson consolidates what earlier lessons introduced, in the order they should be applied, plus the pieces that belong at the end.

None of it is exotic. The measures that prevent real compromises are key-only SSH, automatic patching, a default-deny firewall, unprivileged services and secrets that are not in git.

The final step is auditing: knowing exactly what is exposed, what runs as root, and who can log in.

Think of it like this #

A pre-flight checklist. Every item is mundane, none of it is clever, and running through it in order is what makes the outcome reliable.

The accidents happen when someone skips the boring items because the aircraft looked fine.

Simple example #

A server about to receive production traffic. You work through the checklist, verify each item, and record what remains deliberately open and why.

Code #

BASH
# 1. SSH — the single highest-value change
sudo tee /etc/ssh/sshd_config.d/10-hardening.conf > /dev/null <<'CONF'
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
MaxAuthTries 3
AllowUsers deploy
ClientAliveInterval 300
ClientAliveCountMax 2
X11Forwarding no
CONF
sudo sshd -t && sudo systemctl reload sshd     # test, keep a session open

# 2. Firewall — default deny
sudo ufw allow OpenSSH
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 80,443/tcp
sudo ufw enable

# 3. Automatic security updates
sudo apt install unattended-upgrades
sudo dpkg-reconfigure --priority=low unattended-upgrades

# 4. fail2ban
sudo apt install fail2ban
sudo tee /etc/fail2ban/jail.local > /dev/null <<'CONF'
[DEFAULT]
bantime  = 1h
findtime = 10m
maxretry = 5
[sshd]
enabled = true
[nginx-limit-req]
enabled = true
CONF
sudo systemctl enable --now fail2ban
BASH
# 5. Services: least privilege everywhere
ps aux | awk '<span class="katex-error" title="ParseError: KaTeX parse error: Expected &#x27;}&#x27;, got &#x27;EOF&#x27; at end of input: …= &quot;root&quot; {print" style="color:#cc0000">1 == &quot;root&quot; {print</span>11}' | sort -u     # what runs as root?
sudo ss -tulpn | grep -v 127.0.0.1                    # what is exposed?
# Each entry should have a justification. Databases and app ports should
# not appear in the second list at all.

# 6. Shared memory and temp directories
grep -q '/dev/shm' /etc/fstab || echo \
  "tmpfs /dev/shm tmpfs defaults,noexec,nosuid,nodev 0 0" | sudo tee -a /etc/fstab

# 7. Kernel settings
sudo tee /etc/sysctl.d/99-hardening.conf > /dev/null <<'CONF'
net.ipv4.conf.all.rp_filter = 1
net.ipv4.icmp_echo_ignore_broadcasts = 1
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.all.send_redirects = 0
net.ipv4.tcp_syncookies = 1
kernel.randomize_va_space = 2
CONF
sudo sysctl --system
BASH
# 8. Audit — run this monthly
sudo lastb | head -20                              # failed logins
sudo grep "Accepted" /var/log/auth.log | tail -20  # successful logins
sudo find / -perm -4000 -type f 2>/dev/null        # setuid binaries
sudo ss -tulpn                                      # listening services
sudo ufw status numbered                            # firewall rules
cat /home/*/.ssh/authorized_keys                    # who has access
systemd-analyze security | head -20                 # service hardening scores
TEXT
The checklist, in order

1.  non-root user with sudo and a key
2.  key-only SSH, root login disabled
3.  default-deny firewall, only 22/80/443
4.  automatic security updates with a reboot window
5.  fail2ban on SSH and the web server
6.  every service as its own unprivileged user
7.  databases and app ports bound to localhost
8.  secrets in 600 env files, never in git
9.  HTTPS with automated renewal and expiry monitoring
10. backups, offsite, encrypted and restore-tested
11. monitoring with actionable alerts
12. a monthly audit of the items above

How it works #

The ordering is deliberate. SSH and the firewall come first because they close the paths an attacker would use while you configure everything else.

AllowUsers limits SSH to named accounts. Even if another account exists with a valid key, it cannot log in.

fail2ban watches logs and bans addresses after repeated failures. With key-only SSH its value there is mostly reducing log noise, but the web server jail is genuinely useful against application-level brute force.

The root and exposure audits are the two questions that matter most. Every process running as root and every publicly listening port should have a reason; in practice these lists are usually longer than expected.

Mounting /dev/shm with noexec prevents executing code from shared memory, which is a technique used in some exploit chains.

The sysctl settings enable address space randomisation, SYN cookies and basic anti-spoofing. They are defaults on many distributions already, and setting them explicitly documents the intent.

systemd-analyze security scores each service on its hardening settings and lists what is missing, which turns abstract advice into a concrete list.

The monthly audit catches drift: the port someone opened for a demo, the key belonging to a former colleague, the service that quietly started running as root after an upgrade.

Real-world use #

The measures above address the causes of the overwhelming majority of server compromises: weak or leaked credentials, unpatched software and exposed services.

Sophisticated attacks exist and are not what happens to most servers. Automated scanning finds an unpatched service or an exposed database, and that is the incident.

Compliance frameworks formalise much of this list. Meeting them is largely a matter of doing these things and documenting that you did.

Configuration management — Ansible, or even a well-maintained shell script — makes hardening reproducible. A server configured by hand over a year cannot be rebuilt reliably.

The audit step is the one most often skipped and the one that catches real problems, because servers drift over time regardless of how carefully they were set up.

Common mistakes #

  • Applying some measures and never auditing whether they still hold.
  • Hardening SSH last, after the server has been publicly reachable for days.
  • Leaving services running as root because changing them seemed risky.
  • Exposing a database or admin interface publicly during setup and forgetting.
  • Configuring everything by hand with no way to reproduce it.

Practice #

Work through the checklist on a test server and verify each item. Then produce the audit output: what runs as root, what listens publicly, who has SSH keys, and the systemd security scores. Write a short justification for every item that remains open.

Quick quiz

  1. 1. Which measure should be applied first?

  2. 2. What does `AllowUsers` in sshd_config do?

  3. 3. Which two audit questions matter most?

  4. 4. What is fail2ban most useful for once SSH is key-only?

  5. 5. Why audit monthly rather than once?

Summary

  • Apply SSH hardening and the firewall first, then updates and least privilege.
  • Every root process and every public port needs a justification.
  • Keep secrets out of git and databases off the public internet.
  • Use systemd hardening options and check the scores.
  • Audit monthly — servers drift.