Metadata-Version: 2.4
Name: compose-harden
Version: 0.1.0
Summary: Auto-harden any docker-compose.yml (cap_drop, no-new-privileges, read_only, tmpfs) and generate a human-readable audit report explaining every change.
Author: Joshua Michael
License: MIT
Project-URL: Homepage, https://github.com/joshua-michael/compose-harden
Project-URL: Issues, https://github.com/joshua-michael/compose-harden/issues
Keywords: docker,docker-compose,security,hardening,container-security,audit,devsecops
Classifier: Environment :: Console
Classifier: Intended Audience :: System Administrators
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Security
Classifier: Topic :: System :: Systems Administration
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: click>=8.0
Requires-Dist: PyYAML>=6.0
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Dynamic: license-file

# compose-harden

[![CI](https://github.com/joshua-michael/compose-harden/actions/workflows/ci.yml/badge.svg)](https://github.com/joshua-michael/compose-harden/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)

Auto-harden any `docker-compose.yml` and get a document explaining every
change it made — and every issue it noticed but refused to guess at.

```bash
$ compose-harden docker-compose.yml --dry-run
```

```diff
   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?** A real, generated
example is checked into this repo:
[`examples/docker-compose.yml`](examples/docker-compose.yml) (input) →
[`examples/sample-output/docker-compose.hardened.yml`](examples/sample-output/docker-compose.hardened.yml)
+ [`examples/sample-output/AUDIT.md`](examples/sample-output/AUDIT.md) (output).

## 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. **apply** the standard container-hardening controls automatically,
   without breaking the file's comments or formatting
3. **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 one job.

## What it changes

| Control | What it does | Why |
|---|---|---|
| `cap_drop: [ALL]` | Drops every Linux capability | Shrinks what a compromised process can do to almost nothing |
| `security_opt: [no-new-privileges:true]` | Blocks privilege escalation via setuid binaries | Closes a whole class of local priv-esc |
| `read_only: true` | Makes the root filesystem read-only | A code-exec bug can't persist anything on disk |
| `tmpfs: [/tmp, ...]` | Adds in-memory writable scratch space | Keeps the app working under `read_only` |

Full rationale for each is in [`compose_harden/rules.py`](compose_harden/rules.py)
and gets reproduced in the generated `AUDIT.md` for the exact services it
touched.

## What it deliberately does *not* auto-fix

`compose-harden` will **detect and report** these, but won't silently
change them, because a wrong guess here can break your service outright:

- **`privileged: true`** — sometimes genuinely required; needs a human to
  say why.
- **Unpinned images** (`:latest` or no tag) — this tool doesn't know which
  pinned version/digest is safe for your registry.
- **No non-root `user:`** — the correct UID is image-specific; guessing
  wrong breaks filesystem permissions on startup.

Every flagged item shows up in the audit report with the reasoning, so you
can decide, not the tool.

## 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` 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

```bash
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

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

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

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

# Skip a rule you don't want applied
compose-harden docker-compose.yml --skip read_only --skip tmpfs

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

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

Run `compose-harden --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

- Only edits `cap_drop`, `security_opt` (no-new-privileges), `read_only`,
  and `tmpfs`. It does not touch networking, port bindings, secrets,
  resource limits, or healthchecks — 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

```bash
pip install -e ".[dev]"
pytest
```

## License

MIT — see [LICENSE](LICENSE).
