Skip to main content

🛡️ container-audit

Read-only security audit for Docker hosts.

Point it at your containers and it reports the risky configuration — root users, exposed ports, secrets in environment variables, privileged mode, dangerous capabilities, mounted Docker sockets, shared host namespaces, outdated images and missing hardening — as a clean text, JSON, Markdown or SARIF report.

CI PyPI defensive read-only python license

Quick start · What it checks · Usage · How it works · Safety


🚀 Quick start

No Docker needed to try it — the repo ships a sample docker inspect dump:

git clone https://github.com/petrhromadka/container-audit
cd container-audit
uv run container-audit --input tests/fixtures/sample_inspect.json

You'll see eleven findings on the intentionally-insecure bad-db container and a clean bill of health for the hardened good-web one. To scan your own host instead, just run container-audit — it reads live containers via docker ps / docker inspect.


🔍 What it checks

Severity key: 🔴 critical · 🟠 high · 🟡 medium · 🔵 low. Each finding carries a title, a plain-language explanation, short redacted evidence, and a one-line remediation.

Privilege & escape

Check id What Why it matters
🔴 docker-socket-mount /var/run/docker.sock is mounted in Socket access is effectively root on the host: it can start any other container.
🔴 privileged Container runs --privileged Disables most isolation: full device access and all capabilities.
🟠 root-user Runs as root (no USER, or 0/root) A process escape or file-permission bug has root inside the container.
🟠 dangerous-caps Caps like SYS_ADMIN, NET_ADMIN, SYS_PTRACE added Common container-escape building blocks.
🟠 sensitive-host-mount Host paths (/, /etc, /proc, /root, …) bind-mounted A compromised container can read or alter host config and credentials.

Namespaces & exposure

Check id What Why it matters
🟠 host-pid Shares the host PID namespace (--pid=host) It can see and signal every process on the host.
🟠 host-network Shares the host network namespace (--network=host) Network isolation and port mapping are bypassed.
🟡 port-all-interfaces Port published on 0.0.0.0 / :: Reachable from every network the host is on, not just where you intend.
🟡 host-ipc Shares the host IPC namespace (--ipc=host) Shared memory and semaphores cross the container boundary.
🔵 host-uts Shares the host UTS namespace (--uts=host) It can change the host's hostname.

Secrets & hardening

Check id What Why it matters
🟠 secret-in-env Env var that looks like a secret, or a URL with embedded credentials Secrets in env vars leak via docker inspect, logs and child processes.
🟡 no-new-privileges-missing no-new-privileges:true is not set setuid binaries inside the container can still escalate privileges.
🔵 writable-rootfs Root filesystem is writable (not read_only) An attacker can persist changes and tooling.
🟡 stale-image Image was built more than 180 days ago Old images usually miss security patches released since; rebuild or pull a current one.
🔵 unpinned-image Image uses :latest, no tag, or no digest Pulls aren't reproducible; upstream can silently change what runs.
🔵 unlimited-memory No memory limit set A single container can exhaust host RAM and take the host down.

📦 Install

Requires Python 3.12+ and uv. The docker CLI is only needed to audit a live host; auditing a saved file with --input does not need it.

uv tool install .                 # install as a tool from a checkout
uvx --from . container-audit      # or run without installing
uv run container-audit            # or run inside the project environment
uv run python -m ca               # module form (use this if the console script is blocked)

On Windows, Smart App Control may block the generated container-audit.exe launcher. Run the tool as a module instead: python -m ca (or uv run python -m ca).

Once published to PyPI: pipx install docker-host-audit (then run container-audit), or uvx docker-host-audit.

Run with Docker

The image ships the tool (the Docker CLI is intentionally not bundled); feed it a saved inspect dump on stdin:

docker build -t container-audit .
docker inspect $(docker ps -aq) $(docker inspect -f '{{.Image}}' $(docker ps -aq) | sort -u) | docker run --rm -i container-audit --input /dev/stdin --fail-on high

💻 Usage

container-audit                                             # audit running containers
container-audit --include-stopped                          # include stopped ones too
container-audit --format json                              # machine-readable output
container-audit --format markdown > report.md              # for an issue / PR / wiki

