Python · Container Security

Container Watch — Docker Runtime Security Monitor

A lightweight Python tool that audits running containers for dangerous misconfigurations — privileged mode, sensitive mounts, exposed sockets, and more — in real time or on demand

← Back to projects

ContainerWatch

A lightweight Docker runtime security monitor that alerts on dangerous container configurations as they happen — no agent, no daemon, no overhead.


The Goal

Container security is mostly configuration security. The exploits that matter in practice aren't usually CVEs in the container runtime — they're misconfigurations: a forgotten --privileged, a docker.sock mounted into a CI runner, a homelab compose file with network: host set for convenience and never removed.

Tools like Falco, Trivy, and Docker Bench catch most of this, but they're heavy. I wanted something I could drop onto any machine with a reachable Docker daemon, read the entire codebase in a sitting, and pipe into whatever detection stack I was already using. ContainerWatch is that tool.


How It Works

The core data structure is a Finding dataclass — severity, rule name, container name, and a plain-English detail string. Every check is a pure function that takes a Docker inspect dict and returns zero or more Finding objects. The inspect dict is exactly what docker inspect returns, which means the same check code works whether you're hitting a live daemon or reading a saved JSON snapshot offline.

Two modes:

  • audit — queries the Docker daemon once, inspects every running container, prints findings sorted critical → high → medium, then exits with code 1 if anything critical or high was found. Clean exit code design makes it usable as a pre-deploy gate or CI step.
  • monitor — subscribes to the Docker events socket and audits each container the moment it starts. Designed to run as a long-lived process — a systemd unit on a build server works well.

Output is either color-formatted terminal text or JSON, so findings can be forwarded directly to a SIEM or a tool like LogHound.


Capabilities

The check suite covers the configurations that actually matter in real-world container escapes and privilege escalations:

Finding Severity Why it matters
--privileged container critical Equivalent to root on the host — no effective isolation
Docker socket mounted (/var/run/docker.sock) critical Container can drive the daemon; escape by launching a new privileged container
Root filesystem or /etc, /proc, /sys bind-mounted (rw) high Read or modify host state directly
Same paths mounted read-only medium Still leaks secrets and host configuration
--pid=host high Container sees and can signal every process on the host
--network=host high Bypasses container network isolation entirely
Dangerous capabilities (SYS_ADMIN, NET_ADMIN, SYS_PTRACE, SYS_MODULE, etc.) high Should never be added without a specific documented reason
--security-opt seccomp=unconfined high Disables syscall filtering — all syscalls reachable from inside the container
--security-opt apparmor=unconfined high Disables AppArmor MAC profile
Port 2375/tcp exposed critical Docker API without TLS — usually an accident with large blast radius
Process running as root medium Addressable with --user; not catastrophic alone but stacks badly with other findings

Findings are sorted critical → low and tagged with the container name. The offline mode — audit --offline --inspect-file snapshot.json — is useful for triaging a host you can't connect to directly, or for static analysis in CI before a container ever runs.


Tech & Tools

  • Python 3.10+ — dataclasses, type hints throughout, no runtime dependencies beyond the Docker SDK
  • docker-py — official Docker SDK; used for both live daemon queries (containers.list(), containers.get()) and the events stream (client.events())
  • argparse — CLI surface: two subcommands (audit, monitor), --json, --no-color, --offline, --inspect-file
  • Exit codes0 clean, 1 critical/high findings, 2 config/connectivity error; designed to fail CI pipelines correctly
  • JSON output — structured findings ready for SIEM ingestion or piping to other tools

The check functions are intentionally pure and isolated. audit() composes them all; audit_many() wraps audit() for a list of containers and returns one sorted result set. Adding a new check means writing one function and adding it to the tuple in audit() — nothing else changes.


What I Learned

On container security:

  • The most impactful misconfigurations are almost never accidental CVEs — they're convenience settings that never got reverted. --privileged and docker.sock mounts are the two that matter most; if either is present, everything else is academic.
  • docker.sock is critical even read-only because socket access equals daemon control. A container with read-only access to the socket can still issue docker run --privileged and escape.
  • Capability semantics are subtle. NET_RAW sounds narrow but lets a container craft arbitrary packets on the host network. SYS_ADMIN is essentially root. Normalizing capability names (stripping the CAP_ prefix, uppercasing) was necessary because Docker and the Linux kernel don't always agree on format.

On tool design:

  • Pure check functions that take and return plain data are much easier to test than anything that reaches out to a daemon. Every check in this codebase can be tested with a dict literal — no Docker required.
  • Exit code discipline matters for security tooling. Returning 1 on findings and 2 on errors gives CI pipelines a clean way to distinguish "this container is misconfigured" from "we couldn't check."
  • Offline mode (reading a saved docker inspect JSON) turned out to be more useful than I expected — it means you can collect evidence from a host and analyze it somewhere else, which matters for incident triage.

On the detection problem:

  • Runtime monitoring catches what static analysis misses: containers started interactively, CI runners spun up mid-pipeline, or images pulled and run without going through any review gate. Both modes matter.
  • The gap between "tool found it" and "someone acted on it" is where most security tooling fails. Structured JSON output and well-defined exit codes are the minimum viable interface for getting findings into something that pages a human.