Why This Guide Exists
Most Ubuntu hardening tutorials online cover three or four basic things — install fail2ban, enable UFW, disable root SSH login — and call it done. That is not hardening. That is the bare minimum, and it leaves your server wide open to anyone with patience and a port scanner.
This guide is different. It is the actual checklist we run on every production Ubuntu server we manage. Every item here exists for a reason — usually because we got attacked, audited, or scared by something specific. It is opinionated, comprehensive, and entirely based on real production experience.
Run through the whole thing the first time you set up a new server. It takes about an hour. After that, you have something that is genuinely hard to break into.
One note before we start: this guide assumes Ubuntu 22.04 or 24.04 on a VPS or cloud instance, and that you have sudo access from a non-root user. If you are still SSHing as root, fix that first (we will cover it in the SSH section).
1. SSH Hardening — Stop Brute Force at the Door
SSH is the single most attacked service on any internet-facing server. A fresh Ubuntu install with default settings will get hit with brute-force attempts within minutes of going online. Here is how to lock it down properly.
Move SSH to a Non-Standard Port
Port 22 is the first thing every bot on Earth scans. Moving to a non-standard port eliminates 99 percent of automated scan noise. It is not "real security" by itself, but it dramatically cuts log spam and reduces your attack surface.
Pick a port between 1024 and 65535 that is not in common use. Add it to your SSH config:
sudo nano /etc/ssh/sshd_config.d/99-security.conf
Add:
Port 52731
Protocol 2
Important on Ubuntu 24.04: SSH uses systemd socket activation. Just changing sshd_config is not enough. You also need to override the socket:
sudo systemctl edit ssh.socket
Add:
[Socket]
ListenStream=
ListenStream=52731
Then reload and restart:
sudo systemctl daemon-reload
sudo systemctl restart ssh.socket
Critical: Update your firewall to allow the new port before closing port 22, or you will lock yourself out.
Disable Password Auth, Use Keys Only
This is the single most important thing you can do. SSH keys are dramatically more secure than passwords and are immune to brute force entirely.
Generate a key on your local machine:
ssh-keygen -t ed25519 -C "[email protected]"
Copy the public key to your server:
ssh-copy-id -p 52731 youruser@your-server
Then disable password auth in /etc/ssh/sshd_config.d/99-security.conf:
PasswordAuthentication no
PubkeyAuthentication yes
PermitRootLogin no
PermitEmptyPasswords no
Restrict Other SSH Settings
Add these to the same config file:
MaxAuthTries 3
MaxSessions 2
AllowTcpForwarding no
AllowAgentForwarding no
X11Forwarding no
TCPKeepAlive no
ClientAliveInterval 300
ClientAliveCountMax 2
LogLevel VERBOSE
What each does:
- MaxAuthTries 3 — only 3 failed attempts per connection (default 6)
- MaxSessions 2 — limit multiplexed sessions per connection
- AllowTcpForwarding no — prevents SSH tunneling abuse
- AllowAgentForwarding no — reduces attack surface
- X11Forwarding no — not needed on a server, minor attack surface reduction
- TCPKeepAlive no — prevents spoofed keepalive attacks; use ClientAliveInterval instead
- ClientAliveCountMax 2 — disconnect idle sessions faster
- LogLevel VERBOSE — better forensic logs (records key fingerprints used)
Whitelist Your IP If You Can
If you have a static IP (or you use a VPN with a dedicated IP), restrict SSH to just that IP at the firewall level. This is by far the strongest protection — even if SSH had a zero-day vulnerability tomorrow, the attacker could not reach the port. We will cover this in the firewall section.
Test the new config without breaking your existing connection:
sudo sshd -t
sudo systemctl restart ssh
Open a new terminal and try connecting before closing the existing session. Always.
2. UFW Firewall — Deny Everything By Default
The principle: deny everything inbound by default, then explicitly allow only what you need. Same for outbound if you can swing it.
Basic Setup
sudo apt install ufw -y
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 52731/tcp comment "SSH"
sudo ufw enable
Check status:
sudo ufw status verbose
Restrict SSH to Specific IPs
Instead of allowing SSH from anywhere, allow it only from your own IP or VPN:
sudo ufw delete allow 52731/tcp
sudo ufw allow from YOUR_IP_HERE to any port 52731 proto tcp comment "SSH from VPN only"
Allow Web Traffic Only From Cloudflare
If you run web services behind Cloudflare, restrict ports 80 and 443 to Cloudflare's IP ranges only. This prevents anyone from bypassing Cloudflare's WAF/DDoS protection by hitting your server directly.
Cloudflare publishes their IP ranges at cloudflare.com/ips-v4 and cloudflare.com/ips-v6. Loop through them and allow each:
for ip in $(curl -s https://www.cloudflare.com/ips-v4); do
sudo ufw allow from $ip to any port 80,443 proto tcp comment "Cloudflare"
done
for ip in $(curl -s https://www.cloudflare.com/ips-v6); do
sudo ufw allow from $ip to any port 80,443 proto tcp comment "Cloudflare IPv6"
done
Set up a daily cron to refresh these (Cloudflare adds ranges occasionally):
0 4 * * * /usr/local/bin/update-cloudflare-ufw.sh
Lock Down Outbound Too
Default-allow outbound is convenient but means a compromised process can talk to anything. If your server has a known set of outbound needs (apt, NTP, DNS, your cloud provider, your email relay), lock outbound down too:
sudo ufw default deny outgoing
sudo ufw allow out 53 # DNS
sudo ufw allow out 80/tcp # HTTP for apt
sudo ufw allow out 443/tcp # HTTPS
sudo ufw allow out 587/tcp # SMTP submission (email relay)
sudo ufw allow out 123/udp # NTP
sudo ufw allow out on lo # Loopback
Test thoroughly before applying — outbound restrictions can break things in surprising ways.
3. fail2ban — Auto-Ban Brute Forcers
fail2ban watches your logs for failed login attempts and automatically bans the source IP after too many failures. It is the second line of defense after your firewall.
sudo apt install fail2ban -y
sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
sudo nano /etc/fail2ban/jail.local
Recommended Settings
[DEFAULT]
bantime = 1d
findtime = 1d
maxretry = 5
ignoreip = 127.0.0.1/8 ::1 YOUR_TRUSTED_IP
[sshd]
enabled = true
port = 52731
maxretry = 3
bantime = 1d
Add the recidive Jail (Critical)
The default jails ban IPs for a day. The recidive jail watches the fail2ban log itself — if an IP keeps getting banned over and over, it bans them for much longer on all ports. This is what stops persistent attackers cold.
[recidive]
enabled = true
logpath = /var/log/fail2ban.log
banaction = %(banaction_allports)s
bantime = 4w
findtime = 1w
maxretry = 3
How it works: get banned 3 times in a week, you are banned for 4 weeks on every port. This dramatically reduces repeat offenders.
Restart and verify:
sudo systemctl restart fail2ban
sudo fail2ban-client status
sudo fail2ban-client status sshd
4. Automatic Security Updates
The single most important thing you can do to protect against known vulnerabilities is to install patches as soon as they are released. Manual updates do not happen often enough.
sudo apt install unattended-upgrades apt-listchanges -y
sudo dpkg-reconfigure --priority=low unattended-upgrades
Edit the config to enable automatic reboots when needed:
sudo nano /etc/apt/apt.conf.d/50unattended-upgrades
Make sure these lines are uncommented:
Unattended-Upgrade::Allowed-Origins {
"${distro_id}:${distro_codename}-security";
"${distro_id}ESMApps:${distro_codename}-apps-security";
"${distro_id}ESM:${distro_codename}-infra-security";
};
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "03:00";
Unattended-Upgrade::Mail "[email protected]";
Verify it is running:
sudo systemctl status unattended-upgrades
sudo unattended-upgrade --dry-run -v
Weekly Full Upgrades Too
unattended-upgrades only installs security patches. Non-security updates (MariaDB bugfixes, library improvements, etc.) accumulate. Set up a weekly full upgrade with a simple cron:
sudo crontab -e
Add:
30 4 * * 0 DEBIAN_FRONTEND=noninteractive apt update && apt upgrade -y >> /var/log/weekly-upgrade.log 2>&1
5. Kernel Sysctl Hardening
The Linux kernel has dozens of sysctl parameters that affect security. The defaults are reasonable but not optimal. Create a hardening file:
sudo nano /etc/sysctl.d/99-hardening.conf
Add:
# Network hardening
net.ipv4.conf.all.log_martians = 1
net.ipv4.conf.default.log_martians = 1
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.default.send_redirects = 0
net.ipv4.conf.all.accept_source_route = 0
net.ipv4.conf.default.accept_source_route = 0
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.default.accept_redirects = 0
net.ipv4.conf.all.secure_redirects = 0
net.ipv4.conf.default.secure_redirects = 0
net.ipv4.icmp_echo_ignore_broadcasts = 1
net.ipv4.icmp_ignore_bogus_error_responses = 1
net.ipv4.tcp_syncookies = 1
# Kernel hardening
kernel.kptr_restrict = 2
kernel.sysrq = 0
kernel.core_uses_pid = 1
kernel.perf_event_paranoid = 3
kernel.unprivileged_bpf_disabled = 1
net.core.bpf_jit_harden = 2
# Filesystem hardening
fs.protected_fifos = 2
fs.protected_regular = 2
fs.protected_hardlinks = 1
fs.protected_symlinks = 1
fs.suid_dumpable = 0
dev.tty.ldisc_autoload = 0
Apply:
sudo sysctl --system
What these do, briefly: log spoofed packets, drop spoofed packets, reject ICMP redirects, disable source routing, enable SYN cookies (DDoS protection), hide kernel addresses from unprivileged users, disable magic SysRq, restrict perf events, disable unprivileged BPF, and protect against various filesystem attacks.
6. Disable Unnecessary Kernel Modules
Every loaded kernel module is potential attack surface. On a typical server, you do not need filesystem support for old Mac, OS/2, or Apple disks. You do not need Bluetooth or FireWire. You do not need exotic network protocols that have a history of vulnerabilities.
Disable unused filesystem modules:
sudo nano /etc/modprobe.d/disablefs.conf
install cramfs /bin/true
install freevxfs /bin/true
install jffs2 /bin/true
install hfs /bin/true
install hfsplus /bin/true
install udf /bin/true
install squashfs /bin/true
Skip squashfs if you use snap packages.
Disable unused hardware and network protocol modules:
sudo nano /etc/modprobe.d/disablemod.conf
# Hardware not present on a VPS
install bluetooth /bin/true
install firewire-core /bin/true
install usb-storage /bin/true
# Network protocols with CVE history
install dccp /bin/true
install sctp /bin/true
install rds /bin/true
install tipc /bin/true
These take effect after a reboot.
7. Disable rpcbind and Other Unnecessary Services
Fresh Ubuntu installs sometimes include services you do not need. rpcbind in particular listens on port 111 and is a common reflection attack vector.
sudo systemctl disable rpcbind --now
sudo systemctl mask rpcbind
sudo systemctl mask rpcbind.socket
Check what else is listening:
sudo ss -tlnp
Anything you do not need? Disable it. The smaller your attack surface, the better.
8. Harden /dev/shm
Shared memory (/dev/shm) is a tmpfs filesystem that processes can use to communicate. Attackers love it because it is world-writable and they can drop binaries there to execute. Lock it down:
sudo nano /etc/fstab
Add this line:
tmpfs /dev/shm tmpfs defaults,noexec,nosuid,nodev 0 0
Apply without reboot:
sudo mount -o remount /dev/shm
What this does: noexec prevents executing binaries from /dev/shm (blocks dropped malware), nosuid ignores setuid bits (blocks privilege escalation), nodev prevents device file creation.
This is safe for almost all servers — PostgreSQL uses /dev/shm for data only, not code execution.
9. AIDE — File Integrity Monitoring
AIDE (Advanced Intrusion Detection Environment) monitors system binaries and config files for tampering. If anything changes that should not, you get an alert. This is how you detect a successful compromise after the fact.
sudo apt install aide aide-common -y
sudo aideinit
sudo cp /var/lib/aide/aide.db.new /var/lib/aide/aide.db
That builds the initial database. Now schedule daily checks. AIDE has a built-in systemd timer:
sudo systemctl enable --now dailyaidecheck.timer
sudo systemctl list-timers | grep aide
Edit /etc/default/aide to set the email address for reports.
What to expect: the first few days will have lots of "changes" as you tune what to watch. Edit /etc/aide/aide.conf to exclude paths that change normally (like /var/log). Once tuned, you should see almost no changes day to day.
10. auditd — Kernel-Level Audit Logging
AIDE catches file changes after they happen. auditd catches them as they happen, with the kernel's help. It can also log specific syscalls, file accesses, and command executions.
sudo apt install auditd audispd-plugins -y
Create a custom rules file:
sudo nano /etc/audit/rules.d/custom.rules
Add:
# Identity changes
-w /etc/passwd -p wa -k identity
-w /etc/shadow -p wa -k identity
-w /etc/group -p wa -k identity
-w /etc/gshadow -p wa -k identity
# Sudo and authentication
-w /etc/sudoers -p wa -k sudo_changes
-w /etc/sudoers.d/ -p wa -k sudo_changes
# SSH config and keys
-w /etc/ssh/sshd_config -p wa -k ssh_config
-w /etc/ssh/sshd_config.d/ -p wa -k ssh_config
-w /root/.ssh -p wa -k ssh_keys
-w /home -p wa -k home_changes
# Suspicious execution paths
-w /tmp -p x -k tmp_exec
-w /var/tmp -p x -k tmp_exec
-w /dev/shm -p x -k shm_exec
# Cron and systemd
-w /etc/cron.d/ -p wa -k cron_changes
-w /etc/cron.daily/ -p wa -k cron_changes
-w /etc/cron.hourly/ -p wa -k cron_changes
-w /etc/crontab -p wa -k cron_changes
-w /etc/systemd/ -p wa -k systemd_changes
# Privileged commands
-a always,exit -F arch=b64 -S execve -F euid=0 -k root_commands
Apply:
sudo augenrules --load
sudo systemctl restart auditd
sudo auditctl -l
Search the audit log:
sudo ausearch -k identity --start today
sudo ausearch -k ssh_keys
sudo aureport
11. Process Accounting (acct)
auditd watches specific files. acct logs every command run by every user. It is invaluable for post-incident forensics — if someone gets in, you can see exactly what they ran.
sudo apt install acct -y
sudo systemctl enable --now acct
Useful commands:
lastcomm # recent commands by all users
lastcomm yourusername # commands by a specific user
sa # summary by command, with CPU usage
ac # login time accounting per user
12. Antivirus and Rootkit Scanning
Linux malware exists. It is rarer than Windows malware, but it is real. Run nightly scans.
ClamAV (Antivirus)
sudo apt install clamav clamav-daemon -y
sudo systemctl enable --now clamav-freshclam
Run a manual scan:
sudo clamscan -r --infected /var/www
sudo clamscan -r --infected /home
sudo clamscan -r --infected /tmp
rkhunter (Rootkit Hunter)
sudo apt install rkhunter -y
sudo rkhunter --update
sudo rkhunter --propupd # set baseline
sudo rkhunter --check --skip-keypress
Maldet (Linux Malware Detect)
Maldet specifically targets PHP-based malware and web shells — useful if you run WordPress or other PHP apps.
cd /tmp
wget http://www.rfxn.com/downloads/maldetect-current.tar.gz
tar xzf maldetect-current.tar.gz
cd maldetect-*
sudo ./install.sh
sudo maldet -a /var/www
Schedule Nightly Scans
Create a script at /usr/local/bin/nightly-security-scan.sh:
#!/bin/bash
LOG="/var/log/security-scan.log"
echo "===== Scan started: $(date) =====" >> $LOG
clamscan -r --infected --quiet /var/www /home 2>&1 | tee -a $LOG
rkhunter --check --skip-keypress --report-warnings-only 2>&1 | tee -a $LOG
maldet -b -r /var/www 2>&1 | tee -a $LOG
echo "===== Scan finished: $(date) =====" >> $LOG
Make it executable and schedule:
sudo chmod +x /usr/local/bin/nightly-security-scan.sh
echo "0 3 * * * root /usr/local/bin/nightly-security-scan.sh" | sudo tee /etc/cron.d/security-scan
13. Real-Time File Change Monitoring
For directories that should rarely change (like web app code, config files, or system binaries), set up real-time monitoring with inotify-tools. You get an email the instant something changes.
sudo apt install inotify-tools -y
Create a script that watches specific paths and emails alerts:
#!/bin/bash
# /usr/local/bin/file-change-monitor.sh
WATCH_DIRS=(
"/var/www/yoursite.com"
"/etc/nginx"
"/etc/ssh"
)
EMAIL="[email protected]"
inotifywait -m -r -e create,modify,delete,move \
--exclude '(node_modules|\.git|\.next|cache|logs|\.tmp|\.swp)' \
"${WATCH_DIRS[@]}" |
while read path action file; do
echo "$(date): $action $path$file" | mail -s "[FILE CHANGE] $action $file" $EMAIL
done
Run it under systemd as a service so it survives reboots and restarts on crash. The key thing for production: batch the emails. A WordPress plugin install can fire 1,000 inotify events. Build in a 60-second batch window so you get one summary email instead of an inbox flood.
14. SSH Login Notifications
Get an email every time someone successfully logs into your server. If you ever see one you do not recognize, you know within minutes.
Create /usr/local/bin/login-notify.sh:
#!/bin/bash
if [ "$PAM_TYPE" != "open_session" ]; then exit 0; fi
# Skip notifications from trusted IPs
case $PAM_RHOST in
YOUR_TRUSTED_IP) exit 0 ;;
esac
{
echo "User: $PAM_USER"
echo "From: $PAM_RHOST"
echo "Time: $(date)"
} | mail -s "[SSH LOGIN] $PAM_USER from $PAM_RHOST" [email protected]
Make executable and add to PAM:
sudo chmod +x /usr/local/bin/login-notify.sh
echo "session optional pam_exec.so /usr/local/bin/login-notify.sh" | sudo tee -a /etc/pam.d/sshd
15. Restrict sudo to a Whitelist
Default Ubuntu setups give your user unlimited sudo access. If your account ever gets compromised — supply chain attack, stolen SSH key, anything — the attacker becomes root immediately.
Better: remove your user from the sudo group and create a whitelist of specific commands they can run.
sudo deluser youruser sudo
sudo nano /etc/sudoers.d/youruser
Add (chmod 440 after):
# Commands allowed without password
youruser ALL=(ALL) NOPASSWD: /bin/systemctl restart nginx
youruser ALL=(ALL) NOPASSWD: /bin/systemctl reload nginx
youruser ALL=(ALL) NOPASSWD: /usr/sbin/nginx -t
youruser ALL=(ALL) NOPASSWD: /usr/bin/ufw status
youruser ALL=(ALL) NOPASSWD: /usr/bin/fail2ban-client status
# Commands allowed with password
youruser ALL=(ALL) /usr/bin/apt update
youruser ALL=(ALL) /usr/bin/apt install *
youruser ALL=(ALL) /usr/bin/apt upgrade
# Block dangerous commands even with password
youruser ALL=(ALL) !/bin/bash, !/bin/sh, !/usr/bin/sudo *
Set permissions:
sudo chmod 440 /etc/sudoers.d/youruser
sudo visudo -c
Critical: always have a way to recover root access via your cloud provider's console in case your sudo whitelist is too restrictive. Test before logging out.
16. Restrict Cron Access
By default, any user can create a cron job. On a server with multiple service accounts, that is more attack surface than you need.
sudo nano /etc/cron.allow
Add only the users that need cron:
root
youruser
Now any other user (postgres, www-data, etc.) cannot create cron jobs.
17. Logwatch — Daily Log Summary
Reading raw log files every day is unrealistic. logwatch summarizes everything important from yesterday into a single email.
sudo apt install logwatch -y
Run it daily via cron:
0 4 * * * /usr/sbin/logwatch --output mail --mailto [email protected] --detail Med --range yesterday
You get a daily email covering: SSH logins, sudo usage, disk space, service status, errors, package changes, fail2ban activity, and more. Skim it in 30 seconds. If something looks weird, dig in.
18. Disk Space Monitoring
Servers that fill up their disk fail in extremely annoying ways. Monitor disk usage and get alerts before it becomes a problem.
Create /usr/local/bin/disk-alert.sh:
#!/bin/bash
THRESHOLD=80
EMAIL="[email protected]"
USAGE=$(df / | tail -1 | awk '{print $5}' | sed 's/%//')
if [ $USAGE -gt $THRESHOLD ]; then
echo "Disk usage on / is ${USAGE}%" | mail -s "[DISK ALERT] $(hostname)" $EMAIL
fi
Schedule daily:
0 5 * * * root /usr/local/bin/disk-alert.sh
19. Run a Security Audit With Lynis
After all of the above, validate your work with Lynis — a comprehensive security auditing tool. It checks hundreds of items and gives you a hardening score plus a list of remaining issues.
sudo apt install lynis -y
sudo lynis audit system
Aim for a hardening index above 80. Out of the box, a fresh Ubuntu install scores around 60-65. After all the hardening in this guide, you should be in the high 80s or low 90s.
Schedule weekly audits:
0 4 * * 0 /usr/sbin/lynis audit system --quiet --auditor "weekly-cron" >> /var/log/lynis-weekly.log 2>&1
20. Set Up Email Alerts the Right Way
Almost everything in this guide assumes you have working outbound email. Without it, all the alerting and monitoring is useless.
The best modern approach: install Postfix as a local SMTP relay that forwards mail to a service like AWS SES, Mailgun, or SendGrid. Your scripts use the local mail command, which talks to localhost:25, which Postfix relays to your provider with proper authentication.
sudo apt install postfix mailutils libsasl2-modules -y
Choose "Internet Site" during the prompt, then configure as a relay in /etc/postfix/main.cf:
relayhost = [email-smtp.us-east-2.amazonaws.com]:587
smtp_sasl_auth_enable = yes
smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd
smtp_sasl_security_options = noanonymous
smtp_tls_security_level = encrypt
inet_interfaces = loopback-only
Add credentials to /etc/postfix/sasl_passwd:
[email-smtp.us-east-2.amazonaws.com]:587 AKIA...:secret
Then:
sudo postmap /etc/postfix/sasl_passwd
sudo chmod 600 /etc/postfix/sasl_passwd /etc/postfix/sasl_passwd.db
sudo systemctl restart postfix
Test:
echo "test" | mail -s "test from server" [email protected]
The Quick Reference Checklist
Run through this list when setting up any new Ubuntu server. Check each item off as you go.
SSH
- Move SSH to a non-standard port
- Disable password authentication
- Disable root SSH login
- Set MaxAuthTries to 3
- Disable TCP and agent forwarding
- Disable X11 forwarding
- Set LogLevel to VERBOSE
- Restrict to specific source IPs at the firewall level (if possible)
Firewall
- Install UFW
- Default deny inbound
- Allow only SSH and required service ports
- Restrict web ports to Cloudflare IPs (if applicable)
- Consider default-deny outbound for high-security setups
Authentication
- Install fail2ban with sshd jail
- Enable recidive jail with all-ports ban
- Add trusted IPs to ignoreip
- Restrict sudo to a whitelist of commands
- Set up SSH login notifications
Updates
- Install unattended-upgrades
- Enable automatic reboots for critical updates
- Set up weekly full apt upgrade cron
- Configure update email notifications
Kernel and Services
- Apply sysctl hardening (network, kernel, filesystem)
- Disable unused kernel modules
- Disable rpcbind
- Harden /dev/shm (noexec, nosuid, nodev)
- Restrict cron access via /etc/cron.allow
Monitoring and Detection
- Install AIDE for file integrity
- Configure auditd with custom rules
- Install acct for process accounting
- Install ClamAV with daily scans
- Install rkhunter with set baseline
- Set up real-time file change monitoring with inotifywait
- Install Logwatch for daily log summaries
- Set up disk space alerts
Email and Alerts
- Configure Postfix as relay to AWS SES (or similar)
- Test outbound email from server
- Verify all alert scripts can send mail
Final Validation
- Run Lynis audit, aim for hardening index 80+
- Test SSH login from a fresh terminal
- Verify fail2ban is banning attempts
- Confirm at least one daily summary email arrives
- Document everything you changed for the next person
The Bottom Line
None of this is exotic. Every item in this guide is standard practice in any serious production environment. The reason most servers do not have it is that nobody walks new admins through the whole list at once — they pick up pieces over time, usually after something breaks.
If you do all of this on a new Ubuntu server, you have something that is genuinely hard to compromise. Not impossible — nothing is — but hard enough that the vast majority of attackers will move on to easier targets. And easier targets are almost everywhere.
Bookmark this. Run through it for every new server. Update your existing servers section by section if they are not already hardened. The hour you spend on this is the most leveraged hour of work you will ever do on a production system.