[ Servers & DevOps  /  6 min read ]

Linux server hardening checklist: the steps I run on every new server

[ Short answer ]

A Linux server hardening checklist should cover key-only SSH with root login disabled, a default-deny firewall (ufw or nftables), fail2ban, automatic security updates, a non-root user with limited sudo, tight file permissions, auditd logging, only necessary services listening, off-site encrypted backups that you have test-restored, and monitoring that alerts on changes.

A practical Linux server hardening checklist comes down to ten things: key-only SSH with root login disabled, a default-deny firewall, fail2ban on anything exposed, automatic security updates, a non-root user with scoped sudo, sane file permissions, audit logging, minimal running services, tested off-site backups, and monitoring that tells you when something changes. Below is the checklist I run on every new server, with the exact commands. Examples are for Ubuntu 24.04 LTS; Debian 12 is nearly identical, and I note where they differ.

Work through it in order, and keep a second SSH session open while you change SSH or firewall settings. Locking yourself out of a cloud VM is recoverable, but it is an annoying way to spend an hour.

1. Create a non-root user and apply least privilege

Nobody should log in as root day to day. Create a named user, give it sudo, and put your public key in place before you touch SSH settings:

adduser deploy
usermod -aG sudo deploy
install -d -m 700 -o deploy -g deploy /home/deploy/.ssh
install -m 600 -o deploy -g deploy /root/.ssh/authorized_keys /home/deploy/.ssh/authorized_keys

The permissions matter. If ~/.ssh or authorized_keys is group- or world-writable, sshd refuses the key and logs Authentication refused: bad ownership or modes for directory /home/deploy/.ssh, while the client just sees Permission denied (publickey).

For service accounts (a deploy user for CI, a backup user), do not grant full sudo. Give them exactly the commands they need in a sudoers drop-in, edited with visudo -f /etc/sudoers.d/deploy:

deploy ALL=(root) NOPASSWD: /usr/bin/systemctl reload nginx, /usr/bin/systemctl restart myapp

Application processes should run as their own unprivileged user too, never root. If a web app is compromised, the attacker gets that user’s permissions and nothing more.

2. SSH hardening: keys only, no root login

This is the single most important step on an internet-facing server; password-guessing bots start hitting port 22 within minutes of a new IP going live. Put the settings in a drop-in file rather than editing the main config. One detail that trips people up: sshd uses the first value it reads for each option, and on cloud images /etc/ssh/sshd_config.d/50-cloud-init.conf often sets PasswordAuthentication yes. Name your file so it sorts first:

# /etc/ssh/sshd_config.d/00-hardening.conf
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
AuthenticationMethods publickey
MaxAuthTries 3
LoginGraceTime 30
X11Forwarding no
AllowUsers deploy

Test the config, confirm the effective values, then reload:

sudo sshd -t
sudo sshd -T | grep -E 'permitrootlogin|passwordauthentication|kbdinteractive'
sudo systemctl reload ssh

Now open a new terminal and log in as deploy before closing the old one. Use Ed25519 keys (ssh-keygen -t ed25519) for anything new.

Changing the port is optional; it cuts log noise but is not real security. If you do, note that Ubuntu 22.10 and later use socket activation for SSH, so after editing Port you need sudo systemctl daemon-reload && sudo systemctl restart ssh.socket, and you must open the new port in the firewall first.

3. Firewall: default deny with ufw or nftables

Allow only what the server actually serves. With ufw on Ubuntu:

sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw limit OpenSSH
sudo ufw allow 80,443/tcp
sudo ufw enable
sudo ufw status verbose

ufw limit rate-limits SSH to six new connections per 30 seconds from one address, which blunts brute-force attempts on its own. Better still, if your team has static IPs or a VPN, restrict SSH to those: sudo ufw allow from 203.0.113.10 to any port 22 proto tcp. On a cloud provider, mirror the same rules in the security group or cloud firewall so there are two layers.

If you prefer nftables directly (the default backend on Debian 12), a minimal /etc/nftables.conf input chain looks like this:

table inet filter {
  chain input {
    type filter hook input priority 0; policy drop;
    ct state established,related accept
    iif lo accept
    ip protocol icmp accept
    ip6 nexthdr icmpv6 accept
    tcp dport { 22, 80, 443 } accept
  }
}

One warning I give on every Docker host: Docker writes its own iptables rules, and a port published with -p 5432:5432 is reachable from the internet even if ufw denies it. Bind internal services to localhost (127.0.0.1:5432:5432) or do not publish them at all. I cover this in running Docker Compose in production.

4. fail2ban for anything that accepts logins

fail2ban watches logs and bans IPs that fail repeatedly. With key-only SSH it mostly reduces noise, but it earns its place on web logins, mail and anything else password-based.

sudo apt install fail2ban
sudo tee /etc/fail2ban/jail.local >/dev/null <<'EOF'
[DEFAULT]
bantime  = 1h
findtime = 10m
maxretry = 5
bantime.increment = true

[sshd]
enabled = true
backend = systemd
EOF
sudo systemctl restart fail2ban
sudo fail2ban-client status sshd

backend = systemd matters on Debian 12, which has no /var/log/auth.log by default and logs to the journal only; without it the sshd jail fails to start. Never edit jail.conf directly — package updates overwrite it.