docker inspect $(docker ps -aq) $(docker inspect -f '{{.Image}}' $(docker ps -aq) | sort -u) > sample.json  # save containers + their images
container-audit --input sample.json --fail-on high         # audit the dump; non-zero exit for CI
Flag Meaning
--input FILE Read docker inspect JSON from FILE instead of calling Docker.
--format FORMAT Output format: text (default), json, markdown or sarif.
--ignore CHECK_ID Suppress findings with this check id (e.g. docker-socket-mount). Repeatable.
--include-stopped Also inspect containers that are not running.
--include-host Include the real hostname in the report (off by default, so output is safe to share).
--min-severity LEVEL Hide findings below LEVEL (info, low, medium, high, critical). Default info (show all).
--fail-on LEVEL Exit non-zero if any shown finding is at or above LEVEL (none, low, medium, high, critical). Default none.
--version Print the version and exit.

Exit codes: 0 clean (nothing reached --fail-on) · 1 a finding met --fail-on · 2 could not run (Docker unavailable, or a missing/invalid --input file).

Image age comes from docker inspect --type image. If an image was deleted while its container still runs, or the dump has no image objects, the age is unknown and stale-image skips that container. Images from reproducible builds (distroless, ko, Nix) carry a fixed 1970 date and are skipped too.

Coolify and other self-hosted PaaS

It works unchanged on a Coolify host. Expect docker-socket-mount on Coolify's own coolify-proxy (Traefik) and coolify-sentinel containers: they need the socket by design. Review everything else, especially apps you deployed from public templates, which often publish database ports on 0.0.0.0 and keep passwords in environment variables.

Use in CI

- name: Audit containers
  run: |
    docker inspect $(docker ps -aq) $(docker inspect -f '{{.Image}}' $(docker ps -aq) | sort -u) > containers.json
    uvx --from git+https://github.com/petrhromadka/container-audit \
      container-audit --input containers.json --fail-on high

GitHub code scanning (SARIF)

--format sarif renders the findings as SARIF, the format GitHub reads for code scanning. Each finding becomes a result keyed by its check id, so they show up in the repository's Security → Code scanning tab with severity and remediation.

container-audit --input sample.json --format sarif > results.sarif

The bundled security-scan.yml workflow does exactly this on every push and pull request and uploads results.sarif via github/codeql-action/upload-sarif, so findings surface in the Security tab without failing the build.


⚙️ How it works

docker ps / docker inspect  ->  parse into Container objects  ->  run checks  ->  render report
       (or --input file)                                        (pure functions)   text / json / markdown
  1. Collect — list containers with docker ps and read their config with docker inspect (or parse a saved copy via --input).
  2. Check — each check is a pure function over the parsed containers that yields findings. Checks never talk to Docker and never change anything.
  3. Report — findings are sorted by severity and rendered as text, JSON, Markdown or SARIF; with --fail-on, the exit code reflects the worst severity found.
📋 Example report (from the bundled sample fixture)
container-audit report
Host:      (redacted)
Scanned:   2 container(s)
Timestamp: 2026-11-05T19:32:19+00:00
Findings:  11 (critical=2, high=3, medium=4, low=2, info=0)

[CRITICAL] docker-socket-mount  bad-db - Access to the Docker socket grants control of the Docker daemon, and therefore of the host.
    evidence:    /var/run/docker.sock -> /var/run/docker.sock (rw)
    remediation: Remove the socket mount or use a restricted docker-socket-proxy.
[CRITICAL] privileged  bad-db - Privileged mode grants all capabilities and host device access, so a compromise is effectively a host compromise.
    evidence:    HostConfig.Privileged=true
    remediation: Remove `privileged: true`; add only the capabilities needed.
[HIGH] dangerous-caps  bad-db - These capabilities allow administrative or host-affecting operations and weaken container isolation.
    evidence:    cap_add: SYS_ADMIN
    remediation: Remove these from `cap_add`; grant only what is required.
[HIGH] root-user  bad-db - The main process runs as UID 0, widening the impact of an escape.
    evidence:    Config.User=''
    remediation: Set a non-root USER in the image or `user:` in compose.
[HIGH] secret-in-env  bad-db - Environment variable DB_PASSWORD looks like a secret and has a literal value; it is visible via docker inspect and to anyone who can read the container configuration.
    evidence:    DB_PASSWORD=***
    remediation: Use Docker secrets or an env file kept outside the image.
[MEDIUM] host-ipc  bad-db - The container can access host shared memory and IPC objects, weakening isolation.
    evidence:    HostConfig.IpcMode=host
    remediation: Remove `ipc: host`; do not share the host IPC namespace.
