DEV Community

Alvarito1983
Alvarito1983

Posted on

I scanned my own Docker images. Here's what I found — and how I built the scanner.

I scanned my own Docker images today.

I wasn't expecting much. These are images I built myself, running in my homelab, on a private network. I update them regularly. I'm a Senior Systems Administrator — I know what I'm doing.

308 vulnerabilities. 10 Critical. 298 High.

The Critical ones? Two axios vulnerabilities — SSRF bypass and Cloud Metadata Exfiltration — sitting in production code I wrote. Fixed in axios 1.15.0. I was running 1.14.0.

This is the Docker security problem nobody talks about.


The invisible attack surface

When you run a Docker container, you're not just running your code. You're running:

  • Your application dependencies (npm packages, pip packages, gems)
  • The base image (node:alpine, python:slim, ubuntu)
  • Every library those pull in transitively
  • The OS-level packages in the container

Each layer is a potential attack surface. And unlike your application code — which you read, review, and update — the dependencies update silently, the base images accumulate CVEs, and the transitive dependencies are invisible.

Most developers have no idea what's actually running inside their containers.


What I built: NEXUS Security

NEXUS Security is a module of the NEXUS Ecosystem — a suite of self-hosted Docker management tools. Security's job is to give you visibility into what's actually running in your containers and flag what's dangerous.

It does two things that most tools don't combine:

CVE scanning with Grype — vulnerability detection against the container's actual content
Hash analysis with VirusTotal — malware detection at the image layer level

These are fundamentally different approaches. Understanding why both matter requires understanding how Docker images actually work.


How Docker images work (the part that matters for security)

A Docker image isn't a monolithic file. It's a stack of layers, each one a filesystem delta from the previous.

nexus-nexus-hub:latest
├── Layer 1: node:22-alpine (base OS + Node.js runtime)
├── Layer 2: npm install (your dependencies)
├── Layer 3: COPY . . (your application code)
└── Layer 4: entrypoint configuration
Enter fullscreen mode Exit fullscreen mode

Each layer has a SHA256 hash. The final image hash is derived from all layers combined.

This structure matters for security in two ways:

CVE scanning looks inside the layers — it reads the package manifests, identifies installed software, and checks each one against vulnerability databases. Grype does this: it extracts the SBOM (Software Bill of Materials) from the image and cross-references against NVD, GitHub Advisory, and other databases.

Hash analysis looks at the layer hashes themselves — it asks "has this specific layer ever been seen containing malware?" VirusTotal has seen billions of files. If a layer hash matches something previously flagged, it surfaces immediately.

Neither approach alone is sufficient. Grype catches known CVEs in legitimate software. VirusTotal catches tampered or malicious images that might pass CVE scanning cleanly.


The Grype integration

Grype is an open source vulnerability scanner from Anchore. It runs as a CLI tool, takes an image reference, and outputs a structured JSON report of every vulnerability found.

Installing it in the Security container:

RUN apk add --no-cache curl && \
    curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh \
    | sh -s -- -b /usr/local/bin
Enter fullscreen mode Exit fullscreen mode

Running a scan from Node.js:

const { execSync } = require('child_process');

async function scanImage(imageRef) {
  const result = execSync(
    `grype ${imageRef} -o json --quiet`,
    { maxBuffer: 50 * 1024 * 1024 }
  );

  const report = JSON.parse(result.toString());

  return report.matches.map(match => ({
    id: match.vulnerability.id,
    severity: match.vulnerability.severity,
    package: match.artifact.name,
    version: match.artifact.version,
    fixedIn: match.vulnerability.fix?.versions?.[0] || null,
    description: match.vulnerability.description
  }));
}
Enter fullscreen mode Exit fullscreen mode

The scan is asynchronous — for large images it takes 30-60 seconds. NEXUS Security returns a scanId immediately and emits a Socket.io event when complete, updating the UI in real time.


What the scan actually found

Running against my own ecosystem — six Docker images I built myself:

Image Critical High Total
nexus-nexus-hub 2 17 29
nexus-nexus-watcher 2 17 29
nexus-nexus-security 0 23 48
nexus-nexus 0 17 26
nginx:latest 0 36 209
tecnativa/docker-socket-proxy 1 23 58

The two Critical in Hub and Watcher were identical: axios 1.14.0 with two separate CVEs:

  • GHSA-3p68-rc4w-qgx5: NO_PROXY Hostname Normalization Bypass → SSRF
  • GHSA-fvcv-3m26-pcqx: Unrestricted Cloud Metadata Exfiltration via Header Injection

