Security isn't something you configure once and forget. Over time, configurations drift — a quick fix that opened a port, a new user account that was never locked down, an update that reset a hardening setting. Without regular audits, your Ubuntu VPS accumulates security debt that attackers are happy to exploit.

This checklist gives you 30 specific security checks to run on your Ubuntu VPS, each with the exact command, what to expect, and what to fix if it fails. Run through this list quarterly — or after any significant change to your server configuration. If you haven't done initial hardening yet, start with our Ubuntu VPS security hardening guide first.

MassiveGRID Ubuntu VPS includes: Ubuntu 24.04 LTS pre-installed · Proxmox HA cluster with automatic failover · Ceph 3x replicated NVMe storage · Independent CPU/RAM/storage scaling · 12 Tbps DDoS protection · 4 global datacenter locations · 100% uptime SLA · 24/7 human support rated 9.5/10

Deploy a self-managed VPS — from $1.99/mo
Need dedicated resources? — from $19.80/mo
Want fully managed hosting? — we handle everything

What MassiveGRID Handles vs. What You Handle

On a MassiveGRID Cloud VPS, the infrastructure security layer is handled: 12 Tbps DDoS protection, network isolation, hardware security, hypervisor patching, and physical datacenter security. This checklist covers OS-level and application-level checks — the parts you control.

The shared responsibility model means your hosting provider secures the infrastructure beneath your VPS, but everything inside the VPS — the operating system, software, configurations, and data — is your responsibility on a self-managed plan.

SSH and Access Control (Checks 1–6)

Check 1: SSH Key-Only Authentication

What to check: Password authentication should be disabled. SSH keys are significantly harder to brute-force.

sudo sshd -T | grep -i passwordauthentication

Expected output:

passwordauthentication no

If it fails: Edit /etc/ssh/sshd_config and set PasswordAuthentication no. Then restart SSH:

sudo sed -i 's/^#*PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config
sudo systemctl restart sshd

Warning: Before disabling password auth, verify you can log in with your SSH key from another terminal session. Locking yourself out of SSH requires console access to fix.

Check 2: Root Login Disabled

What to check: Direct root login via SSH should be disabled. Use a regular user with sudo instead.

sudo sshd -T | grep -i permitrootlogin

Expected output:

permitrootlogin no

If it fails: Set PermitRootLogin no in /etc/ssh/sshd_config and restart SSH. Ensure you have a non-root user with sudo access first.

Check 3: SSH Port

What to check: While changing the SSH port isn't true security (security through obscurity), it dramatically reduces automated brute-force noise in your logs.

sudo sshd -T | grep "^port "

Expected output: Either port 22 (default) or your custom port. If using port 22, ensure Fail2ban is active (Check 29).

Assessment: If you see thousands of failed SSH attempts in your logs (journalctl -u ssh --since "1 hour ago" | grep "Failed"), consider changing the port or relying on Fail2ban with aggressive banning.

Check 4: SSH Idle Timeout

What to check: Idle SSH sessions should be terminated automatically to prevent abandoned sessions from being hijacked.

sudo sshd -T | grep -E "clientaliveinterval|clientalivecountmax"

Expected output:

clientaliveinterval 300
clientalivecountmax 2

This configuration disconnects sessions after 10 minutes of inactivity (300 seconds × 2 missed keepalives).

If it fails: Add to /etc/ssh/sshd_config:

ClientAliveInterval 300
ClientAliveCountMax 2

Check 5: Authorized Keys Audit

What to check: Review which SSH keys have access. Unknown or old keys should be removed.