5. Automatic security updates

Most real-world compromises of small servers use known, already-patched vulnerabilities. Unattended-upgrades closes that gap:

sudo apt install unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades

That writes /etc/apt/apt.conf.d/20auto-upgrades. By default only the security pocket is applied, which is what I want on production. Kernel updates need a reboot to take effect, so decide on a policy: either enable Unattended-Upgrade::Automatic-Reboot "true"; with Unattended-Upgrade::Automatic-Reboot-Time "04:00"; in /etc/apt/apt.conf.d/50unattended-upgrades, or check /var/run/reboot-required in your monitoring and reboot during a maintenance window. Doing neither means running an old kernel indefinitely.

6. Secure Ubuntu server basics: services, permissions and kernel settings

Remove what you do not use

Every listening service is attack surface. List them and disable anything unexpected:

sudo ss -tulpn
systemctl list-unit-files --state=enabled

Databases and caches should listen on 127.0.0.1 or a private interface only. A Redis or MongoDB instance on 0.0.0.0 with no auth is still one of the most common ways servers get owned.

File permissions

Find world-writable files and unexpected SUID binaries:

sudo find / -xdev -type f -perm -0002 -ls 2>/dev/null
sudo find / -xdev -type f -perm -4000 -ls 2>/dev/null

Keep secrets such as .env files at 600, owned by the app user. Web roots should be owned by a deploy user and only readable by the web server process, with write access limited to upload directories.

Kernel network settings

A few sysctl values in /etc/sysctl.d/99-hardening.conf, applied with sudo sysctl --system:

net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.all.accept_source_route = 0
net.ipv4.conf.all.rp_filter = 1
net.ipv4.tcp_syncookies = 1
kernel.kptr_restrict = 2

7. Audit logging and monitoring

Hardening reduces the chance of a breach; logging tells you whether one happened. auditd records changes to the files that matter:

sudo apt install auditd
sudo tee /etc/audit/rules.d/hardening.rules >/dev/null <<'EOF'
-w /etc/passwd -p wa -k identity
-w /etc/shadow -p wa -k identity
-w /etc/sudoers -p wa -k sudoers
-w /etc/sudoers.d/ -p wa -k sudoers
-w /etc/ssh/sshd_config -p wa -k sshd
-w /etc/ssh/sshd_config.d/ -p wa -k sshd
EOF
sudo augenrules --load
sudo ausearch -k identity --start today

Logs on the server itself can be wiped by an attacker with root, so ship them off the box (CloudWatch, a syslog host, or any hosted log service). For monitoring, at minimum alert on disk usage, failed systemd units, SSH logins from new IPs and a pending reboot. Run sudo lynis audit system after hardening; it is a free scanner that gives you a score and a list of remaining suggestions, and I use it to verify nothing was missed.

8. Backups you have actually restored

Backups are part of hardening because ransomware and a bad rm both end the same way without them. The rules I follow: off-site (a different provider or account from the server), encrypted, automated, and with a restore tested at least once. restic to S3-compatible storage is a solid default:

export RESTIC_REPOSITORY=s3:s3.amazonaws.com/my-backup-bucket/web01
export RESTIC_PASSWORD_FILE=/root/.restic-pass
restic backup /etc /var/www /home --exclude-caches
restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prune

Dump databases to a file first (pg_dump, mysqldump --single-transaction) rather than copying live data files. Give the backup credentials write access only, with object lock or versioning on the bucket, so a compromised server cannot delete its own backups.

When it is worth getting help

If you run one or two servers and have an afternoon, this checklist gets you most of the way. It is worth getting help when you have a fleet with drifting configs, a compliance requirement to evidence, a server you suspect is already compromised, or simply no one who wants to own patching and log review. My server security hardening service applies this baseline (and the parts that are specific to your stack), documents it, and scripts it so new servers start hardened. For fresh builds, it is part of server setup and configuration. Small jobs are $20/hour; a full hardening project is a fixed price agreed in writing. You can also get an existing server audited before changing anything.

Frequently asked questions

Is changing the SSH port worth it?

It reduces automated log noise, but it is not a security control on its own; scanners find non-standard ports quickly. Key-only authentication, disabled root login and a firewall restricted to known IPs do the real work. Change the port if the noise bothers you, not as a substitute.

Do I still need fail2ban if SSH passwords are disabled?

For SSH it becomes mostly a noise and resource reducer, because keys cannot be brute-forced in practice. It is still worth running for any service that accepts passwords, such as web application logins, mail or FTP.

How often should I re-audit a hardened server?

Re-run a scanner like Lynis and review listening services after any major change and at least quarterly. Configuration drifts as packages, users and applications are added, and new servers built by hand rarely match the original baseline.

Does hardening work the same on Amazon Linux or Rocky Linux?

The principles are identical, but the tools differ: firewalld instead of ufw, dnf-automatic instead of unattended-upgrades, and SELinux enabled by default, which you should keep in enforcing mode rather than disabling.

[ Want it done for you? ]

Server Security Hardening

Lock down SSH, firewall, patching and access before it costs you. Hourly work is $20/hour; defined projects get a fixed price in writing.

See the service

[ Let's talk ]

Broken, slow, or not built yet?

Describe it in a few lines and you'll get a real reply from the person who'd do the work — same working day, no discovery call required.