Linux Server Hardening: Fundamentals for Production Systems
AI generated
$
/etc
Linux · Server Security · SSH · Firewall
Linux Server Hardening: Fundamentals for Production Systems
the 80/20 baseline instead of hardening theater

Most successful attacks on Linux servers exploit open SSH password logins, missing firewall rules, and outdated packages, not sophisticated zero-day exploits. This article shows a practical hardening baseline that closes most of the attack surface with minimal effort, without making operations unnecessarily harder.

17 min read SSH · UFW · unattended-upgrades · fail2ban Ubuntu · Debian · production systems

1. Why the 80/20 rule matters for server hardening

Server hardening is often misunderstood as an endless checklist: hundreds of CIS benchmark items to work through eventually. In practice, the overwhelming majority of successful Linux server compromises trace back to a very small set of causes: password-based SSH with weak or reused passwords, completely open firewalls without default-deny, packages left unpatched for months with known CVEs, and unnecessarily many installed services that offer additional attack surface. Anyone who consistently addresses exactly these four points closes the vast majority of entry points that are actually exploited in the wild.

The remaining theoretically possible hardening measures, such as SELinux enforcing modes with complete policies, kernel hardening via sysctl down to the last detail, or complex AppArmor profiles for every service, deliver significantly less additional protection relative to the effort involved. That does not mean these measures are worthless, only that they make sense once the baseline is in place. This article deliberately focuses on the measures with the best ratio of effort to security gain, and closes by showing how to recognize when hardening has turned into mere theater.

2. Securing SSH: key-only authentication

Password authentication over SSH is by far the most common entry point for automated attacks. Every publicly reachable server on port 22 gets scanned by bots within minutes, systematically trying common usernames and passwords. The single most effective step is to disable password login completely and allow only SSH keys. A private key cannot be brute-forced, while even complex passwords can eventually be compromised through wordlists or credential stuffing.

The migration happens in two steps: first deposit the public key on the server and verify that key-based login works, only then disable password login in the SSH configuration. This order matters, because anyone who changes the configuration first and accidentally locks themselves out needs console access through the hosting provider to fix the problem. It also pays off to move the SSH port from 22 to an alternative port. That is not a real security feature, but it drastically reduces the sheer volume of automated scan attempts showing up in the logs.


# Generate a modern Ed25519 key pair on the local machine
ssh-keygen -t ed25519 -C "admin@mironsoft.de" -f ~/.ssh/id_ed25519_server

# Copy the public key to the target server (still using password auth)
ssh-copy-id -i ~/.ssh/id_ed25519_server.pub deploy@203.0.113.10

# Test key-based login before touching sshd_config
ssh -i ~/.ssh/id_ed25519_server deploy@203.0.113.10 "echo key login works"

# Only after the test succeeds: edit /etc/ssh/sshd_config
sudo sed -i \
  -e 's/^#\?PasswordAuthentication.*/PasswordAuthentication no/' \
  -e 's/^#\?PubkeyAuthentication.*/PubkeyAuthentication yes/' \
  -e 's/^#\?KbdInteractiveAuthentication.*/KbdInteractiveAuthentication no/' \
  /etc/ssh/sshd_config

# Validate the config syntax before restarting the daemon
sudo sshd -t && sudo systemctl restart sshd

3. Disabling root login and restricting access

Direct root login over SSH is risky for two reasons: first, the username root is identical on practically every Linux system, making it the preferred target for automated attacks, and second, direct root access removes any traceability of which administrator ran which command. The setting PermitRootLogin no in /etc/ssh/sshd_config forces every administrator to log in with a personal account first and only then escalate to root privileges via sudo. Every privileged command is then logged with a username and timestamp in /var/log/auth.log.

In addition, SSH access should be restricted to the users who actually need it and, where possible, to known IP ranges. The directive AllowUsers deploy admin in the SSH configuration prevents a newly created system user from accidentally gaining SSH access. For environments with fixed office or VPN IP addresses, access can additionally be limited to those address ranges through the firewall. It is important to always keep a second open session or an emergency access path through the provider console before activating such restrictive rules.


# /etc/ssh/sshd_config - production hardening baseline

# Disable direct root login entirely
PermitRootLogin no

# Key-only authentication (set up in section 2)
PasswordAuthentication no
PubkeyAuthentication yes
KbdInteractiveAuthentication no

# Restrict SSH to specific system users
AllowUsers deploy admin

# Reduce noise from automated scanners
Port 2222

# Disconnect idle sessions after 10 minutes
ClientAliveInterval 300
ClientAliveCountMax 2

# Limit authentication attempts per connection
MaxAuthTries 3
LoginGraceTime 20

4. Default-deny firewall with UFW

A firewall with a default-deny policy means that all incoming traffic is blocked by default unless it has been explicitly allowed. That is the exact opposite of a commonly seen practice where all ports stay open and only individual known threats get blocked. On Ubuntu and Debian systems, ufw (Uncomplicated Firewall) provides a simple interface over iptables or nftables that can be brought to a secure baseline configuration with just a few commands.

