---
name: audit-docker-compose
description: Audit a Docker Compose file for security issues. Use when reviewing a docker-compose.yml for exposed ports, secrets in environment variables, missing network segmentation, Docker socket mounts, missing resource limits, or other security misconfigurations. Outputs prioritized findings with remediation snippets.
argument-hint: "[path/to/docker-compose.yml]"
---

# Audit Docker Compose Security

Audit a Docker Compose file (and any override files) against the 12-check security checklist. Output findings inline, organized by severity, with remediation snippets.

## Input

The path to the Docker Compose file is: `$ARGUMENTS`

If `$ARGUMENTS` is empty, auto-detect from the current directory by checking for these files in order:
1. `docker-compose.prod.yml`
2. `docker-compose.yml`
3. `docker-compose.yaml`

If none are found, report that no Docker Compose file was detected and ask the user to provide the path.

## Multi-File Compose Setup

After identifying the primary compose file, also check for companion files in the same directory:
- `docker-compose.override.yml`
- `docker-compose.override.yaml`

Read all files found. When a finding references configuration that comes from an override file rather than the primary file, note this explicitly in the finding location (e.g., `docker-compose.override.yml:service:nginx`).

## Checklist

Read the full checklist from `${CLAUDE_SKILL_DIR}/references/checklist.md`. It contains 12 checks with pass/fail criteria and remediation examples.

## Analysis Steps

Read and parse all compose files found. Then evaluate each check below.

For each check, determine:
- **PASS** — the configuration meets the pass condition
- **FAIL** — the configuration violates the check (record a finding)
- **SKIP** — the check is not applicable (e.g., no database services present for the internal networks check; explain why)

### Check 1: Port Bindings — Localhost Only

For every `ports:` entry across all services:
- FAIL if the binding omits a host address (e.g., `"8080:8080"` — binds to `0.0.0.0`)
- FAIL if the binding explicitly uses `0.0.0.0` (e.g., `"0.0.0.0:8080:8080"`)
- PASS if the binding uses `127.0.0.1:` explicitly
- Exception: a reverse proxy service (Caddy, Nginx, Traefik) binding to `0.0.0.0` on ports 80 or 443 is acceptable — note it as a known pattern, not a finding

Record each violating port binding individually. Include the service name and port.

### Check 2: Internal Networks for Databases

Identify all database services: look for images containing `postgres`, `mysql`, `mariadb`, `redis`, `mongo`, `elasticsearch`, `clickhouse`, `mssql`.

For each database service:
- FAIL if it has no `networks:` key (lands on the default bridge network alongside internet-facing services)
- FAIL if the network it is on is not declared with `internal: true`
- PASS if the database is on a dedicated network with `internal: true`

If no database services are present, SKIP with a note.

### Check 3: Non-Root User Directive

For every long-running service (exclude one-shot init containers — identified by `command:` containing `init`, by service name containing `init`, or by `restart: no`/`restart: "no"`):
- FAIL if `user:` is not set (container runs as image default, often root)
- FAIL if `user:` is explicitly `"0"` or `"0:0"` or `"root"`
- PASS if `user:` is set to a non-root value

Note: `user: "${AIRFLOW_UID:-50000}:0"` — the primary UID is non-root even if the group is 0, treat as PASS.

### Check 4: Capability Drop

For every service:
- PASS if `cap_drop: [ALL]` (or equivalent list form) is present
- PASS if the service keeps default capabilities **with a documented reason** because its
  entrypoint requires them — e.g. an official image that `gosu`/`su-exec`-drops from root
  (postgres, redis, mariadb), or a root init container that `chown`s a mounted volume
- FAIL if no `cap_drop:` key is present and there is no such documented reason
- FAIL if `cap_drop:` does not include `ALL` without justification

Note any `cap_add:` entries in the finding — they may be justified but should be reviewed.

When recommending `cap_drop: [ALL]`, do not present it as a universal drop-in. See the
"⚠️ not a safe drop-in for every service" note in `${CLAUDE_SKILL_DIR}/references/checklist.md` for the
common services that need specific capabilities restored (init containers that `chown`;
root sidecars that read mode-restricted mounted files → `DAC_READ_SEARCH`; gosu/su-exec
images). Flag that the change must be validated by **starting the stack** (init exits 0,
gosu-based services reach healthy), not by static inspection alone.

### Check 5: Secrets vs Environment Variables

