A security audit answers a simple question: does the server look the way you think it does? Over time packages get installed, users get added, ports get opened for a test and never closed. This checklist walks through a manual audit of an Ubuntu 24.04 server, area by area, with the command to run, what a healthy result looks like and what to do when it does not. It works equally well for a first review of a server you inherited and as a periodic check.

Prerequisites

To follow this checklist you need:

  • A server running Ubuntu 24.04 LTS, for example a CubePath VPS. Most commands also work on Debian 12.
  • A user with sudo privileges.
  • A list of what the server is supposed to run (services, users, open ports). The audit is about finding differences from that list.

The checks only read the system, apart from installing the debsums package in Step 8. The few fixes shown along the way are optional: apply them after the audit, one at a time, and test each change.

Step 1 - Preparing a place for the results

Save the output of each check, so you can attach it to your notes and compare it with the next audit. Create a dated directory that only you can read:

mkdir -p ~/audit-$(date +%F)
chmod 700 ~/audit-$(date +%F)
cd ~/audit-$(date +%F)

When a command's output is worth keeping, pipe it through tee, for example sudo ss -tulpn | tee ports.txt. Record the basic system facts first:

hostnamectl | tee system.txt
uptime | tee -a system.txt

Step 2 - Updates and reboots

Unpatched software is the most common way servers get compromised, so start here. Refresh the package lists and see what is pending:

sudo apt update
apt list --upgradable

Healthy result: no packages, or only a few released in the last days. A long list means updates are not being applied.

Check whether a reboot is required to load a new kernel or libraries:

cat /var/run/reboot-required 2>/dev/null || echo "No reboot required"

Verify that automatic security updates are enabled:

cat /etc/apt/apt.conf.d/20auto-upgrades
systemctl is-active unattended-upgrades
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
active

If the file is missing or the values are "0", install and enable the feature with sudo apt install unattended-upgrades followed by sudo dpkg-reconfigure -plow unattended-upgrades.

Step 3 - Users and privileges

Accounts with UID 0

Only root should have user ID 0. Any other account with UID 0 is a full root account under a different name:

awk -F: '$3 == 0 {print $1}' /etc/passwd
root

Accounts that can log in

List the accounts that have a real login shell:

grep -Ev '(/usr/sbin/nologin|/bin/false|/usr/bin/false)$' /etc/passwd | cut -d: -f1,3,7
root:0:/bin/bash
sync:4:/bin/sync
your_user:1000:/bin/bash

Every human account on the list should belong to someone who still needs access. System accounts (UID below 1000) other than root and sync should not have a shell.

Empty passwords and locked accounts

No account should have an empty password field:

sudo awk -F: '$2 == "" {print $1}' /etc/shadow

Healthy result: no output. Check the password status of each human account:

sudo passwd -S -a | awk '$2 == "P"'

This lists accounts with a usable password (P). Accounts that log in only with SSH keys can have their password locked with sudo passwd -l username.

Sudo access

List members of the sudo group and any extra rules in the sudoers files:

getent group sudo
sudo grep -rEv '^\s*(#|$)' /etc/sudoers /etc/sudoers.d/

Look for users that should not be administrators and for NOPASSWD rules. NOPASSWD is acceptable for a dedicated automation account with a narrow command list, not for general users.

Recent logins

Review who logged in recently and from where:

last -n 20 -a

Check failed login attempts, which are recorded in /var/log/btmp:

sudo lastb -n 20 -a

Unknown source addresses in last, or successful logins at unusual times, deserve a closer look.

Step 4 - SSH configuration

The configuration files can include each other, so audit the effective configuration that the SSH daemon actually uses:

sudo sshd -T | grep -Ei '^(permitrootlogin|passwordauthentication|kbdinteractiveauthentication|pubkeyauthentication|permitemptypasswords|maxauthtries|x11forwarding|allowusers|allowgroups) '
permitrootlogin without-password
pubkeyauthentication yes
passwordauthentication no
kbdinteractiveauthentication no
permitemptypasswords no
maxauthtries 3
x11forwarding no

