Skip to content
Website Security

How to Audit a New Ubuntu VPS for Security

In this article11 sections

To audit a new Ubuntu VPS, run seven read-only checks before you put anything important on it: confirm that SSH refuses passwords and root logins, that the firewall denies everything you did not open, that no database or admin tool is listening on the public address, that Fail2ban is watching SSH, that security updates install themselves, that only the accounts and keys you expect exist, and that the clock, swap and pending reboots are in order. None of the commands below changes the server, and together they take about 15 minutes.

This guide is for anyone who has just received a VPS, whether a host set it up for you or you installed it yourself. The commands are for Ubuntu 22.04 and 24.04 and work the same on Debian 12. Once the server passes, the next thing to add is a database backup of your own, covered in why a daily VPS backup is not a database backup.

Check 1: What SSH really accepts

Do not read /etc/ssh/sshd_config to answer this. Ubuntu also loads every file in /etc/ssh/sshd_config.d/, and for most settings the first value it reads wins. A cloud image often ships a file there that turns password login back on, even though the main file says no. Ask the SSH server for the settings it is actually using:

sudo sshd -T | grep -Ei '^(permitrootlogin|passwordauthentication|kbdinteractiveauthentication|pubkeyauthentication) '

A hardened server answers like this:

permitrootlogin no
pubkeyauthentication yes
passwordauthentication no
kbdinteractiveauthentication no

permitrootlogin prohibit-password is also acceptable: root can then log in with a key but never with a password. If passwordauthentication says yes, find the file that sets it:

sudo grep -ri passwordauthentication /etc/ssh/sshd_config /etc/ssh/sshd_config.d/

Check 2: The firewall’s default is “deny”

sudo ufw status verbose

Look for three things: Status: active, Default: deny (incoming), and a short list of allowed ports that you can explain. A web server normally needs only 22 (SSH), 80 and 443. Every extra line is a question to answer: who added it, and does anything still use it?

Check 3: Nothing private is listening in public

The firewall is the second line of defence. The first is that private services do not listen on the public address at all.

sudo ss -tulpn | grep -v '127.0.0.' | grep -v '\[::1\]'

This lists every service that accepts connections from outside the server. You should see SSH and your web server, and usually nothing else. If any of these appear, fix them even when the firewall blocks the port:

Port Service Where it should listen
3306 MariaDB or MySQL 127.0.0.1 (bind-address)
5432 PostgreSQL localhost (listen_addresses)
6379 Redis 127.0.0.1 (bind)
27017 MongoDB 127.0.0.1 (bindIp)
5678, 3000, 8080 n8n, Node.js apps, admin tools 127.0.0.1, behind Nginx

Warning. Docker publishes ports by writing its own firewall rules, and those are checked before UFW’s. A container started with -p 5678:5678 is reachable from the internet even when ufw status shows the port as closed. Publish to the loopback address instead: -p 127.0.0.1:5678:5678.

Check 4: Fail2ban is watching SSH

sudo fail2ban-client status sshd

You want a jail named sshd with a Currently banned and Total banned count. A total above zero within hours of going online is normal: every public server is scanned constantly. If the command says the jail does not exist, Fail2ban is installed but not protecting SSH.

Check 5: Security updates install themselves

cat /etc/apt/apt.conf.d/20auto-upgrades
systemctl is-enabled unattended-upgrades
apt list --upgradable 2>/dev/null | grep -c security

The first command should show both lines set to "1", the second should print enabled, and the third should print 0 or a small number on a server that has been running for a day. Then check whether an update is waiting for a restart:

[ -f /var/run/reboot-required ] && cat /var/run/reboot-required.pkgs || echo "no reboot needed"

Automatic updates install a new kernel but never reboot the server for you by default. If a reboot is pending, plan it for a quiet hour.

Check 6: Only the accounts and keys you expect

getent group sudo
sudo awk -F: '$3 == 0 {print $1}' /etc/passwd
sudo sh -c 'for f in /root/.ssh/authorized_keys /home/*/.ssh/authorized_keys; do [ -f "$f" ] && echo "== $f" && cut -d" " -f1,3 "$f"; done'

The first line lists who can become root, the second should print only root, and the third shows the type and comment of every SSH key that can log in. Every key should belong to a person or a system you can name. A key nobody recognises is the single most important thing this audit can find: remove it and ask how it got there.

Check 7: Clock, swap and disk

timedatectl | grep -E 'Time zone|synchronized'
swapon --show
df -h /

The clock must say System clock synchronized: yes, or TLS certificates, logins with one-time codes and log timestamps all misbehave. A small swap file (1 to 2 GB) gives the kernel room during a memory spike instead of killing your database. The root disk should have comfortable free space before you install anything.

What a pass looks like

Check Healthy answer If not
SSH No passwords, no root password login Add a file in sshd_config.d that sorts first, run sudo sshd -t, then reload
Firewall Active, deny incoming, 22/80/443 sudo ufw allow OpenSSH before sudo ufw enable
Listeners Only SSH and the web server are public Bind the service to 127.0.0.1 and restart it
Fail2ban sshd jail active Enable the jail in /etc/fail2ban/jail.local
Updates Both values "1", service enabled sudo dpkg-reconfigure -plow unattended-upgrades
Accounts Known users and known keys only Remove the key, then investigate
Clock and swap Synchronised, swap present sudo timedatectl set-ntp true; add a swap file

Tip. Before you change anything about SSH or the firewall, open a second SSH session and leave it connected. If the change locks you out, the open session still works and you can undo it.

Common mistakes to avoid

  • Trusting the config file instead of the running server. sshd -T and ss -tulpn show what is true now. Files show what someone intended.
  • Assuming the firewall covers Docker. It does not, unless you bind published ports to 127.0.0.1.
  • Running the audit once. Repeat checks 3 and 6 after every new install. Most exposed databases were opened “just for a minute” during setup.
  • Ignoring a pending reboot for months. A patched kernel that is not running protects nothing.
  • Enabling UFW before allowing SSH. This is the classic way to lock yourself out of a new server.

Starting from a hardened server

Every SiteHarbour VPS is handed over with this work already done: a non-root sudo user, SSH key login with root password login disabled, a UFW firewall open only for the ports you use, Fail2ban on SSH, automatic security updates, and the timezone, swap and hostname configured. The audit above is how you confirm that on day one, and how you check that it is still true six months later. The server is unmanaged after handover, so keeping it that way is your job.

Frequently asked questions

How often should I repeat this audit?

Run the full list once a quarter, and run the listener and account checks after you install any new software or give someone access. Both take under a minute.

Is changing the SSH port worth it?

It cuts noise in the logs but adds no real protection, because scanners find the new port quickly. Key-only login is what stops password guessing. If you do change the port, allow the new one in the firewall first.

My host set up the server. Do I still need to check?

Yes. It takes 15 minutes, it tells you exactly what you are starting with, and it gives you a baseline to compare against later.

Why does a closed port still show a service listening?

Listening and reachable are different things. The service accepts connections on that address, and the firewall decides whether packets get to it. Fix the listener as well, so one firewall mistake does not expose your database.

Should I disable root login completely?

Use a normal user with sudo for daily work and set PermitRootLogin no or prohibit-password. Either way, no one should be able to log in as root with a password.

The short version: ask the running server, not the config files. Seven read-only commands tell you whether SSH, the firewall, listeners, Fail2ban, updates, accounts and the clock are as they should be, and repeating two of them after every install keeps them that way.

Share this guide