Every server I provision goes through the same checklist before it touches production. I've refined this over about a decade of working in environments where the cost of getting it wrong is high. This is the current version, running on Ubuntu 22.04 LTS.
I'll explain the reasoning behind each step, not just the commands. If you understand why you're doing something, you can adapt it when your situation is slightly different.
Step 1: Update everything first
Before doing anything else, get the system current:
sudo apt update && sudo apt upgrade -y
sudo apt autoremove -y
Reboot if a kernel update was installed:
[ -f /var/run/reboot-required ] && sudo reboot
After reconnecting, check that you're on the latest kernel:
uname -r
Why: Many attacks exploit known vulnerabilities. A system that's fully patched eliminates a large category of attack surface before you've done anything else.
Step 2: Create a non-root user and lock down root
Never log in as root directly. Create a user for your own access:
sudo adduser michael
sudo usermod -aG sudo michael
Switch to the new user and confirm sudo works:
su - michael
sudo whoami
# should output: root
Why: Root has no restrictions. If someone compromises a root SSH session, they own the machine. A regular user with sudo access is far safer - actions require explicit escalation, which is also logged.
Step 3: Set up SSH key authentication
On your local machine, generate a key if you don't have one:
ssh-keygen -t ed25519 -C "michael@packetandprofit"
Copy your public key to the server:
ssh-copy-id michael@SERVER_IP
Test that key authentication works before proceeding:
ssh michael@SERVER_IP
If you get in without a password prompt, key auth is working.
Step 4: Harden the SSH daemon
Open the SSH config:
sudo nano /etc/ssh/sshd_config
Set or change these values:
Port 2222
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
AuthorizedKeysFile .ssh/authorized_keys
MaxAuthTries 3
LoginGraceTime 20
AllowUsers michael
X11Forwarding no
Changing the port from 22 is a minor measure - it stops automated scanners that only target port 22. It is not a security control on its own. PermitRootLogin no is a real control. PasswordAuthentication no is a real control - with keys only, brute-force password attacks become impossible.
Restart SSH:
sudo systemctl restart sshd
Do not close your current session yet. Open a second terminal and confirm you can still connect:
ssh -p 2222 michael@SERVER_IP
If that works, you can safely close the first terminal.
Step 5: Configure the firewall
Install and configure UFW (Uncomplicated Firewall):
sudo apt install ufw -y
Set default policies - deny everything incoming, allow everything outgoing:
sudo ufw default deny incoming
sudo ufw default allow outgoing
Allow your new SSH port. Do this before enabling UFW or you'll lock yourself out:
sudo ufw allow 2222/tcp comment 'SSH'
Add any other services this server needs to expose. For example, a web server:
sudo ufw allow 80/tcp comment 'HTTP'
sudo ufw allow 443/tcp comment 'HTTPS'
Enable the firewall:
sudo ufw enable
Verify:
sudo ufw status verbose
Why: A firewall blocks attack surface that isn't open yet. Ports that aren't open can't be attacked. Default deny incoming means nothing gets through unless you explicitly allow it.
Step 6: Install and configure fail2ban
Fail2ban watches log files for repeated failed authentication attempts and temporarily bans the source IP. Even with password auth disabled, it's useful as a layer of defence against log noise and certain other attack patterns.
sudo apt install fail2ban -y
Create a local config (never edit the main config directly - it gets overwritten on updates):
sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
sudo nano /etc/fail2ban/jail.local
Find the [sshd] section and configure it:
[sshd]
enabled = true
port = 2222
filter = sshd
logpath = /var/log/auth.log
maxretry = 3
bantime = 3600
findtime = 600
maxretry = 3 means 3 failures within findtime = 600 seconds (10 minutes) triggers a ban. bantime = 3600 bans for 1 hour.
Start and enable fail2ban:
sudo systemctl enable fail2ban
sudo systemctl start fail2ban
Check it's running:
sudo fail2ban-client status sshd
Step 7: Configure automatic security updates
Manual patching is a process that gets skipped. Automate the security updates at minimum:
sudo apt install unattended-upgrades -y
sudo dpkg-reconfigure -plow unattended-upgrades
When prompted, select Yes.
Configure what gets auto-updated:
sudo nano /etc/apt/apt.conf.d/50unattended-upgrades
Make sure these lines are uncommented:
Unattended-Upgrade::Allowed-Origins {
"${distro_id}:${distro_codename}";
"${distro_id}:${distro_codename}-security";
};
Unattended-Upgrade::AutoFixInterruptedDpkg "true";
Unattended-Upgrade::MinimalSteps "true";
Unattended-Upgrade::Remove-Unused-Dependencies "true";
Unattended-Upgrade::Automatic-Reboot "false";
I keep Automatic-Reboot set to false on production servers and reboot manually during maintenance windows. Set it to true if you have redundancy that can handle an unexpected reboot.
Step 8: Configure system logging
Make sure the system logging service is running and logs are persisted to disk:
sudo systemctl enable systemd-journald
sudo nano /etc/systemd/journald.conf
Set:
[Journal]
Storage=persistent
SystemMaxUse=500M
Restart the journal:
sudo systemctl restart systemd-journald
For servers that are part of a larger infrastructure, ship logs to a central aggregator (Splunk, Elasticsearch, etc.). A log that only exists on the compromised server is a log that can be deleted.
Step 9: Disable unused services
Check what's running:
sudo systemctl list-units --type=service --state=running
Disable anything you don't need. Common culprits on a fresh Ubuntu install:
# Avahi (mDNS - usually not needed on a server)
sudo systemctl disable --now avahi-daemon
# Bluetooth (definitely not needed on a server)
sudo systemctl disable --now bluetooth
# cups (printing - not needed)
sudo systemctl disable --now cups
```
Why: Every running service is an attack surface. Services you're not using should not be running.
Step 10: Final checks
Run a quick audit of open ports:
sudo ss -tlnp
Everything in that list should be something you intentionally have running. If you see a port you don't recognise, investigate before moving on.
Check the firewall one more time:
sudo ufw status verbose
Check that no one is logged in besides you:
who
Check recent sudo usage:
sudo grep sudo /var/log/auth.log | tail -20
---
This is a baseline, not a ceiling. Depending on what the server is doing, you may also need to configure AppArmor profiles for specific services, set up intrusion detection (I use AIDE for file integrity monitoring on particularly sensitive hosts), configure audit logging, or apply CIS benchmark controls.
But this checklist gets every new server I deploy to a solid starting state, and it takes about 20 minutes to run through. The 20 minutes is worth it every time.