Recommended values:

OptionRecommended
permitrootloginno (or without-password if you really need key-only root access)
passwordauthenticationno, once every user has a working SSH key
kbdinteractiveauthenticationno
permitemptypasswordsno
maxauthtries3 to 4
x11forwardingno on servers

Next, review which SSH keys can log in. Every key in these files grants access:

sudo find /root /home -name authorized_keys -exec sh -c 'echo "== $1"; cat "$1"' _ {} \;

Remove keys you cannot attribute to a current person or system. The comment at the end of each key line usually says who it belongs to.

Step 5 - Network: open ports and firewall

List every listening TCP and UDP socket with the process that owns it:

sudo ss -tulpn | tee ports.txt
Netid State  Recv-Q Send-Q Local Address:Port  Peer Address:Port Process
udp   UNCONN 0      0      127.0.0.53%lo:53         0.0.0.0:*     users:(("systemd-resolve",pid=612,fd=13))
tcp   LISTEN 0      4096       0.0.0.0:22           0.0.0.0:*     users:(("sshd",pid=901,fd=3))
tcp   LISTEN 0      511        0.0.0.0:80           0.0.0.0:*     users:(("nginx",pid=1102,fd=6))
tcp   LISTEN 0      151      127.0.0.1:3306         0.0.0.0:*     users:(("mysqld",pid=1203,fd=21))

Read the Local Address column:

  • 127.0.0.1 or [::1]: reachable only from the server itself. Databases and caches should be here.
  • 0.0.0.0 or [::] or *: reachable on all interfaces. Each of these must be a service you intend to expose.

Then check the firewall:

sudo ufw status verbose
Status: active
Default: deny (incoming), allow (outgoing), deny (routed)

To                         Action      From
--                         ------      ----
22/tcp                     ALLOW IN    Anywhere
80,443/tcp                 ALLOW IN    Nginx Full

Healthy result: the firewall is active, the default incoming policy is deny, and only ports you need are allowed. If UFW reports Status: inactive, allow SSH with sudo ufw allow OpenSSH before running sudo ufw enable.

Also look at current connections, to spot unexpected outbound traffic such as a process talking to an unknown host:

sudo ss -tpn state established

Step 6 - Services and scheduled tasks

List running services and compare them with what the server is supposed to do:

systemctl list-units --type=service --state=running --no-pager

Disable services you do not need, for example sudo systemctl disable --now cups. Also check for failed units, which may indicate a broken security component:

systemctl --failed

Attackers often persist through scheduled tasks. Review all of them:

sudo ls -la /etc/cron.d /etc/cron.hourly /etc/cron.daily /etc/cron.weekly /etc/cron.monthly
sudo cat /etc/crontab
sudo ls -la /var/spool/cron/crontabs
systemctl list-timers --all --no-pager

To read a user's crontab, run sudo crontab -l -u username. Every entry should have a known purpose.

Step 7 - File permissions

Critical system files

stat -c '%a %U:%G %n' /etc/passwd /etc/group /etc/shadow /etc/gshadow /etc/ssh/sshd_config
644 root:root /etc/passwd
644 root:root /etc/group
640 root:shadow /etc/shadow
640 root:shadow /etc/gshadow
644 root:root /etc/ssh/sshd_config

These are the Ubuntu defaults. /etc/shadow and /etc/gshadow must never be world readable.

SUID and SGID binaries

Programs with the SUID or SGID bit run with the owner's or group's privileges, so an unexpected one is a classic privilege escalation path:

sudo find / -xdev -type f \( -perm -4000 -o -perm -2000 \) -exec ls -l {} \; | tee suid.txt

A standard Ubuntu server has around 20 to 30, such as /usr/bin/passwd, /usr/bin/sudo and /usr/bin/su. Investigate anything in /home, /tmp, /var/www or /opt, and check which package owns a file with dpkg -S /path/to/file. A binary no package owns is suspicious.

World-writable files and directories

sudo find / -xdev -type f -perm -0002 -ls
sudo find / -xdev -type d -perm -0002 ! -perm -1000 -ls

