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 -Tandss -tulpnshow 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.