The basic rule: block inbound traffic by default, allow outbound traffic by default, then deliberately open only the ports that are actually needed, typically SSH on the chosen port, HTTP and HTTPS for a web server, and possibly a monitoring port from a restricted IP range. Every additionally opened rule should be justified and documented, because every open rule is potential attack surface. Before activating ufw enable, it is essential to already have the SSH rule in place, otherwise you lock yourself out of the system.


# Set the default policy first: deny inbound, allow outbound
sudo ufw default deny incoming
sudo ufw default allow outgoing

# Allow SSH on the custom port BEFORE enabling the firewall
sudo ufw allow 2222/tcp comment 'SSH admin access'

# Allow web traffic for the reverse proxy
sudo ufw allow 80/tcp comment 'HTTP'
sudo ufw allow 443/tcp comment 'HTTPS'

# Restrict a monitoring port to a known IP range
sudo ufw allow from 198.51.100.0/24 to any port 9100 comment 'Prometheus node_exporter'

# Enable the firewall (confirms the SSH rule is active first!)
sudo ufw enable

# Review the active ruleset with numbered entries
sudo ufw status numbered

5. Setting up automatic security updates

A large share of successful server compromises exploits vulnerabilities for which a patch has long existed. The window between the publication of a CVE and the first automated exploit attempt is often only a few days, while manual update routines can take weeks or months in practice. On Debian and Ubuntu systems, the unattended-upgrades package solves this problem by automatically installing security-relevant package updates without manual intervention, while functional feature updates can still be approved manually.

The correct configuration matters: only enable the security repositories for automatic updates, not the entire repository, otherwise functional changes could reach the production environment unreviewed. An automatic reboot should only be enabled for a defined maintenance window, for example at three in the morning, and never right after a kernel update when load spikes are occurring. Installation happens with sudo apt install -y unattended-upgrades apt-listchanges followed by sudo dpkg-reconfigure --priority=low unattended-upgrades, and the actual fine-tuning then happens in the configuration file. Email notifications about applied updates create additional transparency without anyone having to manually check daily what got updated.


// /etc/apt/apt.conf.d/50unattended-upgrades (excerpt)
Unattended-Upgrade::Allowed-Origins {
        "${distro_id}:${distro_codename}-security";
        "${distro_id}ESMApps:${distro_codename}-apps-security";
};

// Only reboot automatically inside a defined maintenance window
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "03:00";

// Send a mail summary after every run
Unattended-Upgrade::Mail "admin@mironsoft.de";
Unattended-Upgrade::MailReport "on-change";

// Remove unused dependencies automatically
Unattended-Upgrade::Remove-Unused-Dependencies "true";

6. Minimal package installation and attack surface

Every installed package, every running service, and every open port is potential attack surface, regardless of whether the service is actually used. Many server images ship with software nobody needs on a pure application server: a complete mail transport agent, graphical libraries, compiler toolchains, or print services. Every one of these packages can carry its own security vulnerabilities that still need to be patched despite no active use, and in the worst case serve as an entry point for privilege escalation.

The rule of thumb: only install what is actually needed for the concrete function of the server. A pure web server needs no X server, no local mail daemon for incoming mail, and generally no development header packages in a production system. With apt-mark showmanual you can list explicitly installed packages, sudo apt autoremove --purge removes orphaned dependencies, and sudo ss -tulpn shows every listening port with its owning process. Equally important is regularly checking running services with systemctl list-units --type=service --state=running, to identify services nobody needs anymore but that still keep a port open, for example with sudo systemctl disable --now cups.service.

7. Brute-force protection with fail2ban

Even with password authentication disabled, automated scan bots keep generating noise in the logs by continuously starting connection attempts. fail2ban complements the previous measures by monitoring log files such as /var/log/auth.log and automatically banning IP addresses via a firewall rule for a set period of time after a defined number of failed attempts. This reduces not only the attack surface, but also the amount of log noise an administrator would have to review daily.

The default configuration already monitors the SSH service, but can easily be extended to other services such as Nginx, WordPress login forms, or mail servers. A sensible balance of thresholds matters: overly aggressive settings occasionally lock out legitimate users who mistyped once, while overly generous settings render the protection ineffective. An ignore list for your own office or VPN IP prevents administrators from locking themselves out.


# /etc/fail2ban/jail.local
[DEFAULT]
# Never lock out the office/VPN network
ignoreip = 127.0.0.1/8 198.51.100.0/24

# Ban duration and lookback window
bantime  = 3600
findtime = 600
maxretry = 4

[sshd]
enabled  = true
port     = 2222
filter   = sshd
logpath  = /var/log/auth.log
backend  = systemd

8. Recognizing and avoiding hardening theater

