Skip to main content

Auto-harden and audit any docker-compose.yml: read-only `scan` with a pass/fail score (JSON/SARIF for CI), and `fix` to apply cap_drop, no-new-privileges, read_only, and tmpfs with a human-readable audit report.

Project description

compose-harden

CI License: MIT

Scan any docker-compose.yml for container-hardening gaps, get a score, then auto-fix what's safe to auto-fix — with a document explaining every change, and every issue it noticed but refused to guess at.

$ compose-harden scan docker-compose.yml
Service: web
  ✖ cap_drop: ALL           -- no `cap_drop: [ALL]`
  ✖ no-new-privileges       -- missing `no-new-privileges` in security_opt
  ✖ read_only root filesystem -- root filesystem is writable
  ✔ not running privileged
  ✔ image version pinned
  ✖ non-root user set       -- no non-root `user:` configured
  ✔ not using host network
  ✔ seccomp not disabled

Score: 50/100  (8/16 checks passed)
$ compose-harden fix docker-compose.yml --dry-run
   depends_on:
     - db
+    cap_drop:
+      - ALL
+    security_opt:
+      - no-new-privileges:true
+    read_only: true
+    tmpfs:
+      - /tmp

...plus an AUDIT.md that explains why each line was added, in plain language, service by service.

Don't want to run it just to see what it does? Real, generated examples are checked into this repo:

Input Output
examples/docker-compose.yml scan-output.txt · scan-output.json · scan-output.sarif
same file docker-compose.hardened.yml · AUDIT.md

Why this exists

