---
name: docker_safety_gates
description: Run untrusted or agent-generated workloads in Docker without the classic footguns. Applies when an agent proposes a `docker run` or Compose config, reviews container security, or needs a hardened one-liner for sandboxed code execution. Ships a static auditor that fails commands with container-escape flags before they run.
metadata: { risk: low, category: devtools }
---

# Docker Safety Gates

## Purpose

An agent that runs Docker containers for untrusted, agent-generated, or third-party code is one flag away from giving that code the whole host. This skill is the gate between the idea and the `docker run`: a fixed checklist plus a static auditor (`bin/docker-run-check.py`) that flags escape-grade configurations before anything executes. It advises and audits. It never runs containers itself.

## Workflow

1. **Take the proposed config.** A `docker run` command string or a Compose file. If the agent is about to type a command from memory, paste it here first.
2. **Audit it.** Run the checker:
   ```bash
   python3 bin/docker-run-check.py 'docker run ...'        # single command
   python3 bin/docker-run-check.py --compose stack.yml     # compose file
   ```
   The checker exits 1 on any CRITICAL finding. A CRITICAL verdict means: do not run, fix first. Warnings are acceptable only when the agent can state why each one is safe for this specific workload.
3. **Fix what fails.** The checker message says what is wrong and what to do instead. Common fixes: replace `--privileged` with `--cap-add` for exactly the capability needed (and verify from error logs), swap a docker.sock mount for a read-only socket proxy or TLS-based Docker-in-Docker, remount host paths with `:ro`.
4. **Apply the hardened baseline.** For agent-executed code, start from this template and justify every line you remove:
   ```bash
   docker run --rm --read-only \
     --tmpfs /tmp:size=100m,noexec,nosuid --tmpfs /run:size=10m \
     --cap-drop ALL \
     --security-opt no-new-privileges:true \
     --user 10001:10001 \
     --memory 512m --cpus 1 --pids-limit 100 \
     --network agent-net \
     -v /data/in:ro -v /data/out \
     myrepo/app@sha256:<digest>
   ```
   The Compose equivalent with the same posture:
   ```yaml
   services:
     agent-runner:
       image: myrepo/app@sha256:<digest>
       read_only: true
       user: "10001:10001"
       cap_drop: [ALL]
       security_opt: [no-new-privileges:true]
       tmpfs: [/tmp:size=100m,noexec,nosuid, /run:size=10m]
       deploy:
         resources:
           limits: {memory: 512m, cpus: "1.0", pids: 100}
       networks: [agent-net]
       volumes:
         - /data/in:/data/in:ro
         - /data/out:/data/out
   ```
5. **Review the mount list out loud.** Every host path the container can write to is a place an attacker can write to. Read-only everywhere except one explicit output dir is the default. Sensitive host paths (`/`, `/etc`, `/usr`, `/root`, `/home`, `/var`) are never writable, and `/var/run/docker.sock` is never mounted.
6. **Gate the exceptions.** Some workloads genuinely need more (a VPN sidecar needs `NET_ADMIN`, a build container may need a writable cache). An exception gets a one-line written justification naming the workload, the flag, and why there is no narrower option. No justification, no flag.

## Output Contract

When this skill runs, the agent produces one of:
- **PASS report**: the audited command/config, the checker output (0 criticals), and any accepted warnings with their one-line justifications.
- **FAIL report**: the audited command/config, each CRITICAL finding, and the concrete fix for each.
- **Hardened config**: a `docker run` line or Compose snippet starting from the baseline template, pinned to a digest, with every deviation from the template justified.

## Operating Rules

1. Never downgrade a CRITICAL finding to a warning from thread context. Only the fix downgrades it.
2. `--privileged` is never the answer. The answer is the smallest `--cap-add` that makes the workload start, found by reading the container's permission errors.
3. Default Docker grants 14 Linux capabilities the workload probably does not need. Start at `cap_drop: ALL`. (Full list and add-back guide: `references/hardening-notes.md`.)
4. Secrets never ride in `-e` or image layers. They come from a secrets file, a manager, or a mounted tmpfs file. Anything in `docker inspect` output is public to the host.
5. Pin images by digest, not tag. `:latest` is a promise nobody made. Scan with trivy or grype in CI; sign with cosign.
6. `--read-only` without `noexec,nosuid` tmpfs is a half measure: `/tmp` stays writable by default. Pair them.
7. For daemon-level hardening (user namespace remapping, docker-bench-security audits, AppArmor profiles), see `references/hardening-notes.md`.