Scan all `environment:` blocks and `env_file:` references across all services.

Flag as FAIL any environment variable whose name or value pattern suggests it holds a credential:
- Names containing: `PASSWORD`, `SECRET`, `KEY`, `TOKEN`, `CONN`, `FERNET`, `DSN`, `URL` (when the value looks like a connection string)
- Values matching patterns like: `postgresql://`, `mysql://`, `amqp://`, connection strings with embedded credentials

Report the variable name and service only — do NOT report the value.

PASS condition: no such variables found, or all sensitive config uses Docker Compose `secrets:` blocks.

### Check 6: Docker Socket Mount Exposure

Scan all `volumes:` entries (both service-level and top-level `x-` anchors) for `/var/run/docker.sock`.

For each mount found:
- If the service name clearly indicates Docker-in-Docker or DockerOperator use (service name contains `docker-proxy`, `dind`, `docker`, or similar): flag as **Important** and note it as a known architectural pattern, but still record the finding — the risk exists regardless of intent
- For all other services: flag as **Important** with full remediation

### Check 7: Image SHA Pinning

For every `image:` reference across all services and `x-` anchors:
- FAIL if the image tag does not include `@sha256:`
- This applies to both hardcoded image names and variable-substituted names (e.g., `${AIRFLOW_IMAGE_NAME:-apache/airflow:3.0.3}`)
- PASS if `@sha256:<digest>` is present

This check generates a finding per service, but group multiple services with the same base image into one finding to keep output concise.

### Check 8: Resource Limits

For every service:
- FAIL if no `deploy.resources.limits` block is present (also check legacy `mem_limit` / `cpus` top-level keys)
- PASS if at least `memory` and `cpus` limits are set

### Check 9: Log Rotation

For every service:
- FAIL if no `logging:` key is set (defaults to `json-file` with no size limit)
- FAIL if `logging.driver` is `json-file` (or absent) without `max-size` and `max-file` options
- PASS if any of these are true: (a) `max-size` and `max-file` are configured, (b) an external log driver is used (`loki`, `splunk`, `awslogs`, `gelf`, `fluentd`)

### Check 10: Read-Only Filesystem

For every service:
- FAIL if `read_only: true` is not set
- PASS if `read_only: true` is present

Note whether `tmpfs:` mounts are configured for write-needed paths when `read_only: true` is set (a best-practice note, not a separate finding).

### Check 11: no-new-privileges Security Option

For every service:
- FAIL if `security_opt:` does not include `no-new-privileges:true`
- PASS if `security_opt: [no-new-privileges:true]` (or equivalent) is present

### Check 12: Health Check Configuration

For every service that accepts connections (exclude one-shot init containers and proxy/sidecar containers whose health is monitored by the service they back):
- FAIL if no `healthcheck:` block is defined
- PASS if `healthcheck:` is defined with a `test:` command

## Output Format

Output findings directly in the conversation. Do NOT write to a file — findings are shown inline.

### Severity Emoji

- 🔴 **Critical** — immediate security risk; fix before going to production
- 🟠 **Important** — significant hardening gap; fix in near term
- 🟡 **Recommended** — security best practice; fix when practical
- ⚪ **Note** — informational; no action required but worth knowing

### Findings Section

Output a `## Findings` section. Group findings by severity (Critical first, then Important, Recommended, Notes). Within each group, list findings in order of their check number.

For each finding use this format:

```
### [SEVERITY EMOJI] [SEVERITY] — [Short Title]

**Service**: `<service-name>` (or `all services` / list of services)
**Location**: `<filename>:<service-or-line-context>`
**Issue**: One sentence describing what was found.
**Risk**: One sentence explaining the security consequence.
**Remediation**:
<before/after code snippet from the checklist, adapted to the actual service config>
```

If there are no findings for a severity level, omit that section header.

### Summary Section

After all findings, output a `## Summary` section:

```
## Summary

| Severity | Count |
|---|---|
| 🔴 Critical | N |
| 🟠 Important | N |
| 🟡 Recommended | N |
| ⚪ Note | N |

### Checks Passed
- [List each check that passed, one line each]

### Checks Skipped
- [List each check that was skipped, with the reason]
```

End with a one-sentence overall assessment: e.g., "This compose file has critical exposure risks that should be addressed before the service is accessible from the internet." or "Configuration is well-hardened with only minor recommended improvements remaining."