[MEDIUM] no-new-privileges-missing  bad-db - Without no-new-privileges, setuid binaries inside the container can escalate privileges.
    evidence:    HostConfig.SecurityOpt lacks no-new-privileges
    remediation: Add `security_opt: ["no-new-privileges:true"]`.
[MEDIUM] port-all-interfaces  bad-db - The port is reachable from every network the host is attached to.
    evidence:    0.0.0.0:5432 -> 5432/tcp
    remediation: Bind to 127.0.0.1 (e.g. 127.0.0.1:<port>:<port>) unless public.
[MEDIUM] stale-image  bad-db - The image is 1390 days old, so it likely lacks security patches released since it was built.
    evidence:    image built 2023-01-15 (1390 days ago)
    remediation: Pull or rebuild the image on a current base and recreate the container; automate updates (e.g. Renovate, Dependabot).
[LOW] unlimited-memory  bad-db - With no memory limit, one container can exhaust the host's RAM.
    evidence:    HostConfig.Memory=0
    remediation: Set a memory limit (`mem_limit` in compose).
[LOW] writable-rootfs  bad-db - A writable root filesystem lets an attacker persist changes.
    evidence:    HostConfig.ReadonlyRootfs=false
    remediation: Set `read_only: true` and use tmpfs or volumes for writable paths.

The second container, good-web, is hardened (non-root, read-only rootfs, memory limit, no-new-privileges, port bound to 127.0.0.1) and produces no findings.


🔒 Safety

  • Read-only. The only Docker commands used are docker ps and docker inspect. Nothing is started, stopped, restarted, or reconfigured.
  • Secret values are never printed. For env vars that look like secrets, only the variable name is reported, with the value masked to *** — no characters of the value appear in any output format.
  • Hostname hidden by default. Reports show (redacted) as the host unless you pass --include-host.
  • Output is sanitized. Control and terminal-escape characters from container metadata are stripped, so a report is safe to print in a terminal or paste into Markdown.
  • No network use, no runtime dependencies. The tool never transmits data anywhere.
  • Reports can still reveal your container names, image names, ports and mount paths — review one before sharing it publicly.

🧰 Development

uv sync                      # create the environment
uv run ruff check .          # lint
uv run ruff format --check . # formatting
uv run ty check src          # type check
uv run pytest                # tests

Pre-commit hooks (ruff, gitleaks, whitespace fixes) live in .pre-commit-config.yaml; install them with prek or pre-commit. CI runs the same checks plus a gitleaks scan over the full history.

To add a check, write a function that takes list[Container] and yields Finding objects, then register it in the CHECKS list of the relevant module under src/ca/checks/.


📄 License

MIT © 2026 Petr Hromádka

Metadata

Release files for docker-host-audit 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for docker-host-audit 0.2.0
File Size Uploaded Provenance Actions
docker_host_audit-0.2.0.tar.gz 18.4 kB With provenance Details for docker_host_audit-0.2.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for docker-host-audit 0.2.0
File Interpreter ABI Platform Actions
docker_host_audit-0.2.0-py3-none-any.whl Python 3 none any Details for docker_host_audit-0.2.0-py3-none-any.whl

Total release size: 40.4 kB

Release files / docker_host_audit-0.2.0.tar.gz

Download URL docker_host_audit-0.2.0.tar.gz
Size 18.4 kB
Tags Source
SHA-256 checksum
How to use checksums
56983a1674ea62809dc0788e719655e32f12e8d32bc66796ce282635d86c2f5e
BLAKE2b-256 checksum
How to use checksums
bf6e72d7799b1ed5d2d0c12b9fb2d3ee311ed807c7bc8de4dc20f6c10a468804
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 5, 2026.

Transparency log

Release files / docker_host_audit-0.2.0-py3-none-any.whl

Download URL docker_host_audit-0.2.0-py3-none-any.whl
Size 22.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
b15303c512ab089d7208473ba8318df0f4a3cc6e5fd3f5c725c575113369097b
BLAKE2b-256 checksum
How to use checksums
3e6716d57e6d15975ba9ef97322d8b6ce8a7b0a48e19004474011924428879c2
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 5, 2026.

Transparency log

Security for docker-host-audit 0.2.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for docker_host_audit-0.2.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 5, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page