# List all authorized_keys files and their contents
for user_home in /home/*; do
    user=$(basename "$user_home")
    keyfile="$user_home/.ssh/authorized_keys"
    if [ -f "$keyfile" ]; then
        echo "=== $user ==="
        cat "$keyfile" | while read -r line; do
            # Show just the key comment (usually email or hostname)
            echo "$line" | awk '{print $NF}'
        done
    fi
done

# Also check root
if [ -f /root/.ssh/authorized_keys ]; then
    echo "=== root ==="
    cat /root/.ssh/authorized_keys | awk '{print $NF}'
fi

What to look for: Every key should be recognizable. If you see keys you don't recognize, investigate immediately. Remove old keys for team members who no longer need access.

Check 6: Sudo Users Audit

What to check: Only authorized users should have sudo privileges.

# List users in the sudo group
getent group sudo

# List users in the admin group (legacy)
getent group admin 2>/dev/null

# Check for users with UID 0 (root equivalent)
awk -F: '$3 == 0 {print $1}' /etc/passwd

# Check sudoers drop-in directory for custom rules
ls -la /etc/sudoers.d/

# Check for NOPASSWD entries (sudo without password)
sudo grep -r "NOPASSWD" /etc/sudoers /etc/sudoers.d/ 2>/dev/null

What to look for: Only root should have UID 0. The sudo group should contain only users you recognize. NOPASSWD entries should be minimized — each one is a privilege escalation risk if that account is compromised.

Firewall and Network (Checks 7–12)

Check 7: UFW Active and Enabled

What to check: Your firewall should be active and configured to start on boot.

sudo ufw status verbose

Expected output:

Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), disabled (routed)

If it fails: Enable UFW with a safe default configuration. See our UFW advanced rules guide.

sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow ssh
sudo ufw enable

Check 8: Firewall Rules Audit

What to check: Review all allowed ports. Each rule should correspond to a service you're intentionally running.

sudo ufw status numbered

What to look for: Every allowed port should map to a running service. If you see ports you don't recognize, investigate what's listening on them (Check 10) and remove unnecessary rules:

# Remove a rule by number
sudo ufw delete 5

Check 9: Docker/UFW Bypass Check

What to check: Docker manipulates iptables directly, potentially bypassing UFW rules. This is a critical and commonly overlooked issue — see our UFW guide for the full explanation.

# Check if Docker is adding its own iptables rules
sudo iptables -L DOCKER -n 2>/dev/null

# Check if ports Docker publishes are accessible from outside
# Run this from ANOTHER machine (not the VPS):
# nmap -p 3000,8080,9090 your-vps-ip

# Check docker-compose for published ports NOT bound to 127.0.0.1
grep -r "ports:" ~/*/docker-compose.yml 2>/dev/null
grep -rE '"\d+:\d+"' ~/*/docker-compose.yml 2>/dev/null

What to look for: Docker port mappings like "3000:3000" are accessible from the internet regardless of UFW rules. Change them to "127.0.0.1:3000:3000" to bind only to localhost.

Check 10: Open Ports Scan

What to check: Identify all ports listening on external interfaces.

# Show all listening ports with process names
sudo ss -tlnp

# More readable format
sudo ss -tlnp | awk 'NR>1 {print $4, $7}' | sort

Expected output example:

0.0.0.0:22          users:(("sshd",pid=1234))
0.0.0.0:80          users:(("nginx",pid=5678))
0.0.0.0:443         users:(("nginx",pid=5678))
127.0.0.1:3000      users:(("grafana",pid=9012))
127.0.0.1:3306      users:(("mysqld",pid=3456))

What to look for: Services bound to 0.0.0.0 or :: are accessible on all interfaces. Only SSH, web servers, and intentionally public services should be bound to 0.0.0.0. Databases, admin panels, and monitoring tools should be bound to 127.0.0.1.

Check 11: IPv6 Firewall Rules

What to check: If IPv6 is enabled, UFW should be managing IPv6 rules as well.

# Check if IPv6 is enabled in UFW
grep "IPV6=" /etc/default/ufw

# Check for IPv6 addresses on interfaces
ip -6 addr show scope global

Expected output: IPV6=yes. If IPv6 is enabled on the system but UFW has IPV6=no, your IPv6 interfaces are unprotected.

Check 12: Outbound Rules Review

What to check: On high-security servers, consider restricting outbound connections to prevent data exfiltration or command-and-control communication.

# Current outbound policy
sudo ufw status verbose | grep "Default:"

# List any explicit outbound rules
sudo ufw status | grep "OUT"

Assessment: The default allow (outgoing) is fine for most VPS use cases. For sensitive workloads, consider restricting outbound to specific ports (80, 443, 53 for DNS) and destinations.

System Updates (Checks 13–16)

Check 13: Pending Security Updates

What to check: Check for available security updates that haven't been applied.

# Update package list and check for upgrades
sudo apt update
apt list --upgradable 2>/dev/null

# Check specifically for security updates
sudo unattended-upgrade --dry-run 2>&1 | grep "Packages that will be upgraded"

If it fails: Apply security updates immediately:

sudo apt upgrade -y

Check 14: Unattended Upgrades Active

What to check: Security updates should be applied automatically so zero-day patches don't wait for your next manual login.

# Check if unattended-upgrades is installed and active
dpkg -l unattended-upgrades | grep "^ii"
systemctl is-active unattended-upgrades

# Check configuration
cat /etc/apt/apt.conf.d/20auto-upgrades

Expected output:

APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";

If it fails: Install and configure unattended-upgrades:

sudo apt install unattended-upgrades -y
sudo dpkg-reconfigure -plow unattended-upgrades

Check 15: Kernel Version

What to check: Verify you're running the latest available kernel.

# Current running kernel
uname -r

# Latest installed kernel
dpkg -l | grep linux-image | grep "^ii" | awk '{print $2}' | sort -V | tail -1

# Check if a newer kernel is available
apt list --upgradable 2>/dev/null | grep linux-image

Assessment: If the running kernel doesn't match the latest installed kernel, a reboot is needed.

Check 16: Reboot Required

What to check: After kernel or critical library updates, a reboot may be pending.

# Check if reboot is needed
[ -f /var/run/reboot-required ] && echo "REBOOT REQUIRED" || echo "No reboot needed"

# Check what packages require the reboot
cat /var/run/reboot-required.pkgs 2>/dev/null

If it fails: Schedule a maintenance window and reboot:

sudo reboot

Services and Processes (Checks 17–20)

Check 17: Unnecessary Services

What to check: Every running service is a potential attack surface. Remove or disable what you don't need.

# List all enabled services
systemctl list-unit-files --type=service --state=enabled | grep -v "@"

# List all running services
systemctl list-units --type=service --state=running

Common services that can often be disabled on a VPS:

ServicePurposeDisable If...
cupsPrint serverAlways (no printers on a VPS)
avahi-daemonmDNS/DNS-SDAlways (local network discovery)
bluetoothBluetoothAlways (no hardware)
ModemManagerModem managementAlways (no modems)
postfixMail serverIf you don't send email from the server
snapdSnap package managerIf you don't use snap packages
# Disable an unnecessary service
sudo systemctl stop cups
sudo systemctl disable cups
sudo systemctl mask cups  # Prevents re-enabling

Check 18: Unexpected Listening Processes

What to check: Cross-reference listening ports with expected services.

# All listening TCP and UDP ports with process names
sudo ss -tulnp | sort -k5

# More detailed view with full command
sudo ss -tulnp | awk 'NR>1' | while read -r line; do
    pid=$(echo "$line" | grep -oP 'pid=\K[0-9]+')
    if [ -n "$pid" ]; then
        echo "$line"
        echo "  -> $(ps -p $pid -o comm= 2>/dev/null) ($(ps -p $pid -o user= 2>/dev/null))"
    fi
done

What to look for: Any process listening on a port that you don't recognize. Pay special attention to processes running as root on public interfaces.

Check 19: Cron Jobs Audit

What to check: Cron jobs run with the privilege of their owning user. A malicious cron job could maintain persistent access. For more on cron jobs, see our cron scheduling guide.

# System crontabs
sudo ls -la /etc/cron.d/
sudo ls -la /etc/cron.daily/
sudo ls -la /etc/cron.hourly/
sudo ls -la /etc/cron.weekly/
sudo cat /etc/crontab

# User crontabs
for user in $(cut -d: -f1 /etc/passwd); do
    crontab_content=$(sudo crontab -l -u "$user" 2>/dev/null)
    if [ -n "$crontab_content" ]; then
        echo "=== $user ==="
        echo "$crontab_content"
    fi
done

What to look for: Every cron job should be recognizable. Watch for entries that download and execute scripts from URLs, obfuscated commands, or entries you didn't create.

Check 20: Systemd Timers Audit

What to check: Systemd timers are the modern alternative to cron. Audit them the same way.

# List all active timers
systemctl list-timers --all

# Check for user-level timers
systemctl --user list-timers --all 2>/dev/null

What to look for: Every timer should correspond to a known service. Investigate any timer you don't recognize with systemctl cat timer-name.timer.

File System (Checks 21–24)

Check 21: World-Writable Files

What to check: World-writable files outside of /tmp and /var/tmp are a privilege escalation risk.

# Find world-writable files (excluding tmpfs and proc)
sudo find / -xdev -type f -perm -0002 -not -path "/proc/*" -not -path "/sys/*" 2>/dev/null

Expected output: Empty (no results). If files are found, fix their permissions:

# Remove world-writable permission
sudo chmod o-w /path/to/file

Check 22: SUID/SGID Files

What to check: SUID and SGID files run with elevated privileges. They're necessary for some system tools but can be exploited if a new one appears.

# Find all SUID files
sudo find / -xdev -type f -perm -4000 2>/dev/null | sort

# Find all SGID files
sudo find / -xdev -type f -perm -2000 2>/dev/null | sort

Expected SUID files (normal on Ubuntu):

/usr/bin/chfn
/usr/bin/chsh
/usr/bin/gpasswd
/usr/bin/mount
/usr/bin/newgrp
/usr/bin/passwd
/usr/bin/su
/usr/bin/sudo
/usr/bin/umount
/usr/lib/openssh/ssh-keysign
/usr/lib/dbus-1.0/dbus-daemon-launch-helper

What to look for: Any SUID/SGID binary that isn't in the expected list. Save this list during your first audit and compare in subsequent audits:

# Save current SUID list for future comparison
sudo find / -xdev -type f -perm -4000 2>/dev/null | sort > /root/suid_baseline.txt

# Compare against baseline in next audit
sudo find / -xdev -type f -perm -4000 2>/dev/null | sort | diff /root/suid_baseline.txt -

Check 23: /tmp Permissions

What to check: The /tmp directory should have the sticky bit set, preventing users from deleting each other's files.

stat -c "%a %U %G" /tmp

Expected output:

1777 root root

The 1 prefix indicates the sticky bit. If missing, fix it:

sudo chmod 1777 /tmp

Check 24: Log File Permissions

What to check: Log files may contain sensitive information. They should not be world-readable.

# Check permissions on log files
ls -la /var/log/auth.log
ls -la /var/log/syslog
ls -la /var/log/kern.log

# Find world-readable log files
find /var/log -type f -perm -004 2>/dev/null

Expected: Log files should be owned by root:adm or syslog:adm with permissions 640 or more restrictive. For more on log management, see our server logs troubleshooting guide.

Application Security (Checks 25–28)

Check 25: SSL Certificate Expiry

What to check: SSL/TLS certificates should not be expiring within the next 30 days.

# Check certificate expiry for your domain
echo | openssl s_client -servername yourdomain.com -connect yourdomain.com:443 2>/dev/null | openssl x509 -noout -dates

# Check all certificates on the system
sudo find /etc/letsencrypt/live -name "cert.pem" -exec sh -c '
    echo "=== {} ==="
    openssl x509 -in {} -noout -subject -enddate
' \; 2>/dev/null

# Check if certbot auto-renewal is working
sudo certbot renew --dry-run

Expected: notAfter should be more than 30 days in the future. Certbot dry-run should succeed without errors.

Check 26: Application Updates

What to check: Web applications (WordPress, custom apps) and their dependencies should be current.

# Check Nginx version
nginx -v 2>&1

# Check PHP version (if applicable)
php -v 2>/dev/null

# Check Node.js version (if applicable)
node -v 2>/dev/null

# Check Docker images for updates
docker images --format "{{.Repository}}:{{.Tag}} (created {{.CreatedSince}})"

# Check for known vulnerabilities in Docker images
docker scout cves your-image:tag 2>/dev/null

Assessment: Compare versions against known CVEs. Update any software with published security vulnerabilities. For PHP performance and version management, see our PHP optimization guide.

Check 27: Database Access Controls

What to check: Database users should have minimum necessary privileges and not use default credentials.

# MySQL/MariaDB: List users and their hosts
sudo mysql -e "SELECT user, host, plugin FROM mysql.user;"

# Check for users with wildcard host access
sudo mysql -e "SELECT user, host FROM mysql.user WHERE host = '%';"

# PostgreSQL: List roles and their attributes
sudo -u postgres psql -c "\du"

What to look for: No users with the host set to % (allows connection from any IP) unless specifically needed. No default or empty passwords. Each application should have its own database user with access limited to its specific database. See our PostgreSQL installation guide for secure database setup.

Check 28: Resource Isolation

What to check: On shared hosting, verify your workloads are properly isolated.

# Check if running in a container/VM
systemd-detect-virt

# Check cgroup resource limits
cat /proc/self/cgroup

# Check available resources match your plan
free -h
nproc
df -h /

Check #28: Resource Isolation. If you handle sensitive data, dedicated resources provide hardware-level isolation — your workloads don't share CPU or memory with other tenants.

Logging and Monitoring (Checks 29–30)

Check 29: Fail2ban Active and Configured

What to check: Fail2ban should be running and actively protecting SSH (and ideally other services).

# Check Fail2ban status
sudo systemctl is-active fail2ban

# Check active jails
sudo fail2ban-client status

# Check SSH jail specifically
sudo fail2ban-client status sshd

# Check recent bans
sudo fail2ban-client status sshd | grep "Currently banned"
sudo zgrep "Ban " /var/log/fail2ban.log* | tail -10

Expected: Fail2ban should be active with at least the sshd jail enabled. If you see zero bans and your SSH port is 22, something may be misconfigured — brute-force attempts are essentially guaranteed on port 22. For advanced Fail2ban configuration, see our Fail2ban guide.

Check 30: Log Rotation Active

What to check: Without log rotation, log files grow indefinitely and can fill your disk.

# Check logrotate status
logrotate --version
cat /etc/logrotate.conf

# Check last rotation
ls -la /var/log/syslog*
ls -la /var/log/auth.log*

# Test logrotate configuration
sudo logrotate -d /etc/logrotate.conf 2>&1 | head -20

# Check for oversized log files
sudo find /var/log -type f -size +100M 2>/dev/null

What to look for: Rotated log files (syslog.1, syslog.2.gz) should exist, indicating rotation is happening. No single log file should be larger than 100 MB. If you find oversized logs, investigate what's generating excessive log entries and configure appropriate rotation. For disk space management, see our disk space guide.

Automated Scanning with Lynis

Running all 30 checks manually is valuable, but automated scanning catches things you might miss. Lynis is the industry-standard open-source security auditing tool for Linux.

Install Lynis

# Install from the official repository for the latest version
sudo apt install apt-transport-https -y

# Add Lynis repository
echo "deb https://packages.cisofy.com/community/lynis/deb/ stable main" | sudo tee /etc/apt/sources.list.d/lynis.list
sudo wget -O - https://packages.cisofy.com/keys/cisofy-software-public.key | sudo apt-key add -

sudo apt update
sudo apt install lynis -y

# Or install from Ubuntu's default repos (may be slightly older)
sudo apt install lynis -y

Run a Full System Audit

# Run the audit
sudo lynis audit system

Lynis performs over 300 checks across these categories:

Interpreting Results

# View the full report
sudo cat /var/log/lynis-report.dat

# View just the warnings
sudo grep "warning" /var/log/lynis-report.dat

# View suggestions
sudo grep "suggestion" /var/log/lynis-report.dat

# View the hardening index score
sudo grep "hardening_index" /var/log/lynis-report.dat

Hardening index scores:

ScoreAssessmentAction
0–49Minimal hardeningAddress all warnings and critical suggestions immediately
50–69Basic hardeningGood start; work through remaining suggestions
70–84Well-hardenedSolid security posture; address remaining items by priority
85–100ExcellentMaintain this level; review after any system changes

Building an Audit Script

Combine the essential checks into a single script you can run periodically:

cat > ~/security-audit.sh << 'SCRIPT'
#!/bin/bash
# Security Audit Script for Ubuntu VPS
# Run: sudo bash security-audit.sh

echo "========================================="
echo " Ubuntu VPS Security Audit"
echo " Date: $(date)"
echo " Hostname: $(hostname)"
echo "========================================="
echo ""

PASS=0
WARN=0
FAIL=0

check() {
    local description="$1"
    local result="$2"  # 0=pass, 1=warn, 2=fail
    local detail="$3"

    case $result in
        0) echo "[PASS] $description"; PASS=$((PASS+1)) ;;
        1) echo "[WARN] $description - $detail"; WARN=$((WARN+1)) ;;
        2) echo "[FAIL] $description - $detail"; FAIL=$((FAIL+1)) ;;
    esac
}

# Check 1: Password authentication
pw_auth=$(sshd -T 2>/dev/null | grep "^passwordauthentication" | awk '{print $2}')
[ "$pw_auth" = "no" ] && check "SSH password auth disabled" 0 || check "SSH password auth disabled" 2 "Currently enabled"

# Check 2: Root login
root_login=$(sshd -T 2>/dev/null | grep "^permitrootlogin" | awk '{print $2}')
[ "$root_login" = "no" ] && check "Root login disabled" 0 || check "Root login disabled" 2 "Currently: $root_login"

# Check 7: UFW active
ufw_status=$(ufw status | head -1)
echo "$ufw_status" | grep -q "active" && check "UFW firewall active" 0 || check "UFW firewall active" 2 "Firewall not active"

# Check 13: Pending updates
updates=$(apt list --upgradable 2>/dev/null | grep -c upgradable)
[ "$updates" -eq 0 ] && check "System fully updated" 0 || check "System fully updated" 1 "$updates packages need updating"

# Check 14: Unattended upgrades
dpkg -l unattended-upgrades 2>/dev/null | grep -q "^ii" && check "Unattended upgrades installed" 0 || check "Unattended upgrades installed" 2 "Not installed"

# Check 16: Reboot required
[ ! -f /var/run/reboot-required ] && check "No reboot required" 0 || check "No reboot required" 1 "Reboot pending"

# Check 29: Fail2ban
systemctl is-active fail2ban &>/dev/null && check "Fail2ban active" 0 || check "Fail2ban active" 2 "Not running"

# Check 30: Log rotation
ls /var/log/syslog.1* &>/dev/null && check "Log rotation working" 0 || check "Log rotation working" 1 "No rotated logs found"

echo ""
echo "========================================="
echo " Results: $PASS passed, $WARN warnings, $FAIL failed"
echo "========================================="
SCRIPT
chmod +x ~/security-audit.sh

Scheduling Quarterly Audits

Automate the audit to run quarterly and email you the results. Use cron (see our cron scheduling guide) to schedule it:

# Run on the 1st of every 3rd month at 9 AM
sudo crontab -e

# Add this line:
0 9 1 */3 * /root/security-audit.sh 2>&1 | mail -s "Quarterly VPS Security Audit - $(hostname)" admin@yourdomain.com