The first command lists world-writable files and should return nothing. The second lists world-writable directories without the sticky bit (unlike /tmp, which has it) and should also return nothing.

Files without an owner

Files owned by a deleted user or group can be claimed by a new account that gets the same ID:

sudo find / -xdev \( -nouser -o -nogroup \) -ls

Assign them to the right owner or remove them.

Step 8 - Package integrity

debsums compares installed files against the checksums shipped in each package, which reveals modified system binaries and configuration:

sudo apt install debsums
sudo debsums -s

With -s, only problems are printed. No output means all packaged files match. To list modified configuration files, which is often expected, run:

sudo debsums -ce

Every changed file in the output should be one that you or your configuration management edited on purpose.

Step 9 - Logs

Confirm that logging works and look at recent problems. Show errors from the current boot:

sudo journalctl -p err -b --no-pager | tail -n 30

Count failed SSH password attempts in the last 24 hours:

sudo journalctl -u ssh --since "24 hours ago" --no-pager | grep -c "Failed password"

A high number with password authentication still enabled is a strong reason to switch to key-only logins and install Fail2Ban.

Check that the journal keeps enough history and fits on disk:

journalctl --disk-usage
sudo journalctl --list-boots --no-pager | head -n 3

If the oldest boot is only a few hours old, logs are being rotated too quickly to investigate an incident. Raise SystemMaxUse in /etc/systemd/journald.conf.

Step 10 - Kernel network settings

Check the sysctl values that affect network security:

sysctl net.ipv4.tcp_syncookies net.ipv4.conf.all.accept_redirects net.ipv4.conf.all.send_redirects net.ipv4.conf.all.accept_source_route net.ipv4.conf.all.rp_filter kernel.randomize_va_space net.ipv4.ip_forward
net.ipv4.tcp_syncookies = 1
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.all.accept_source_route = 0
net.ipv4.conf.all.rp_filter = 2
kernel.randomize_va_space = 2
net.ipv4.ip_forward = 0

The output above shows the desired values for a server that is not a router. net.ipv4.ip_forward = 1 is normal on hosts that run Docker, Kubernetes or a VPN, and suspicious anywhere else.

On a default Ubuntu install, accept_redirects and send_redirects are usually 1. To change them, create a drop-in file:

sudo nano /etc/sysctl.d/99-hardening.conf
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.default.accept_redirects = 0
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.default.send_redirects = 0

Apply it with sudo sysctl --system and run the check again.

Step 11 - TLS certificates

If the server hosts websites, check when their certificates expire. For Let's Encrypt certificates managed by Certbot:

sudo certbot certificates

For any certificate file, or a live site:

sudo openssl x509 -in /etc/ssl/certs/your_certificate.pem -noout -subject -enddate
echo | openssl s_client -connect your_domain:443 -servername your_domain 2>/dev/null | openssl x509 -noout -enddate

Certificates expiring in less than 30 days whose renewal is not automated need action.

Step 12 - Summarizing the findings

Write down each finding with a priority, so the fixes can be planned:

PriorityExamplesFix within
CriticalExtra UID 0 account, unknown SSH key, unknown SUID binary, database listening on all interfacesImmediately
HighPending security updates, password SSH login for root, firewall disabled1 to 3 days
MediumUnneeded services running, NOPASSWD sudo rules, short log retention2 weeks
LowMinor hardening values, documentation gapsNext maintenance window

Keep the audit directory with your notes. At the next audit, diff the saved outputs (for example diff ~/audit-2026-09-24/suid.txt ~/audit-2026-12-24/suid.txt) to see exactly what changed.

Conclusion

You audited updates, accounts, SSH, network exposure, services, scheduled tasks, file permissions, package integrity, logs, kernel settings and certificates, and saved the evidence for comparison. Repeat the audit every quarter and after any major change. To automate part of this work, run Lynis for a scored audit of hundreds of checks, protect SSH with Fail2Ban, and scan uploaded content for malware with ClamAV.