Basic Server Hardening
A new cloud server is attacked within minutes of getting a public IP address. Nobody is targeting you personally: automated bots scan the whole internet, trying common passwords on SSH and looking for outdated software. Hardening means reducing what an attacker can reach and making the remaining doors strong.
This lesson is a practical checklist for your own servers: keep software updated, stop using root and passwords, close every port you do not need, block repeated login failures, and keep an eye on the logs. Do these steps on every server you create, in this order, ideally in the first ten minutes.
Step 1: update, and keep updating
Most break-ins use security holes that were fixed long ago in software nobody updated. Update immediately, then make sure security updates keep installing by themselves:
sudo apt update && sudo apt full-upgrade -y
sudo apt install unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades
cat /var/run/reboot-required 2> /dev/null*** System restart required ***
unattended-upgrades installs security updates automatically every day; Ubuntu Server enables it by default, and the dpkg-reconfigure step confirms it is switched on. Kernel updates need a reboot to take effect; the file /var/run/reboot-required appears when one is waiting, so check it and reboot at a quiet time. Ubuntu Pro (free for personal use on a few machines) adds Livepatch, which applies many kernel fixes without rebooting.
Step 2: a normal user with sudo
Cloud providers often give you only root. Create your own user, give it sudo rights, and copy root's authorised SSH key to it, so you can log in as yourself:
adduser priya
usermod -aG sudo priya
rsync --archive --chown=priya:priya /root/.ssh /home/priyaNow open a second terminal and check that ssh priya@203.0.113.10 works and that sudo whoami prints root. Only then continue. Working as a normal user means a typo or a compromised program does not automatically have full control.
Step 3: key-only SSH, no root login
Next, change the SSH server's settings. Rather than editing the main /etc/ssh/sshd_config, put your changes in a small file in /etc/ssh/sshd_config.d/, which package updates never overwrite. For SSH, the first value read for a setting wins, and files in that folder are read in name order before the main file, so give yours a low number:
sudo nano /etc/ssh/sshd_config.d/01-hardening.confPermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
MaxAuthTries 3
AllowUsers priyaThis blocks root logins, turns off passwords entirely (only keys work), allows three attempts per connection and permits only the user priya. Test the configuration, check the values sshd will actually use, and reload:
sudo sshd -t
sudo sshd -T | grep -E '^(permitrootlogin|passwordauthentication|allowusers)'
sudo systemctl reload sshpermitrootlogin no passwordauthentication no allowusers priya
sshd -t prints nothing when the syntax is fine. sshd -T prints the effective settings, which catches the case where another file (for example one written by cloud-init) sets PasswordAuthentication yes first.
Step 4: a firewall with ufw
A firewall decides which network connections are allowed in. Ubuntu's friendly tool is ufw (Uncomplicated Firewall), which writes rules for the kernel's built-in firewall. The idea is "deny everything incoming, then allow only what you need". Always allow SSH before enabling it:
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable
sudo ufw status verboseCommand may disrupt existing ssh connections. Proceed with operation (y|n)? y Firewall is active and enabled on system startup Status: active Logging: on (low) Default: deny (incoming), allow (outgoing), disabled (routed) New profiles: skip To Action From -- ------ ---- 22/tcp (OpenSSH) ALLOW IN Anywhere 80,443/tcp (Nginx Full) ALLOW IN Anywhere 22/tcp (OpenSSH (v6)) ALLOW IN Anywhere (v6) 80,443/tcp (Nginx Full (v6)) ALLOW IN Anywhere (v6)
OpenSSH and Nginx Full are application profiles installed by those packages (sudo ufw app list shows them); you can also allow ports directly, as in sudo ufw allow 443/tcp. Allow a port only from one address with sudo ufw allow from 203.0.113.7 to any port 3306. Remove a rule with sudo ufw delete allow 'Nginx Full'. Many cloud providers also have their own network firewall in the control panel; use both.
Step 5: fail2ban against brute force
Even with passwords disabled, bots fill your logs. fail2ban reads log files, and when one IP address fails too often, it adds a temporary firewall ban. Install it and create a local settings file (never edit jail.conf, which updates replace):
sudo apt install fail2ban
sudo nano /etc/fail2ban/jail.local[DEFAULT]
bantime = 1h
findtime = 10m
maxretry = 5
[sshd]
enabled = trueThis bans an address for one hour after five failures within ten minutes. Restart it and check the SSH "jail":
sudo systemctl restart fail2ban
sudo fail2ban-client status sshdStatus for the jail: sshd |- Filter | |- Currently failed: 2 | |- Total failed: 57 | `- File list: /var/log/auth.log `- Actions |- Currently banned: 1 |- Total banned: 4 `- Banned IP list: 192.0.2.55
If you ban yourself by mistake, unban from another connection with sudo fail2ban-client set sshd unbanip 203.0.113.7. fail2ban has jails for nginx, mail servers and more. Recent OpenSSH versions also slow down misbehaving clients on their own (the PerSourcePenalties feature), but fail2ban adds firewall-level blocking and covers other services.
Step 6: less software, regular checks
- Remove what you do not use.
sudo ss -tulpnlists listening services; stop and disable anything unexpected, and uninstall it. - Bind private services to localhost. Databases and admin panels should listen on
127.0.0.1only. - Use AppArmor. Ubuntu enables this security module by default; it confines programs such as nginx to the files they need.
sudo aa-statusshows what is protected. - Watch the logs. Check
journalctl -p err -b,/var/log/auth.logandfail2ban-client statusregularly. - Keep backups off the server, and test restoring them. Hardening reduces risk; backups are what save you when something still goes wrong.
- Audit occasionally. The open-source tool Lynis (
sudo apt install lynis, thensudo lynis audit system) reviews your settings and suggests improvements.
Common mistakes
- Enabling ufw before allowing SSH. You cut off your own connection. Allow
OpenSSHfirst. - Disabling passwords before testing key login. Test in a second terminal first.
- Editing settings that another file overrides. Check effective SSH settings with
sshd -T. - "Security by obscurity" alone. Moving SSH to another port reduces log noise but is not protection; keys, updates and a firewall are.
- Never rebooting after kernel updates. The fixes are installed but not running.
Key points
- Update immediately and keep
unattended-upgradeson; reboot when/var/run/reboot-requiredexists. - Log in as a normal sudo user, never directly as root.
- In
/etc/ssh/sshd_config.d/, setPermitRootLogin noandPasswordAuthentication no; test withsshd -tandsshd -T. ufwdenies incoming traffic by default; allowOpenSSHbeforeufw enable, then only the ports you need.- fail2ban bans IPs that fail repeatedly; check it with
fail2ban-client status sshd.
Exercise
You have just created an Ubuntu 26.04 VPS and can log in as root with an SSH key. Write the complete sequence of commands to: update it, create user kabir with sudo and your key, disable root and password SSH logins, allow only SSH and HTTPS through the firewall, and turn on fail2ban for SSH. Mark where you must test from a second terminal.
Show solution
apt update && apt full-upgrade -y
adduser kabir
usermod -aG sudo kabir
rsync --archive --chown=kabir:kabir /root/.ssh /home/kabir
# TEST: in a second terminal, ssh kabir@SERVER and run sudo whoami
printf 'PermitRootLogin no\nPasswordAuthentication no\nKbdInteractiveAuthentication no\nAllowUsers kabir\n' | sudo tee /etc/ssh/sshd_config.d/01-hardening.conf
sudo sshd -t && sudo systemctl reload ssh
# TEST: a new ssh kabir@SERVER login still works; ssh root@SERVER is refused
sudo ufw allow OpenSSH
sudo ufw allow 443/tcp
sudo ufw enable
sudo apt install -y fail2ban
printf '[sshd]\nenabled = true\nmaxretry = 5\nbantime = 1h\n' | sudo tee /etc/fail2ban/jail.local
sudo systemctl restart fail2ban
sudo fail2ban-client status sshdStatus for the jail: sshd |- Filter | |- Currently failed: 0 | |- Total failed: 0 | `- File list: /var/log/auth.log `- Actions |- Currently banned: 0 |- Total banned: 0 `- Banned IP list:
Each risky step (SSH settings, firewall) comes after a tested way back in. sshd -t && reloads SSH only if the config is valid, and SSH is allowed in ufw before the firewall is switched on.
Quick Quiz
1. Why should ufw allow OpenSSH come before ufw enable on a remote server?
The default policy denies incoming connections, including SSH, unless it is allowed first.
2. Which SSH setting turns off password logins?
With PasswordAuthentication no, only keys (and other non-password methods) can log in.
3. What does fail2ban do?
fail2ban watches logs and adds firewall bans after repeated failures.
4. Which command shows the SSH settings the server will really use?
-T prints the effective configuration after all files are read; -t only checks syntax.
Learn Linux with a mentor
Free lessons get you started. Our Ethical Hacking & Cyber Security training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
