Sysadmin Reference · 2026

Linux Server Hardening

A practical, opinionated walkthrough of the steps that take a freshly provisioned cloud instance from "default" to "defensible" — without breaking the services you actually need to run.

01Why hardening matters

A default server image is built for convenience, not survival. It ships with password authentication enabled, a permissive firewall, services listening on interfaces you never asked for, and a kernel tuned for general-purpose workloads. Within minutes of getting a public IP, automated scanners will start knocking on port 22, port 3389, and a few hundred others, looking for exactly those defaults.

Hardening is the process of systematically reducing that attack surface. The goal is not to build an impenetrable fortress — no such thing exists — but to raise the cost of an attack high enough that opportunistic adversaries move on to easier targets, and to ensure that if someone does get in, the blast radius is contained and the event is recorded.

Principle

Defence in depth. No single control is sufficient. A locked-down SSH config means little if an unpatched web app gives an attacker a shell, and a perfect firewall is useless if your private key sits unencrypted on a laptop. Layers compound.

This guide assumes a Debian or Ubuntu base (the commands translate cleanly to RHEL-family systems with minor substitutions), root or sudo access, and a server you can afford to lock yourself out of while you practise. Always keep a second session open when changing SSH or firewall rules.

02Users & privileges

The first rule of account security is that nobody logs in as root. Direct root login over the network is a gift to attackers: it is a known username, it has unlimited power, and a successful brute force needs only one credential rather than two.

Create an unprivileged administrative user

# Create the user and add to the sudo group
adduser deploy
usermod -aG sudo deploy

# Copy your public key into place for that user
mkdir -p /home/deploy/.ssh
cp ~/.ssh/authorized_keys /home/deploy/.ssh/
chown -R deploy:deploy /home/deploy/.ssh
chmod 700 /home/deploy/.ssh
chmod 600 /home/deploy/.ssh/authorized_keys

The principle of least privilege

Every account, process, and service should run with the minimum permissions needed to do its job — and no more. A web server does not need to read other users' home directories. A backup script does not need write access to system binaries. When you grant sudo, prefer narrowly scoped rules over blanket access.

# /etc/sudoers.d/deploy — allow only specific commands
deploy ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart nginx, \
                            /usr/bin/systemctl reload nginx
ControlCommand / fileEffect
Lock root passwordpasswd -l rootDisables password-based root login
Audit sudo groupgetent group sudoLists everyone with admin rights
Expire stale accountschage -ESets an account expiry date
Find empty passwordsawk -F: '($2=="")' /etc/shadowFlags accounts with no password
Watch out

Before disabling root, confirm your new admin user can sudo -i successfully in a separate session. Locking root with no working alternative is the single most common self-inflicted lockout.

03SSH lockdown

SSH is almost always the primary remote-access path, which makes it the primary target. A small set of configuration changes eliminates the overwhelming majority of automated attacks against it.

Key-based authentication only

Passwords can be guessed, phished, or reused. A 4096-bit RSA or, better, an Ed25519 key cannot be brute-forced in any meaningful timeframe. Once your keys are confirmed working, disable password auth entirely.

# /etc/ssh/sshd_config.d/99-hardening.conf
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
AuthenticationMethods publickey
X11Forwarding no
AllowAgentForwarding no
MaxAuthTries 3
LoginGraceTime 20
ClientAliveInterval 300
ClientAliveCountMax 2
AllowUsers deploy
Ubuntu 24.04 gotcha

On modern Ubuntu cloud images, /etc/ssh/sshd_config.d/50-cloud-init.conf may re-enable PasswordAuthentication after your file is read, depending on lexical ordering. Name your override file with a higher number (e.g. 99-) and verify the effective config with sshd -T | grep -i passwordauth rather than trusting the file you edited.

Validate before reloading

# Always test the config before applying — a typo here locks you out
sshd -t && systemctl reload ssh

# Confirm the running daemon agrees with your intent
sshd -T | grep -E 'permitrootlogin|passwordauth|pubkeyauth'

Should you change the port?

Moving SSH off port 22 is security theatre against a targeted attacker — a port scan finds it in seconds. But it does dramatically cut the volume of automated log noise, which makes real anomalies easier to spot. It is a convenience, not a control. Spend your effort on keys and rate limiting first.

