Bots start probing a new server within minutes of it going online: they brute-force SSH passwords and scan for open services. Below is a short initial server setup for Ubuntu, after which the server is safe to use. Each step ends with a check: do not move on until it passes.
Tested on Ubuntu 24.04 and 26.04. We connect as root, since that is how servers are usually handed over. Replace 203.0.113.10 with your server's address.
1. Update the system
apt update && apt full-upgrade -y
If the update created /var/run/reboot-required, the kernel or important libraries were updated; reboot the server:
[ -f /var/run/reboot-required ] && reboot
2. Create a sudo user
Working as root all the time is dangerous: any mistake in a command runs with full privileges. Create a regular user and allow it to use sudo:
adduser admin # set a password; you will need it for sudo
usermod -aG sudo admin
Copy the key you use to log in as root to the new user, so that it also logs in with a key:
install -d -m 700 -o admin -g admin /home/admin/.ssh
install -m 600 -o admin -g admin /root/.ssh/authorized_keys /home/admin/.ssh/authorized_keys
Check. Without closing the current session, in a new terminal window:
ssh admin@203.0.113.10
sudo whoami # asks for the admin password and prints root
If you have no key login at all yet, set it up first using SSH key authentication.
3. Disable password login and root login over SSH
Once admin can log in with a key and sudo works, disable password authentication and direct root login:
sudo tee /etc/ssh/sshd_config.d/00-hardening.conf > /dev/null <<'EOF'
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin no
EOF
sudo sshd -t && sudo systemctl reload ssh
The 00 prefix makes this file load before the cloud image's 50-cloud-init.conf: SSH uses the first value it finds for each option. Details, and what to do if you get locked out, are in the SSH key article.
Check in a new window: ssh admin@203.0.113.10 lets you in, while ssh root@203.0.113.10 returns Permission denied.
4. Enable the UFW firewall
UFW is a simple front end for the Linux firewall and comes preinstalled on Ubuntu. The key rule: allow SSH first, then enable it, otherwise your current session will drop.
sudo ufw allow OpenSSH # port 22; if you changed the SSH port: sudo ufw allow <port>/tcp
sudo ufw allow 80/tcp # only if the server will host a website
sudo ufw allow 443/tcp
sudo ufw enable # answer y
sudo ufw status verbose
The output should show Status: active, Default: deny (incoming), allow (outgoing) and your rules.
If you plan to run Docker: ports a container publishes with
-p 8080:80are opened by Docker, bypassing UFW. Publish services that should not be reachable from outside on localhost only:-p 127.0.0.1:8080:80. More in the article on Docker Compose in production.
5. Enable automatic security updates
The unattended-upgrades package installs security updates on its own once a day. It is usually already installed; enable it and confirm:
sudo apt install -y unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades # choose "Yes"
cat /etc/apt/apt.conf.d/20auto-upgrades
You should see two lines with the value "1":
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
To see what it would install right now without installing anything, run sudo unattended-upgrade --dry-run --debug.
6. Install fail2ban
fail2ban watches the SSH logs and bans addresses that brute-force passwords. It is useful even with key-only login: less noise in the logs and less load from bots.
sudo apt install -y fail2ban
sudo systemctl enable --now fail2ban
sudo fail2ban-client status sshd
On Ubuntu 24.04 and 26.04, SSH protection is enabled right after installation: the settings are in /etc/fail2ban/jail.d/defaults-debian.conf, and logs are read from the systemd journal. Put your own values in a separate file rather than editing the system ones:
sudo tee /etc/fail2ban/jail.local > /dev/null <<'EOF'
[sshd]
maxretry = 5
bantime = 1h
# if SSH runs on a non-standard port:
# port = 22746
EOF
sudo systemctl restart fail2ban
Do not test banning by connecting from the server itself: by default fail2ban does not ban its own addresses (ignoreself = true). Repeat offenders, scheduled reboots after updates and testing a ban from a second machine are covered in Fail2ban and automatic updates.
7. Time and time zone
timedatectl
The line System clock synchronized: yes means the time is synchronized automatically. Servers are often left on UTC, which makes it easier to compare logs across machines. If you prefer local time, set your own zone, for example:
sudo timedatectl set-timezone Europe/Amsterdam
8. Add a swap file on a low-memory server
On a server with 1–2 GB of RAM, a swap file protects against processes being killed suddenly under peak load, for example during a project build. It is slower than RAM and does not replace it.
sudo fallocate -l 1G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
swapon --show
The last command should show /swapfile with a size of 1G. The line in /etc/fstab turns swap on after a reboot. How to tell that you are running out of memory and swap no longer helps is covered in the article on the OOM killer.
Summary
| What we did | How to check |
|---|---|
| Updated the system | apt list --upgradable shows an empty list |
Sudo user admin |
ssh admin@…, then sudo whoami → root |
| Key-only login, root disabled | sudo sshd -T shows passwordauthentication no and permitrootlogin no |
| Firewall | sudo ufw status verbose |
| Automatic security updates | cat /etc/apt/apt.conf.d/20auto-upgrades |
| fail2ban | sudo fail2ban-client status sshd |
| Time | timedatectl |
| Swap | swapon --show |
Next, whatever you got the server for:
- installing Docker and Docker Compose in production on a single VPS;
- Nginx as a reverse proxy with HTTPS from Let's Encrypt;
- PostgreSQL on Ubuntu;
- deploying Next.js to a VPS (in Russian) and automated deployment from GitHub Actions;
- running a Telegram bot on a server with systemd.
How much memory these workloads need is covered in How much RAM does a VPS need.