Initial Server Setup on Ubuntu 20.04
20.04 has been out long enough for the first point release, so it's my default for new servers now. It brings OpenSSH 8.2, Python 3.8 and kernel 5.4 with WireGuard built in; ufw, netplan and systemd-timesyncd behave as on 18.04. My checklist barely changed, but I rewrote it anyway because I kept looking up the SSH option names.
Here it is: sudo user, key-only SSH, firewall, hostname and time, automatic security updates, and a few extras I now do on every box. You need a fresh 20.04 server with root access and an SSH key on your workstation (ssh-keygen -t ed25519 if you don't have one).
Update first
apt update && apt full-upgrade -y
apt install -y vim curl htop net-tools
[ -f /var/run/reboot-required ] && reboot
Admin user
adduser deploy
usermod -aG sudo deploy
While still root, copy your key to the new user rather than typing it in:
rsync --archive --chown=deploy:deploy ~/.ssh /home/deploy
That preserves the 700/600 permissions and sets ownership in one step. It assumes root's authorized_keys already holds your key, which is how most cloud providers provision the image.
SSH hardening
Edit /etc/ssh/sshd_config:
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
X11Forwarding no
ClientAliveInterval 300
ClientAliveCountMax 2
On OpenSSH 8.2 ChallengeResponseAuthentication still works but KbdInteractiveAuthentication is the current name. Also note that 20.04's OpenSSH refuses ssh-rsa signatures using SHA-1 by default in newer builds, so very old clients with RSA keys may need updating; ed25519 keys sidestep the issue.
sshd -t && systemctl restart ssh
Confirm ssh deploy@203.0.113.60 works in a new window before closing the root session.
Firewall
ufw allow OpenSSH
ufw enable
ufw status verbose
Hostname and time
hostnamectl set-hostname app02.example.com
echo "127.0.1.1 app02.example.com app02" >> /etc/hosts
timedatectl set-timezone Europe/Berlin
timedatectl
systemd-timesyncd is active by default; timedatectl should report System clock synchronized: yes. timedatectl timesync-status (new since 18.04) shows the server it is using and the current offset.
Unattended security updates
apt install -y unattended-upgrades
dpkg-reconfigure -plow unattended-upgrades
Then in /etc/apt/apt.conf.d/50unattended-upgrades, uncomment and set:
Unattended-Upgrade::Remove-Unused-Dependencies "true";
Unattended-Upgrade::Automatic-Reboot "false";
Unattended-Upgrade::Mail "admin@example.com";
I leave automatic reboots off on servers and schedule them myself; /var/run/reboot-required and the Mail option tell me when one is due. A surprise reboot at the wrong moment is worse than a delayed patch.
Small extras
Swap is not created by most cloud images; a small swap file keeps the OOM killer at bay on a 1 GB instance:
fallocate -l 1G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
echo '/swapfile none swap sw 0 0' >> /etc/fstab
sysctl vm.swappiness=10
echo 'vm.swappiness=10' > /etc/sysctl.d/99-swappiness.conf
Snap is preinstalled and lxd is often seeded as a snap you will never use. If the host will not use snaps, snap list and remove what is there; otherwise leave it.
Final check
sudo -v
sudo ufw status
sudo sshd -T | grep -iE 'permitrootlogin|passwordauthentication|kbdinteractive'
timedatectl | grep -E 'Time zone|synchronized'
swapon --show
From your workstation, ssh root@203.0.113.60 should return Permission denied (publickey).
Notes
sign_and_send_pubkey: no mutual signature supportedwhen connecting from an older workstation. The client is offering an RSA key with SHA-1 and the server refuses. Update the client's OpenSSH or generate an ed25519 key.sudo: unable to resolve host app02.example.com: Name or service not known. The/etc/hostsline from the hostname section is missing.ufw enableprintsERROR: problem running ufw-init. Usually a leftover iptables rule from a provider agent or Docker.ufw disable,iptables -F, andufw enableagain resolves it on a fresh host.fallocate: fallocate failed: Operation not supportedon some filesystems. Usedd if=/dev/zero of=/swapfile bs=1M count=1024instead, then continue withchmod,mkswapandswapon.
Under twenty minutes, and later posts on a "fresh Ubuntu 20.04 server" assume this baseline.