How to Scan Your Server for Vulnerabilities (Lynis, nmap, Trivy and a Monthly Audit Script)

The security checklist (39) is what you intended. A scan is what's true: the port you forgot, the container image with a two-year-old OpenSSL, the TLS setting that's weak, the sudoers line from a tutorial you half-followed. Four free tools cover it, each from a different angle, and together they make a monthly report that reads like a to-do list.
Quick reference
| Tool | Angle | Command |
|---|---|---|
| Lynis | host configuration audit | sudo lynis audit system |
| nmap | what's reachable, from outside | nmap -sV -p- your-public-ip (from elsewhere) |
| Trivy | known CVEs in container images and files | trivy image nginx:latest |
| testssl.sh | TLS configuration of the proxy | testssl.sh https://cloud.example.com |
| Also | apt list --upgradable, docker scout, CrowdSec's own view (107) |
1. Lynis: the host
sudo apt install lynis -y
sudo lynis audit system
Ten minutes of checks: kernel hardening, SSH settings, file permissions, password policies, firewall, logging, malware scanners, and more. It ends with a hardening index (0–100) and a list of warnings and suggestions, each with a code and a link. Read /var/log/lynis-report.dat or the screen output.
What to act on: warnings (always), and suggestions that match this series — SSH-7408 items (keys only, no root, 3), FIRE-4512 (a firewall, 6), PKGS-7392 (updates, 36), AUTH-9328 (umask), KRNL-6000 sysctl hardening (worth applying: kernel.kptr_restrict=2, net.ipv4.conf.all.rp_filter=1, etc., via a sysctl.d file). Ignore suggestions aimed at corporate compliance (antivirus, auditd) unless you want them.
Aim for the index to climb each month, not to hit 100.
2. nmap: what the internet sees
Run it from outside — a phone hotspot, a VPS, or the relative's network (108) — against your public IP:
sudo apt install nmap -y
nmap -sV -p- -T4 203.0.113.10
nmap -sU --top-ports 50 203.0.113.10
The first lists every open TCP port with the service version; the second the common UDP ones. Expected: 51820/udp (WireGuard) and, if you host a site, 80 and 443. Anything else is a finding. A forgotten forward, a UPnP rule a device opened (turn UPnP off on the router), IPv6 (145) exposing something — nmap -6 against your v6 address too.
From inside, nmap -sV 192.168.1.50 shows what the LAN sees; compare with sudo ss -tlnp on the server — a listening service you didn't expect is worth a question.
3. Trivy: the containers
Images carry their own OS packages, and nobody runs apt upgrade inside them — the image gets rebuilt upstream, or it doesn't:
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sudo sh -s -- -b /usr/local/bin
trivy image --severity HIGH,CRITICAL nextcloud:30
for i in $(docker images --format '{{.Repository}}:{{.Tag}}' | grep -v none); do echo "== $i"; trivy image -q --severity CRITICAL "$i" | tail -n +2; done
Each image's CRITICAL CVEs, with the fixed version. A big count on an image that hasn't been updated in a year is the signal to update it (50) or swap it for a maintained one. Trivy also scans your own Dockerfiles and compose files for misconfigurations (trivy config ~/docker) and the host filesystem (trivy fs /).
4. testssl: the proxy
git clone --depth 1 https://github.com/drwetter/testssl.sh ~/testssl
~/testssl/testssl.sh https://cloud.example.com
Protocols (TLS 1.0/1.1 should be off), weak ciphers, HSTS, certificate chain, known vulnerabilities. For Nginx the fix is usually Mozilla's "intermediate" config (ssl-config.mozilla.org); Caddy's defaults already pass. ssllabs.com gives the same grade from outside if the site is public.
5. A monthly audit script
sudo nano /usr/local/bin/security-audit.sh
#!/bin/bash
R=/var/log/security-audit-$(date +%F).txt
{
echo "=== Updates pending ==="; apt list --upgradable 2>/dev/null | tail -n +2
echo; echo "=== Listening ==="; ss -tlnpu | awk 'NR>1{print $1,$5,$7}'
echo; echo "=== UFW ==="; ufw status numbered | tail -n +4
echo; echo "=== Lynis ==="; lynis audit system --quiet --no-colors | grep -E "Warning|Suggestion|Hardening index"
echo; echo "=== Trivy CRITICAL per image ==="
for i in $(docker images --format '{{.Repository}}:{{.Tag}}' | grep -v none); do
n=$(trivy image -q --severity CRITICAL --format json "$i" 2>/dev/null | grep -c '"Severity": "CRITICAL"'); echo "$n $i"
done | sort -rn | head -15
echo; echo "=== sudoers ==="; grep -rv '^#' /etc/sudoers.d/ /etc/sudoers | grep -v '^$'
echo; echo "=== SUID binaries changed? ==="; find / -xdev -perm -4000 -type f 2>/dev/null | sort | md5sum
} > "$R" 2>&1
mail -s "Security audit $(hostname) $(date +%F)" you@example.com < "$R"
First of the month from root's timer (69), alongside the weekly report (156). The SUID checksum line is a cheap tripwire: if it changes without an update, ask why.
What to do with findings
Updates → run them. Open port → close it or document it (39). Trivy CRITICAL on an image → update the tag or find a maintained image. Lynis warning → the code's link has the exact fix. Weak TLS → the Mozilla config. Then rerun; the next report is shorter.
Troubleshooting
Lynis complains about things Docker needs. Some sysctl and module suggestions conflict with containers (e.g. net.ipv4.ip_forward). Accept those knowingly; note them.
nmap from outside shows ports you can't find on the server. The router itself (admin page on WAN, UPnP) — check its settings; or IPv6.
Trivy finds CRITICALs with no fix. Upstream hasn't patched; note it, check the next month, and limit the container (68, 166) meanwhile.
Scans trigger CrowdSec/Fail2ban against your scanning host. Allowlist it, or expect to unban yourself.
Wrapping up
Lynis for the host, nmap from outside, Trivy for every image, testssl for the proxy, one script on the first of the month. The checklist becomes verified rather than assumed, and the month's to-do list writes itself — usually a few updates and one port you'd forgotten about.