There are great tools for scanning a Docker setup for problems (docker-bench-security, Trivy, CrowdSec, Docker's own Hardened Images at the base-image level). There's nothing that closes the loop end to end:

  1. read an existing, real-world docker-compose.yml
  2. score it against known hardening controls, in a format a CI pipeline can gate on
  3. apply the ones that are safe to auto-fix, without breaking the file's comments or formatting
  4. explain, in a document a non-security person can read, what changed and why — and just as importantly, what it refused to touch automatically and why

compose-harden does exactly that.

What it checks

Check Auto-fixed by fix? What it means
cap_drop: [ALL] Yes Drops every Linux capability, shrinking what a compromised process can do
security_opt: [no-new-privileges:true] Yes Blocks privilege escalation via setuid binaries
read_only: true Yes Root filesystem can't be written to at runtime
tmpfs scratch space Yes (when read_only is active) Keeps the app working under read_only
Not privileged: true No — flagged only Privileged mode disables nearly all isolation at once
Image version pinned No — flagged only :latest/no tag means the image can silently change under you
Non-root user: set No — flagged only The correct UID is image-specific; a wrong guess breaks startup
Not network_mode: host No — flagged only Host networking removes the container's network namespace entirely
seccomp not unconfined No — flagged only Only fires if seccomp was explicitly turned off; Docker's default profile is fine and isn't flagged

Full rationale for each is in compose_harden/rules.py and gets reproduced in the generated AUDIT.md for the exact services it touches. The four auto-fixable ones need no service-specific knowledge to apply safely; the rest depend on things only the operator knows (why privileged mode is on, what UID an image expects), so they're surfaced, never guessed at.

Deliberately not in scope: healthchecks, restart policies, resource limits. Those are operability/reliability concerns, not attack-surface ones — folding them into a "security score" would make the score mean two different things at once.

CI/CD integration

scan is built to gate a pipeline:

compose-harden scan docker-compose.yml --min-score 80   # exits 1 if score < 80
compose-harden scan docker-compose.yml --json           # machine-readable
compose-harden scan docker-compose.yml --sarif out.sarif  # for GitHub Code Scanning

A ready-to-copy workflow that scans on every PR and uploads findings to GitHub's Security tab is at examples/github-actions/compose-harden.yml.

Design principle: never re-serialize your YAML

Most tools that "fix" a YAML file parse it into a data structure, edit the structure, and dump it back out — which silently strips comments, reorders keys, and changes quoting style. compose-harden fix never does this. It reads your file as plain text, decides what to change using a read-only parse, and then makes surgical line insertions at the correct indentation. Everything you didn't ask it to touch comes out byte-for-byte identical.

Install

pip install compose-harden   # once published
# or, from source:
git clone https://github.com/joshua-michael/compose-harden
cd compose-harden
pip install -e .

Usage

# Read-only check + score, exit 1 if not fully clean -- safe to run anywhere
compose-harden scan docker-compose.yml

# Same, but gate CI at a lower bar while you migrate an existing project
compose-harden scan docker-compose.yml --min-score 80

# Show what fix would change + full audit report, write nothing
compose-harden fix docker-compose.yml --dry-run

# Write docker-compose.hardened.yml + AUDIT.md next to it
compose-harden fix docker-compose.yml

# Overwrite in place (keeps a .bak backup automatically)
compose-harden fix docker-compose.yml --in-place

# Skip a check/rule you don't want applied or scored
compose-harden scan docker-compose.yml --skip host_network
compose-harden fix docker-compose.yml --skip read_only --skip tmpfs

# Add extra writable paths some images need alongside read_only
compose-harden fix docker-compose.yml --extra-tmpfs /var/cache/nginx

# Custom output locations
compose-harden fix docker-compose.yml -o hardened.yml --audit-output SECURITY-CHANGES.md

Run compose-harden scan --help or compose-harden fix --help for the full flag list.

How it decides what's safe to touch

  • If a control is already present and correct → skipped, marked "already present" in the audit (the tool is idempotent — running it twice is a no-op the second time).
  • If a control is present but customized (e.g. you already listed specific capabilities in cap_drop instead of ALL) → left untouched and flagged as a conflict for manual review, instead of overwriting your deliberate choice.
  • If a control is absent → added, using the same indentation style as the rest of your file.
  • tmpfs is only added when read_only is actually active for that service — there's no point adding writable scratch space you don't need.

Limitations

  • fix only edits cap_drop, security_opt (no-new-privileges), read_only, and tmpfs. It does not touch networking, port bindings, secrets, or resource limits — that's a deliberate scope boundary, not an oversight.
  • Assumes conventional 2-space YAML indentation and a top-level services: map (the vast majority of real-world compose files). YAML anchors/aliases and extends: are not specially handled yet.
  • This is a fast first pass, not a substitute for an actual security review or for Docker's own Hardened Images at the base-image layer — use both.

Development

pip install -e ".[dev]"
pytest

See CONTRIBUTING.md for how the rule/check engine is structured before adding a new one.

License

MIT — see LICENSE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

compose_harden-0.2.0.tar.gz (23.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

compose_harden-0.2.0-py3-none-any.whl (21.3 kB view details)

Uploaded Python 3

File details

Details for the file compose_harden-0.2.0.tar.gz.

File metadata

  • Download URL: compose_harden-0.2.0.tar.gz
  • Upload date:
  • Size: 23.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for compose_harden-0.2.0.tar.gz
Algorithm Hash digest
SHA256 163481a68f91f4f24f49223249ea2e4d43eb9d7cd4522a7b8bc3c6da82b08090
MD5 8078ae238daefcb3cf874db43cf2579a
BLAKE2b-256 9514c4e71255800d214a04b2ca46882582d6390e0e68531347ba63a8e6183516

See more details on using hashes here.

Provenance

The following attestation bundles were made for compose_harden-0.2.0.tar.gz:

Publisher: publish.yml on joshua-michael/compose-harden

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file compose_harden-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: compose_harden-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 21.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for compose_harden-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 6ce85bb24f2c44dc4bc50791d78b57935b7f231c0b688345de10d611ab073815
MD5 ab0c478b7ae6de373fe2d430a5dc5276
BLAKE2b-256 b7a74b47261fcd03dfca7d0593944a1eca992324838cc8ce47ec14c762062b14

See more details on using hashes here.

Provenance

The following attestation bundles were made for compose_harden-0.2.0-py3-none-any.whl:

Publisher: publish.yml on joshua-michael/compose-harden

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page