04Firewall & networking

A host firewall enforces a simple, powerful policy: deny everything inbound except what you explicitly permit. Even if a service is accidentally left listening, the firewall keeps it unreachable from the outside.

nftables: the modern default

# /etc/nftables.conf — a minimal default-deny ruleset
table inet filter {
  chain input {
    type filter hook input priority 0; policy drop;

    # Allow established and related connections
    ct state established,related accept
    ct state invalid drop

    # Loopback is always trusted
    iif lo accept

    # ICMP for diagnostics (rate-limited)
    ip protocol icmp limit rate 5/second accept
    ip6 nexthdr icmpv6 limit rate 5/second accept

    # Public services
    tcp dport { 22, 80, 443 } accept
  }

  chain forward { type filter hook forward priority 0; policy drop; }
  chain output  { type filter hook output  priority 0; policy accept; }
}

Rule ordering matters

Firewalls evaluate rules top to bottom and stop at the first match. A common, painful mistake is placing a broad accept or a logging rule before the specific rule you intended to hit — for example, accepting all UDP before a more specific QUIC rule on port 443 can have the matching/counters behave differently than expected. When a rule "isn't working," check whether an earlier rule already consumed the packet.

Tip

Use nft list ruleset to see the live, fully expanded ruleset — including any rules added dynamically by other tools. The on-disk config file is intent; the live ruleset is reality.

Tasknftablesiptables (legacy)
List rulesnft list rulesetiptables -L -n -v
Add accept rulenft add rule ...iptables -A INPUT ...
Flush allnft flush rulesetiptables -F
Persist/etc/nftables.confiptables-save

05Patching & updates

The vast majority of real-world compromises exploit vulnerabilities for which a patch already exists. Timely updates are the single highest-leverage security activity available to you, and the cheapest.

Unattended security upgrades

apt install unattended-upgrades apt-listchanges

# /etc/apt/apt.conf.d/50unattended-upgrades
Unattended-Upgrade::Allowed-Origins {
    "${distro_id}:${distro_codename}-security";
};
Unattended-Upgrade::Automatic-Reboot "false";
Unattended-Upgrade::Mail "ops@example.com";
Trade-off

Automatic reboots close kernel vulnerabilities fastest but can interrupt services at unpredictable times. For a fleet, stagger reboots or gate them behind a maintenance window. For a single critical box, prefer notification over auto-reboot and apply kernel updates by hand.

Know what's installed and exposed

# What's listening, and on which interface?
ss -tulpn

# Which packages have pending security fixes?
apt list --upgradable 2>/dev/null | grep -i security

06Kernel & sysctl tuning

The kernel exposes hundreds of tunables via sysctl. A focused subset improves both security and resilience against common network-level attacks.

# /etc/sysctl.d/99-hardening.conf

# --- Network spoofing & redirection protection ---
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.all.accept_source_route = 0
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.all.log_martians = 1

# --- SYN flood mitigation ---
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_max_syn_backlog = 4096
net.ipv4.tcp_synack_retries = 2

# --- Resource limits (avoid thread/connection exhaustion) ---
kernel.threads-max = 100000
fs.file-max = 2097152

# --- Kernel exposure reduction ---
kernel.kptr_restrict = 2
kernel.dmesg_restrict = 1
kernel.yama.ptrace_scope = 1

Apply with sysctl --system and verify individual values with sysctl net.ipv4.tcp_syncookies.

Hard-won lesson

A kernel.threads-max set too low, combined with a process that leaks threads, will eventually make the system unable to fork — meaning you can't even SSH in to fix it. Size resource limits for your actual workload plus headroom, and monitor thread/FD counts so a leak is caught before it becomes a crisis.

07Service minimisation

Every running service is a potential entry point. The fastest way to shrink your attack surface is to stop running things you don't need.

  1. Enumerate what's running: systemctl list-units --type=service --state=running
  2. Identify the unfamiliar ones and research whether they're required.
  3. Disable and mask anything unnecessary: systemctl disable --now <service>
  4. Re-run ss -tulpn and confirm the listening sockets match your intent.

Bind to localhost

If a service only serves other local processes (a database, a metrics exporter), bind it to 127.0.0.1 so the network can't reach it at all.

systemd sandboxing

