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:
| Service | Purpose | Disable If... |
|---|---|---|
cups | Print server | Always (no printers on a VPS) |
avahi-daemon | mDNS/DNS-SD | Always (local network discovery) |
bluetooth | Bluetooth | Always (no hardware) |
ModemManager | Modem management | Always (no modems) |
postfix | Mail server | If you don't send email from the server |
snapd | Snap package manager | If 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:
- Boot and services
- Kernel hardening
- Memory and processes
- Users, groups, and authentication
- Shells
- File systems
- Storage
- NFS, DNS, packages
- Networking, firewalls
- SSH, SNMP, databases
- LDAP, PHP, Squid, logging
- Insecure services, scheduling
- Accounting, cryptography
- Virtualization, containers
- Security frameworks, file integrity
- Malware, file permissions
- Home directories, kernel hardening
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:
| Score | Assessment | Action |
|---|---|---|
| 0–49 | Minimal hardening | Address all warnings and critical suggestions immediately |
| 50–69 | Basic hardening | Good start; work through remaining suggestions |
| 70–84 | Well-hardened | Solid security posture; address remaining items by priority |
| 85–100 | Excellent | Maintain 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:
| # | Check | Category | Command |
|---|---|---|---|
| 1 | SSH key-only auth | SSH | sshd -T | grep passwordauthentication |
| 2 | Root login disabled | SSH | sshd -T | grep permitrootlogin |
| 3 | SSH port review | SSH | sshd -T | grep "^port" |
| 4 | SSH idle timeout | SSH | sshd -T | grep clientalive |
| 5 | Authorized keys audit | SSH | Check all authorized_keys files |
| 6 | Sudo users audit | Access | getent group sudo |
| 7 | UFW active | Firewall | ufw status verbose |
| 8 | Firewall rules audit | Firewall | ufw status numbered |
| 9 | Docker/UFW bypass | Firewall | Check Docker port bindings |
| 10 | Open ports scan | Network | ss -tlnp |
| 11 | IPv6 firewall | Network | Check UFW IPv6 setting |
| 12 | Outbound rules | Network | ufw status verbose |
| 13 | Pending updates | Updates | apt list --upgradable |
| 14 | Unattended upgrades | Updates | dpkg -l unattended-upgrades |
| 15 | Kernel version | Updates | uname -r |
| 16 | Reboot required | Updates | Check /var/run/reboot-required |
| 17 | Unnecessary services | Services | systemctl list-unit-files --state=enabled |
| 18 | Listening processes | Services | ss -tulnp |
| 19 | Cron jobs audit | Services | List all user crontabs |
| 20 | Systemd timers | Services | systemctl list-timers |
| 21 | World-writable files | Filesystem | find / -xdev -perm -0002 |
| 22 | SUID/SGID files | Filesystem | find / -xdev -perm -4000 |
| 23 | /tmp permissions | Filesystem | stat /tmp |
| 24 | Log file permissions | Filesystem | Check /var/log permissions |
| 25 | SSL certificate expiry | Application | openssl s_client + certbot renew --dry-run |
| 26 | Application updates | Application | Check all service versions |
| 27 | Database access | Application | Audit database users/privileges |
| 28 | Resource isolation | Application | systemd-detect-virt |
| 29 | Fail2ban active | Monitoring | fail2ban-client status |
| 30 | Log rotation active | Monitoring | Check 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.