The first useful security question in a homelab is not “which scanner should I install?” It is “what is actually reachable from the internet?”
I started by making an inventory of public hostnames, their tunnel or proxy origins, the container behind each origin, and whether the route was intentionally enabled. That immediately exposed an important distinction: a service can exist and run perfectly while its public route is disabled.
Then I scanned the images with Trivy. The result was not a simple secure/insecure label. Some services had no Critical findings, some had High findings inherited from a base image, and two application images needed real dependency work before deployment.
The workflow that proved useful was:
- Inventory public routes and Docker origins.
- Scan the exact image currently deployed.
- Separate Critical, High, and lower-severity findings.
- Back up data and configuration before changing anything.
- Build a candidate image without replacing production.
- Scan and smoke-test the candidate locally.
- Deploy only after the result and rollback path are understood.
- Check logs, health endpoints, and login flows afterward.
The biggest lesson was that vulnerability management is a process, not a one-off command. A clean scan today does not replace image pinning, update discipline, backups, or runtime controls tomorrow.
I also learned to record exceptions. A High finding in an unused route is different from the same finding in an authenticated public application. A Critical finding in build-only tooling deserves investigation, but it should not be silently ignored just because the scanner found it in an unusual path.
The inventory and the decision record are as valuable as the scan output. Without them, it is easy to patch the wrong image, forget a disabled route, or lose track of why a finding remains open.
# Inventory running containers and their images
docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Ports}}'
# Scan the exact deployed image and save JSON for comparison
trivy image --severity CRITICAL,HIGH --ignore-unfixed example/app:current
trivy image --format json --output trivy-example-app.json example/app:current
# Test the public route separately from the local origin
curl -I https://service.example.test
curl -I http://127.0.0.1:8080
The names above are placeholders. Scan the exact deployed tag or digest, not merely a similarly named candidate.
The rest of this series covers the defenses that made the scan results actionable.
Comments