Directives like ProtectSystem=strict, PrivateTmp=true, and NoNewPrivileges=true confine a service even if it's compromised.

Drop capabilities

Use CapabilityBoundingSet= to strip every Linux capability a service doesn't actually need, such as CAP_SYS_ADMIN.

Resource caps

MemoryMax, TasksMax, and LimitNOFILE stop a misbehaving service from starving the whole host.

08Logging & auditing

You cannot respond to what you cannot see. Comprehensive logging turns an invisible compromise into a recorded, investigable event — and is often a compliance requirement on top of that.

The audit daemon

apt install auditd audispd-plugins

# Watch for changes to critical auth files
auditctl -w /etc/passwd -p wa -k identity
auditctl -w /etc/shadow -p wa -k identity
auditctl -w /etc/ssh/sshd_config -p wa -k sshd

# Review events by key
ausearch -k identity --start today

Ship logs off the box

An attacker who gains root will try to erase their tracks. Forwarding logs to a separate, write-only collector (via rsyslog, journald remote, or an agent) means the evidence survives even if the host is wiped. Centralised logs also make correlation across a fleet possible.

logrotate trap

When a service runs as a non-root user but its logrotate config assumes root ownership, rotation silently fails and logs grow without bound — or worse, the service can't write after rotation. Match the su / create directives in the logrotate stanza to the actual runtime user, and watch for held-open deleted file descriptors (lsof | grep deleted) eating disk.

09Intrusion prevention

Even with password auth disabled, automated clients will hammer your services. Fail2ban watches logs for failure patterns and temporarily bans the offending IPs at the firewall, cutting noise and blunting brute-force attempts.

# /etc/fail2ban/jail.local
[DEFAULT]
bantime  = 1h
findtime = 10m
maxretry = 5
backend  = systemd

[sshd]
enabled = true
maxretry = 3
bantime = 24h

Check the state of a jail with fail2ban-client status sshd. Tune bantime and findtime to your traffic — too aggressive and you'll ban legitimate users behind a flaky connection; too lenient and it provides little protection.

Reality check

Fail2ban is a noise reducer and a speed bump, not a primary control. A distributed attack from thousands of IPs sidesteps per-IP rate limits entirely. Treat it as one layer on top of key-only auth and a default-deny firewall — never as a substitute for them.

10TLS & certificates

Any service exposed over the network should encrypt its traffic. Modern automation makes free, auto-renewing certificates trivial — there's no longer any excuse for plaintext or expired certs.

Automated issuance

# Issue and install a cert with acme.sh (standalone mode)
acme.sh --issue -d example.com --standalone --keylength 4096

acme.sh --install-cert -d example.com \
  --key-file       /etc/ssl/private/example.key \
  --fullchain-file /etc/ssl/certs/example.fullchain.pem \
  --reloadcmd     "systemctl reload nginx"

The chain-of-trust pitfall

A certificate that a browser trusts perfectly may still fail with other clients. Some VPN and IKEv2 clients ship a limited root store and won't accept a newer ECDSA-rooted chain that web browsers handle fine. When a cert "works in the browser but not in the app," the issue is almost always the trust chain, not the cert itself — switching the issuing CA to one whose root is widely embedded (e.g. a Sectigo/USERTrust chain) often resolves it.

SymptomLikely causeDirection
Browser OK, IKEv2 failsECDSA root not in client storeSwitch CA / use RSA chain
Issuance fails on port 80Port blocked or proxiedUse DNS-01 challenge
Cert expired silentlyRenewal cron not firingCheck timer + reloadcmd
Behind CDN proxyHTTP-01 hits the proxyDNS-01 via provider API
Don't forget renewal

Issuing a cert is the easy part. The failure mode that actually causes outages is a renewal that silently stopped working months ago. Confirm the renewal timer is active, that the reloadcmd actually reloads the right service, and monitor expiry externally so you find out before your users do.

11Final checklist

Run through this before declaring a server production-ready. Each item maps to a section above.

Closing thought

Hardening is not a one-time event you complete and forget. Threats evolve, packages change defaults, and configuration drifts. The real win is making this baseline reproducible — captured in configuration management — so that every server in your fleet starts hardened and stays that way, and so the next box you provision inherits everything you learned the hard way on this one.