Basic Server Hardening

Linux · Lesson 30 of 32 8 min read Updated

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:

Terminal
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
Output
*** 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:

Terminal
adduser priya
usermod -aG sudo priya
rsync --archive --chown=priya:priya /root/.ssh /home/priya

Now 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:

Terminal
sudo nano /etc/ssh/sshd_config.d/01-hardening.conf
Text
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
MaxAuthTries 3
AllowUsers priya

This 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:

Terminal
sudo sshd -t
sudo sshd -T | grep -E '^(permitrootlogin|passwordauthentication|allowusers)'
sudo systemctl reload ssh
Output
permitrootlogin 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:

Terminal
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 verbose
Output
Command 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):

Terminal
sudo apt install fail2ban
sudo nano /etc/fail2ban/jail.local
Text
[DEFAULT]
bantime = 1h
findtime = 10m
maxretry = 5

[sshd]
enabled = true

This bans an address for one hour after five failures within ten minutes. Restart it and check the SSH "jail":

Terminal
sudo systemctl restart fail2ban
sudo fail2ban-client status sshd
Output
Status 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 -tulpn lists 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.1 only.
  • Use AppArmor. Ubuntu enables this security module by default; it confines programs such as nginx to the files they need. sudo aa-status shows what is protected.
  • Watch the logs. Check journalctl -p err -b, /var/log/auth.log and fail2ban-client status regularly.
  • 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, then sudo lynis audit system) reviews your settings and suggests improvements.

Common mistakes

  • Enabling ufw before allowing SSH. You cut off your own connection. Allow OpenSSH first.
  • 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-upgrades on; reboot when /var/run/reboot-required exists.
  • Log in as a normal sudo user, never directly as root.
  • In /etc/ssh/sshd_config.d/, set PermitRootLogin no and PasswordAuthentication no; test with sshd -t and sshd -T.
  • ufw denies incoming traffic by default; allow OpenSSH before ufw 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
Terminal
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 sshd
Output
Status 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?

2. Which SSH setting turns off password logins?

3. What does fail2ban do?

4. Which command shows the SSH settings the server will really use?

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