Hardening theater describes measures that look good on a checklist but bring little real security gain while noticeably complicating operations. A classic example is completely disabling ICMP echo requests, because "ping is a security risk". In practice this only hinders your own network troubleshooting, while a determined attacker still finds the server through open TCP ports anyway. Something similar applies to constantly changing the SSH port as the supposed primary security measure: it reduces log noise, but under no circumstances replaces real authentication security.

Another pattern is enabling complex kernel parameters or SELinux policies without understanding the implications, often resulting in legitimate applications failing inexplicably and administrators disabling the protective measure entirely out of frustration, ending up with less security than before. The better strategy is to measure every hardening measure against the question: what concrete attack scenario does this prevent, and is the effort in a reasonable relationship to that benefit? Measures that cannot clearly answer this question deserve additional scrutiny before being deployed to production.

9. Measures compared: effort versus effect

Not every hardening measure delivers the same ratio of implementation effort to actual security gain. The following table compares commonly recommended measures against their actual benefit for a typical production system, helping to set priorities sensibly instead of spending time on measures with a low marginal benefit.

Measure Hardening theater Real effect Assessment
SSH authentication Password with a "strong" policy Key-only exclusively Highest priority, low effort
Firewall Everything open, block individual IPs Default-deny, open selectively Highest priority, low effort
Patch management Manual updates "when there's time" unattended-upgrades automated Highest priority, low effort
Blocking ICMP Disable ping completely Little effect, hinders diagnostics Low priority, often theater
Changing SSH port Treated as the main measure Only reduces log noise Useful only as a supplement
Package minimalism Leave everything preinstalled Install only needed packages Medium priority, good leverage

The table makes clear that the four baseline measures from sections 2 through 5 deliver the greatest security gain per hour invested. Only once this baseline is in place and running stably does it make sense to invest in more elaborate measures such as complete SELinux policies or detailed audit rules with auditd. Anyone who reverses this order spends time on complex configuration while the actual entry point, an open password login, remains unlocked.

Mironsoft

Server hardening, monitoring, and infrastructure operations for Magento stores

Production system not hardened yet?

We audit existing servers for open entry points, set up a solid hardening baseline, and on request take over ongoing patch management, so your Magento or Hyva store stays reliably secured.

Security audit

Reviewing SSH, firewall, and package state for real attack surface

Hardening baseline

Setting up SSH keys, UFW, and automatic updates in production

Ongoing operations

Patch management, monitoring, and incident response on subscription

10. Summary

A solid Linux server hardening baseline for production systems does not need a hundred checklist items, but four consistently implemented core measures: SSH exclusively via key-only authentication, root login disabled and access restricted to needed users, a firewall with default-deny policy that opens only explicitly used ports, and automatic security updates via unattended-upgrades. In addition, minimal package installation further reduces the attack surface, and fail2ban dampens automated scan noise.

The decisive point is the order: first establish the baseline stably, then consider more elaborate measures such as complete SELinux policies or audit frameworks. Anyone who instead immediately invests in complex hardening-theater measures such as blocking ICMP lulls themselves into a false sense of security while the actual entry point remains open. Every measure should be measured against a clear attack scenario, not against a generic checklist.

Linux server hardening for production systems, the essentials at a glance

Securing SSH

Key-only authentication only, PermitRootLogin no, restrict access to needed users.

Default-deny firewall

Block inbound traffic, open only explicitly needed ports with ufw.

Automatic updates

unattended-upgrades for security repositories, defined maintenance window for reboots.

No hardening theater

Measure every measure against a concrete attack scenario, not a generic checklist.

11. FAQ: Linux Server Hardening for Production Systems

1What is the most important first step in server hardening?
Disable password authentication over SSH and allow only key-only login. Closes the most common entry point at the lowest effort.
2Why isn't a strong root password enough?
root is identical on every system and a preferred target. PermitRootLogin no combined with key-only auth is far more secure than any password.
3What does default-deny firewall mean in practice?
All incoming traffic is blocked by default. Only explicitly needed ports are opened deliberately.
4Are automatic updates risky for stability?
Not with correct configuration. Only apply security repositories automatically, reboots within a defined maintenance window.
5What is hardening theater?
Measures that look good on checklists but bring little real security gain, such as completely blocking ICMP.
6Does changing the SSH port provide real security?
No, it only reduces scan noise in logs. Real security comes from key-only auth and a default-deny firewall.
7How do I avoid locking myself out?
Always test the new access method first before disabling the old one. Keep an emergency access path through the provider console.
8What is fail2ban needed for in addition?
fail2ban bans IPs after multiple failed login attempts automatically via a firewall rule and dampens brute-force noise.
9Why uninstall unnecessary packages?
Every package can carry its own security vulnerabilities. Less installed software means less attack surface.
10When is SELinux or AppArmor worthwhile?
Only once baseline measures are stably established. Without this baseline, kernel hardening delivers little relative to the effort.