Both fixed in axios 1.15.0. I updated all backends immediately.

The nginx findings are worth noting: 209 vulnerabilities, none Critical. This is normal — nginx:latest carries a lot of system library CVEs that have no patches available. The signal-to-noise problem in Docker security is real. A tool that shows you 209 vulnerabilities without context is almost worse than no tool at all.


The VirusTotal integration

VirusTotal's API v3 lets you check a file hash against their database of 70+ antivirus engines. For Docker images, you extract the layer digests and check each one:

async function scanWithVirusTotal(imageRef, apiKey) {
  // Get image manifest to extract layer hashes
  const inspect = JSON.parse(
    execSync(`docker inspect ${imageRef}`).toString()
  )[0];

  const imageId = inspect.Id.replace('sha256:', '');

  // Query VirusTotal
  const response = await fetch(
    `https://www.virustotal.com/api/v3/files/${imageId}`,
    { headers: { 'x-apikey': apiKey } }
  );

  if (response.status === 404) {
    return { status: 'unknown', message: 'Not seen by VirusTotal' };
  }

  const data = await response.json();
  const stats = data.data.attributes.last_analysis_stats;

  return {
    malicious: stats.malicious,
    suspicious: stats.suspicious,
    clean: stats.undetected,
    total: Object.values(stats).reduce((a, b) => a + b, 0),
    permalink: `https://virustotal.com/gui/file/${imageId}`
  };
}
Enter fullscreen mode Exit fullscreen mode

The free VirusTotal tier allows 500 requests/day — more than enough for a homelab or small team.


The event-driven alert system

Scanning is only useful if someone sees the results. NEXUS Security integrates with the Hub's event bus — when a Critical vulnerability is found, it emits an event that flows through the entire ecosystem:

// Security emits when Critical found
await emitEvent('vulnerability.critical', 'critical', {
  cve: vuln.id,
  package: vuln.package,
  version: vuln.version,
  image: imageRef,
  fixedIn: vuln.fixedIn
});
Enter fullscreen mode Exit fullscreen mode
// Hub flow engine receives and routes to Notify
{
  id: 'vulnerability.critical',
  eventType: 'vulnerability.critical',
  action: 'notify',
  message: '🔴 Critical vulnerability: {cve} in {package} ({image}) — fix: {fixedIn}'
}
Enter fullscreen mode Exit fullscreen mode
Grype detects Critical CVE
        ↓
Security emits event to Hub
        ↓
Hub flow engine processes it
        ↓
Notify sends to all active channels
        ↓
Email / Telegram / Discord alert
Enter fullscreen mode Exit fullscreen mode

No polling. No manual checking. The moment a scan finds something Critical, it lands in your inbox.


What's coming when Security ships as a Hub module

The current implementation is functional but early. When NEXUS Security ships as a full Hub module, the plan includes:

Scheduled scanning — automatic scans on a configurable schedule, not just on demand. Images get re-scanned when Watcher detects an update.

Baseline suppression — mark known/accepted vulnerabilities so they don't keep triggering alerts. The noise problem is real; you need a way to say "I know about this one, it has no fix, stop telling me."

Cross-host visibility — Hub already manages multiple hosts. Security will aggregate vulnerability data across all of them. One dashboard, complete picture.

SBOM export — export the Software Bill of Materials for each image in standard formats (SPDX, CycloneDX) for compliance and audit trails.

Fix suggestions — when a vulnerability has a fixed version, Security will suggest the exact package.json or Dockerfile change needed.


The uncomfortable conclusion

I built these tools. I ran them on a private network. I updated them regularly. And I still had two Critical vulnerabilities in production because I wasn't tracking transitive dependency updates in axios.

The uncomfortable truth about Docker security is that you can do everything right — write good code, review PRs, keep your application logic clean — and still be exposed by a dependency you didn't know you were running.

The answer isn't paranoia. It's visibility. Know what's running. Know what's vulnerable. Know what has a fix. Act on the Critical ones.

Everything else is acceptable risk, consciously taken.


NEXUS Security is part of NEXUS Ecosystem — open source, self-hosted Docker management.

  • GitHub: github.com/Alvarito1983
  • Docker Hub: hub.docker.com/u/afraguas1983

docker #security #opensource #selfhosted #devops #programming #devsecops #nodejs

Top comments (0)