← Back to Wiki
Security

Homelab Vulnerability Scanning with Greenbone OpenVAS, Trivy and ZAP

"We should have some vulnerability scanning" means three different things. Scanning hosts on your LAN. Scanning the container images your fleet runs. Scanning your public-facing web apps. Here is a free, complementary stack that covers all three.

Share on X

1. Network and host scanning: Greenbone Community Edition

Greenbone Community Edition, which is OpenVAS-based, deploys via an official Docker Compose bundle in a few commands.

BE WARNED: the default config only binds to localhost. The stock compose file publishes the web UI as 127.0.0.1 only, deliberately LAN-inaccessible out of the box. Opening it to your LAN is a security-relevant change, an admin UI becoming reachable from more places. Treat it as one. Do not flip the binding without thinking about who can now reach it.
Be patient on first boot. Scan configs will not exist for roughly an hour after first start. The scanner is importing the full CVE feed, going back to 1999, plus supplementary vulnerability data. Check the container logs before you assume the deployment is broken.

Once configs appear, a full authenticated scan of your whole subnet is straightforward over the GMP protocol, from either the web UI or the CLI tools. A "Full and fast" scan of a typical home subnet takes a few hours. Schedule it monthly rather than weekly, given the runtime.

Greenbone task list showing a completed 'Homelab LAN Full Scan' task with a Medium severity result
Greenbone → Scans → Tasks. A completed scan task. One recurring job covering the whole subnet.

2. Container image scanning: Trivy

Trivy scans container images for known CVEs by name and tag. No connecting to each host's Docker daemon individually. Run it as a native binary so it scans arbitrary images with no extra setup, and write a small script that scans every image your fleet runs.

BE WARNED: do not parse Trivy's human-readable table output for totals. It is fragile in two ways. A target with zero findings omits the "Total:" line entirely rather than printing zero, so a naive grep misreports it as a scan failure. And some images have more than one scanned artifact in the same report, an OS layer plus a separately tracked binary, so reading only the first table row silently drops real findings. Scan with --format json and sum with jq instead:
trivy image --severity HIGH,CRITICAL --quiet --format json "$img" > "$img.json"
total=$(jq '[.Results[]?.Vulnerabilities // [] | length] | add // 0' "$img.json")

3. Web app scanning: OWASP ZAP

OWASP ZAP's baseline mode is safe to run against live production sites, including anything security-sensitive. Spider and passive analysis only, no active attack traffic. Run it one-shot per target from its official Docker image.

BE WARNED: do not build deeply nested shell commands inline over SSH. Multiple layers of quoting, an outer SSH command containing a nested bash -c containing its own quoted arguments, reliably makes a variable fail to expand where you expect. Write a real script file with normal, non-nested quoting and copy it over instead.
BE WARNED: the ZAP container writes reports as a non-root user. If your output directory is root-owned at the default permissions, the scan completes fine and report generation fails silently with an access-denied error. chmod 777 the output directory before running, or bind-mount with a matching non-root UID.

Putting it on a schedule

One-off manual runs are a good start. The real value is recurring scans that get looked at:

Feeding results into your monitoring stack

If your monitoring system supports local checks, that is a clean way to surface scan trends without building a separate dashboard. A local check is a small script on the monitored host printing a status line the agent picks up as a real service with thresholds. Checkmk works this way. Have each scanner write a small summary file, then have a local check script read it and print WARN or CRIT against a threshold you pick.

A general lesson from running this. An interim fix superseded by a permanent one still needs an explicit cleanup step. A temporary swap file added under memory pressure, later made unnecessary by a RAM upgrade, for instance. Skip the cleanup and it becomes the next incident. Nothing removes it automatically just because it is no longer needed.