Alternatively, run Lynis on the same schedule for a more comprehensive scan:

0 9 1 */3 * lynis audit system --cronjob 2>&1 | mail -s "Quarterly Lynis Audit - $(hostname)" admin@yourdomain.com

Prefer Continuous Security Management?

Running security audits quarterly is good practice, but threats don't operate on a quarterly schedule. If you need continuous security monitoring, vulnerability scanning, and immediate response to security events, MassiveGRID's fully managed dedicated hosting includes proactive security management, regular patching, and 24/7 incident response — giving you enterprise-grade security without the operational overhead.

Summary

Here's your complete checklist for quick reference:

#CheckCategoryCommand
1SSH key-only authSSHsshd -T | grep passwordauthentication
2Root login disabledSSHsshd -T | grep permitrootlogin
3SSH port reviewSSHsshd -T | grep "^port"
4SSH idle timeoutSSHsshd -T | grep clientalive
5Authorized keys auditSSHCheck all authorized_keys files
6Sudo users auditAccessgetent group sudo
7UFW activeFirewallufw status verbose
8Firewall rules auditFirewallufw status numbered
9Docker/UFW bypassFirewallCheck Docker port bindings
10Open ports scanNetworkss -tlnp
11IPv6 firewallNetworkCheck UFW IPv6 setting
12Outbound rulesNetworkufw status verbose
13Pending updatesUpdatesapt list --upgradable
14Unattended upgradesUpdatesdpkg -l unattended-upgrades
15Kernel versionUpdatesuname -r
16Reboot requiredUpdatesCheck /var/run/reboot-required
17Unnecessary servicesServicessystemctl list-unit-files --state=enabled
18Listening processesServicesss -tulnp
19Cron jobs auditServicesList all user crontabs
20Systemd timersServicessystemctl list-timers
21World-writable filesFilesystemfind / -xdev -perm -0002
22SUID/SGID filesFilesystemfind / -xdev -perm -4000
23/tmp permissionsFilesystemstat /tmp
24Log file permissionsFilesystemCheck /var/log permissions
25SSL certificate expiryApplicationopenssl s_client + certbot renew --dry-run
26Application updatesApplicationCheck all service versions
27Database accessApplicationAudit database users/privileges
28Resource isolationApplicationsystemd-detect-virt
29Fail2ban activeMonitoringfail2ban-client status
30Log rotation activeMonitoringCheck for rotated log files

Run through this checklist quarterly, after any significant server changes, or when onboarding new team members who have server access. Each audit should take 30–45 minutes manually, or you can use the automated script and Lynis to cover the bulk of it in minutes.