A server exposed to the internet starts receiving automated login attempts within minutes of booting. Not because anyone is interested in you — because everything with a public IP address gets scanned continuously. This guide is the first hour of work on a new machine, in the order it should be done.
It assumes a fresh Ubuntu or Debian server that you can currently reach as root over SSH. Notes for Fedora, RHEL, Rocky and Alma are included at each step.
One rule throughout: keep your original SSH session open until you have proved the new one works. Every step below can lock you out if it goes wrong, and an open session is the only thing that lets you undo it.
1. Update everything first
sudo apt update && sudo apt upgrade -y # Debian, Ubuntu
sudo dnf upgrade -y # Fedora, RHEL, Rocky, AlmaServer images are built at a point in time and are usually weeks or months behind. Doing this first means the rest of the work happens on current software. See package management if anything here is unfamiliar.
If the upgrade replaces a kernel, reboot before continuing.
2. Create a user that is not root
sudo adduser kevin # creates the account and home directory
sudo usermod -aG sudo kevin # Debian, Ubuntu
sudo usermod -aG wheel kevin # Fedora, RHEL, Rocky, AlmaWorking as root all the time means every typo runs with full privileges and nothing is attributable to a person. A normal account plus sudo gives you the same power with a pause before the dangerous half.
Note the -a in usermod -aG. Without it you replace the user’s groups rather than adding one.
Confirm it worked before going further:
su - kevin
sudo whoami # should print: root
exit3. Set up SSH keys
This is the step that does most of the work. Passwords can be guessed at thousands of attempts per hour; a key cannot.
On your own machine, not the server:
ssh-keygen -t ed25519 -C "kevin@laptop" # if you do not already have one
ssh-copy-id kevin@your-server-ipGive the key a passphrase. A key with no passphrase is a plain-text credential sitting on your laptop.
Now open a second terminal and test it, leaving the first one connected:
ssh kevin@your-server-ipIf it asks for your account password rather than your key passphrase, the key is not working and you must not continue to the next step. Run ssh -v to see which keys were offered, and check permissions — SSH silently ignores keys that others can read:
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keysThe ssh and file permissions pages cover this in more detail.
4. Lock down SSH
Only once key login is confirmed working. Edit /etc/ssh/sshd_config:
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
AllowUsers kevinOn recent Ubuntu images, settings in /etc/ssh/sshd_config.d/*.conf override the main file — cloud provider images often drop a file there enabling password authentication. Check that directory, or your change will appear to do nothing.
sudo sshd -t # validate the config before applying it
sudo systemctl reload ssh # Debian, Ubuntu
sudo systemctl reload sshd # Fedora, RHEL, Rocky, Almasshd -t catches syntax errors before they take the service down — but it parses, it does not bind. A ListenAddress, Port or HostKey the running sshd cannot actually use passes -t cleanly, and then kills the daemon on reload, because sshd’s SIGHUP handler re-execs it: systemctl reload ssh exits 1, the unit goes to failed, and nothing is left listening on port 22. Then open a third terminal and connect again. Only when that works should you close the original session — that open session is the mitigation for exactly this case, and it is not optional.
Changing the SSH port is often suggested. It reduces log noise from automated scanners and does not make the server meaningfully harder to attack. Disabling password authentication is the change that matters.
5. Turn on a firewall
Allow SSH before enabling the firewall. Getting this backwards disconnects you immediately, and on a cloud VM with no console that is a rebuild.
# Debian, Ubuntu
sudo apt install ufw
sudo ufw allow OpenSSH # do this FIRST
sudo ufw enable
sudo ufw status verbose
# Fedora, RHEL, Rocky, Alma
sudo firewall-cmd --permanent --add-service=ssh
sudo firewall-cmd --reload
sudo firewall-cmd --list-allAdd other services only as you actually need them:
sudo ufw allow 80/tcp
sudo ufw allow 443/tcpRemember that a cloud provider’s security group is a second firewall your machine cannot see. If a port is open in ufw and still unreachable, check the provider’s console. Networking basics covers telling these apart.
6. Automatic security updates
# Debian, Ubuntu
sudo apt install unattended-upgrades
sudo dpkg-reconfigure --priority=low unattended-upgrades
# Fedora, RHEL, Rocky, Alma
sudo dnf install dnf-automatic
sudo systemctl enable --now dnf-automatic.timerUnpatched software is a far more common route in than a guessed password. Restrict it to security updates rather than everything, so a routine upgrade cannot change application behaviour unannounced. On an Ubuntu cloud image, note that unattended-upgrades is already a dependency of the base image and the debconf default is already true — so those two commands confirm a default rather than switching anything on. And because Ubuntu’s security pocket is a copy rather than a superset, restricting installs to it means apt list --upgradable never reaches zero, even on a machine that patches itself every night. That count is correct, and adding -updates to clear it is the single most dangerous change in this subject. The Half of Patching That Happens After the File Is Correct has the arithmetic behind both.
On the Fedora side that takes two more lines, and without them nothing is installed. dnf-automatic ships apply_updates = no: the timer fires nightly, downloads packages into the cache and exits 0, having installed none of them. The two families ship as mirror images — unattended-upgrades is security-only and enabled; dnf-automatic is everything-but-installs-nothing and disabled — so set both keys in /etc/dnf/automatic.conf before enabling the timer:
# /etc/dnf/automatic.conf
[commands]
upgrade_type = security # shipped default is "default" — i.e. everything
apply_updates = yes # shipped default is "no" — i.e. nothing is installed
# then, and only then:
sudo systemctl enable --now dnf-automatic.timer
Automatic updates do not reboot for kernel patches by default — and the usual way of checking is unreliable on exactly the machines most people are running. /var/run/reboot-required is written by update-notifier-common, which is absent from Ubuntu cloud images and from Debian altogether, so a missing file does not mean no reboot is needed; where the file does exist, /var/run/reboot-required.pkgs names which packages asked for it. dnf needs-restarting -r answers from a hard-coded list of about ten package names, and openssl is not among them. The check that always works is comparing what you are running with what is installed — uname -r against the newest /boot/vmlinuz-*.
7. fail2ban, if you like
sudo apt install fail2ban # or: sudo dnf install fail2ban
sudo systemctl enable --now fail2ban
sudo fail2ban-client status sshdfail2ban watches logs and temporarily blocks addresses that fail repeatedly. Worth being honest about what it buys you: with password authentication already disabled, it mostly reduces log noise. The attempts were never going to succeed. It is genuinely useful in front of services that do accept passwords — mail, a web application login — and that is where the effort belongs.
Configure it in /etc/fail2ban/jail.local, never in jail.conf, which package updates overwrite. Add your own address to ignoreip so a fat-fingered login does not lock you out.
8. Check your work
sudo ss -tulpn # what is listening, and what owns it
sudo systemctl --failed # anything broken
sudo journalctl -u ssh --since today | tail -30
sudo ufw status verbose
sudo lastb | head # recent failed logins
id kevin # confirm group membershipss -tulpn is the important one. Every listening port is a way in, and a fresh server often has more open than you expect — a database bound to 0.0.0.0 rather than 127.0.0.1 is a common and serious default. The other one to look for is port 25: Ubuntu ships Postfix with inet_interfaces = all, so answering “Internet Site” to the install prompt leaves a mail daemon listening on every interface, and an open relay is found within hours. If the machine only needs to send mail, set inet_interfaces = loopback-only. See networking basics for reading that output.
Also worth confirming from your laptop rather than the server itself:
ssh root@your-server-ip # should be refused
nc -zv your-server-ip 3306 # a database port should not answerWhat this does and does not do
Worth being clear, because “hardened” is often claimed too readily. The steps above remove the routine attacks: credential guessing, root login, and known vulnerabilities in unpatched packages. That is the large majority of what a small server actually faces.
They do nothing about a vulnerability in the application you are about to install, a compromised dependency, or a leaked key from your own laptop. Security is ongoing rather than a checklist you complete once — which is why backups matter as much as any item above. A server you can rebuild and restore is a bad afternoon; one you cannot is a catastrophe.
Quick checklist
- Update all packages, reboot if the kernel changed
- Create a non-root user and give it sudo
- Install your SSH key and test it in a second terminal
- Disable root login and password authentication;
sshd -tbefore reloading - Allow SSH, then enable the firewall
- Turn on automatic security updates
- fail2ban if you want quieter logs
- Review listening ports with
ss -tulpn - Set up backups — the step everyone postpones
Related
- The Half of Patching That Happens After the File Is Correct — the long one, and what step 6 above turns into once the machine has been running for a while.
- ssh — keys, the config file, and hardening in more depth.
- sudo — granting privileges narrowly rather than all at once.
- Networking basics —
ss -tulpn, and telling a firewall from a bind address. - Package management — updates, and where automatic ones come from.
- systemctl — enabling services and reading their logs.
- File permissions — why SSH ignores a key that is too readable.
