# Docker Bench for Security Cheat Sheet

> **Tool:** Docker Bench for Security
> **Category:** Security & Compliance
> **Verified against:** The project's `docker-bench-security.sh` script and README at
> github.com/docker/docker-bench-security (main branch), 2026-09-05 (a shell script, not installable via
> a package manager in this sandboxed environment — verified against the script's own current source and
> official README rather than a local `--help` run)
> **Official docs:** https://github.com/docker/docker-bench-security

## What it is and where it fits 🎯

Docker Bench for Security is a shell script that audits a Docker host's **runtime and daemon
configuration** against the CIS Docker Benchmark — host settings, how the Docker daemon itself is
configured, image/build practices, and how containers are actually being run right now. It's the direct
Docker-daemon counterpart to **kube-bench** (its own cheat sheet, same CIS-benchmark philosophy applied to
Kubernetes instead) — and it deliberately checks a different layer than **Trivy** or **Grype**, which
scan *inside* an image for known-vulnerable packages. Docker Bench doesn't care what's inside an image at
all; it cares whether the container running that image was started with a privileged flag, whether the
Docker socket is exposed somewhere it shouldn't be, and whether the daemon's own logging/auditing is
configured per the benchmark.

This is a genuinely common and easy distinction to blur, so it's worth stating precisely: **Trivy scans
what's inside an image; Docker Bench audits how a container is actually run and how the host/daemon
serving it is configured.** A perfectly clean Trivy scan (zero known CVEs) says nothing about whether that
same image was then started with `--privileged` and a bind-mounted `/var/run/docker.sock` — exactly the
kind of runtime misconfiguration Docker Bench exists to catch.

## What Docker Bench actually inspects, section by section

```mermaid
flowchart TD
    Run["docker-bench-security.sh"] --> Host["1. Host Configuration<br/>(kernel, auditd, partitions)"]
    Run --> Daemon["2. Docker Daemon Configuration<br/>(logging driver, live-restore, ...)"]
    Run --> Files["3. Docker Daemon Files<br/>(ownership/permissions of daemon.json, certs)"]
    Run --> Images["4. Container Images / Build<br/>(non-root USER, HEALTHCHECK present)"]
    Run --> Runtime["5. Container Runtime<br/>(--privileged, socket mounts,<br/>capability drops, PID/IPC namespace sharing)"]
    Run --> Ops["6-7. Docker Swarm / Security Ops"]

    classDef info fill:#e5f0fa,stroke:#1d6fb8,color:#10161c
    classDef crit fill:#fbe8e6,stroke:#b3261e,color:#10161c
    class Runtime crit
    class Host,Daemon,Files,Images,Ops info
```

The "Container Runtime" section is where most real-world findings concentrate — it's evaluated against
**currently-running** containers, so the same host can score very differently before and after a
dangerous container is actually started, unlike the mostly-static host/daemon sections.

## Installation and invocation

```bash
git clone https://github.com/docker/docker-bench-security.git
cd docker-bench-security
sudo sh docker-bench-security.sh
```

Docker Bench needs to inspect the host's Docker daemon state directly (socket access, host filesystem
paths), which is why it's normally run as a plain shell script with `sudo` on the host itself, rather than
purely from inside an isolated container — running it *as* a container is supported by the project but
requires deliberately mounting the host's Docker socket and several host paths in, which itself reproduces
exactly the kind of privileged-access pattern the benchmark is checking for elsewhere.

## Core concepts

| Concept | What it means |
|---|---|
| **CIS-based check** | Named `check_<section>_<number>` (e.g. `check_2_6`) — maps directly to a numbered control in the published CIS Docker Benchmark |
| **Community check** | Named `check_c_<number>` — useful, Docker-Bench-maintainer-curated checks that aren't part of the official CIS document itself |
| **INFO vs. WARN vs. PASS** | `WARN` is an actual finding; `INFO` is informational/manual-review; `PASS` means the check succeeded |
| **Section grouping** | Checks are grouped (`host_configuration`, `container_images`, `container_runtime`, ...) — filterable as a group, not just by individual check ID |

## Running the full audit and reading output

```bash
sudo sh docker-bench-security.sh                 # full run, all sections
sudo sh docker-bench-security.sh -b               # disable colored output (useful piping into a log parser)
sudo sh docker-bench-security.sh -l /var/log/docker-bench.log    # write output to a specific log file
```

Sample output shape (representative — exact WARN/PASS counts depend entirely on the host and running
containers at scan time):

```
[INFO] 5 - Container Runtime
[WARN] 5.4  - Ensure that privileged containers are not used
[WARN]      * Container running in Privileged mode: dangerous-app
[PASS] 5.9  - Ensure that the host's network namespace is not shared
[WARN] 5.31 - Ensure that the Docker socket is not mounted inside any containers
[WARN]      * Container running with docker socket mounted: dangerous-app: /var/run/docker.sock

== Summary ==
Checks: 92
Score: 61
```

A `[WARN]` entry that names a specific running container by name (as `5.4` and `5.31` do above) is the
single most actionable output shape Docker Bench produces — it tells you exactly which workload to fix,
not just which control category has a gap somewhere.

## Filtering to specific checks or sections

```bash
sudo sh docker-bench-security.sh -c check_2_2                        # run only check 2.2
sudo sh docker-bench-security.sh -e check_2_2                        # run everything EXCEPT check 2.2
sudo sh docker-bench-security.sh -c container_images,container_runtime  # run only these two sections
sudo sh docker-bench-security.sh -c container_images -e check_4_5     # a section, minus one specific check within it
sudo sh docker-bench-security.sh -i my-app-name                       # scope container/image checks to names matching this pattern
sudo sh docker-bench-security.sh -x noisy-sidecar-container            # exclude a specific container/image by name from the scan
```

The `-i`/`-x` include/exclude-by-name filters matter in practice on a host running many containers, where
scrolling through findings for infrastructure sidecars you don't directly control just adds noise to the
findings that are actually yours to fix.

## Understanding check naming and where to focus first

| Naming pattern | Meaning | Where these findings usually cluster |
|---|---|---|
| `check_1_x` | Host configuration | Kernel parameters, separate partition for Docker's data directory, auditd rules |
| `check_2_x` | Docker daemon configuration | Logging driver, live-restore, default ulimits, TLS on the daemon socket |
| `check_3_x` | Docker daemon files | Ownership/permissions on `daemon.json`, TLS certs, the Docker socket file itself |
| `check_4_x` | Container images and build | Non-root `USER` in the Dockerfile, `HEALTHCHECK` present, no secrets baked into image layers |
| `check_5_x` | Container runtime | Privileged mode, capability drops, namespace sharing, resource limits — see the WARNING below |
| `check_c_x` | Community checks | Useful, maintainer-curated additions not part of the official CIS document |

In practice, section 5 (container runtime) is where a security review should spend the most attention
first — it's evaluated against containers that are actually running right now, so it directly reflects
real operational risk rather than a static host setting configured once and rarely revisited.

## Real-world scenario: auditing a shared CI runner host

A self-hosted CI runner is a genuinely high-value target for this kind of audit: it routinely builds and
runs untrusted, PR-triggered code, often with more privileges than a typical production host because "it's
just CI":

```bash
sudo sh docker-bench-security.sh -c container_runtime,container_images -l /var/log/ci-runner-audit.log
grep -c '^\[WARN\]' /var/log/ci-runner-audit.log
```

- [ ] Confirm no long-lived container on the runner has `--privileged` set "just in case a job needs it."
- [ ] Confirm the Docker socket is never bind-mounted into a job's build container — a compromised
      dependency in a PR build with socket access can escalate to full host compromise.
- [ ] Confirm build images run their actual build steps as a non-root `USER`, not root by default.
- [ ] Schedule this audit to run automatically after every runner AMI/image update, not just once at
      initial provisioning — a "hardened" base image can silently regress with an update.

> [!CAUTION]
> **A CI runner is one of the most attractive privilege-escalation targets in most companies' infrastructure**
> precisely because it's designed to build and execute code from pull requests — including, potentially, a
> malicious one from an external contributor on a public repo. Treat Docker Bench findings on a CI runner
> host with at least the same urgency as findings on a production host, not less.

## Real-world scenario: baselining before and after a risky container

Reproducing the exact before/after comparison the tool is built to demonstrate:

```bash
sudo sh docker-bench-security.sh -c container_runtime      # baseline — note which runtime checks currently PASS

docker run -d --name dangerous-app --privileged \
  -v /var/run/docker.sock:/var/run/docker.sock \
  nginx:latest

sudo sh docker-bench-security.sh -c container_runtime      # re-run — 5.4 and 5.31 now WARN, naming "dangerous-app" directly
```

> [!WARNING]
> **A container with the Docker socket bind-mounted in (`-v /var/run/docker.sock:/var/run/docker.sock`)
> has an effective path to root on the host**, regardless of what user the container process itself runs
> as — anything with that socket can launch a brand-new, fully-privileged container of its own. This is
> one of the single most severe, most commonly-seen misconfigurations Docker Bench flags, and it shows up
> surprisingly often in CI runners and monitoring sidecars that "just needed to check container status."

## Real-world scenario: CI gate on a build host, not just production

```yaml
# .github/workflows/docker-bench.yml
name: Docker Bench for Security
on:
  schedule:
    - cron: "0 6 * * *"   # daily — this audits the HOST'S daemon config, not a specific build artifact
jobs:
  audit:
    runs-on: self-hosted   # must run on a real Docker host — GitHub-hosted runners don't expose daemon config the same way
    steps:
      - name: Clone and run Docker Bench
        run: |
          git clone https://github.com/docker/docker-bench-security.git /tmp/dbs
          cd /tmp/dbs
          sudo sh docker-bench-security.sh -b -l /tmp/dbs-results.log
      - name: Fail on any WARN in the container_runtime section
        run: |
          ! grep -q "^\[WARN\]" /tmp/dbs-results.log
```

Docker Bench audits the **host**, not a single artifact — it belongs on a recurring schedule against your
actual build/production hosts, not as a per-PR gate the way Trivy's image scan is, since nothing about a
single PR's diff changes the host's own daemon configuration.

## Comparing findings across two hosts

```bash
sudo sh docker-bench-security.sh -l /tmp/host-a.log
diff <(grep '^\[WARN\]' /tmp/host-a.log) <(grep '^\[WARN\]' /tmp/host-b.log)
```

A raw score alone (see the pitfall below) hides *which* checks actually differ — diffing the WARN lines
between a known-good baseline host and a newly-provisioned one is a fast way to catch configuration drift
introduced by a change to a shared provisioning script, before it reaches every host in a fleet.

## Shell completion and log limiting

```bash
sudo sh docker-bench-security.sh -n 50    # cap the number of items listed per finding in JSON output (default: unlimited)
```

`-n` matters specifically on a host running a large number of containers/images — an unbounded findings
list for a single check (e.g. "every image missing a `HEALTHCHECK`") can otherwise dwarf the rest of the
report and make the genuinely actionable findings harder to spot at a glance.

## Common pitfalls

- **Confusing this with an image content scanner.** See the "What it is" section above — Docker Bench
  finds zero CVEs by design; that's Trivy/Grype's job, not this tool's.
- **Running it inside an unprivileged container and expecting full results.** Several checks need direct
  host filesystem/daemon access; a naive containerized run of Docker Bench itself often under-reports
  findings unless the exact host mounts the project documents are deliberately included.
- **Ignoring `[WARN]` findings that name a legitimate infrastructure container** (a monitoring agent that
  genuinely needs `--privileged`) instead of documenting the exception. Use `-x`/`-i` to scope future runs
  around a reviewed, accepted exception rather than re-triaging the same known finding every time.
- **Treating the numeric "Score" alone as meaningful without reading which specific checks failed.** Two
  hosts with the same score can have completely different actual risk profiles depending on *which*
  checks are the ones failing.

## Interpreting remediation guidance with `-p`

```bash
sudo sh docker-bench-security.sh -p                # print remediation guidance alongside every WARN/INFO finding
```

By default, Docker Bench's console output states *what* failed but not always the exact fix inline — `-p`
surfaces the CIS Benchmark's own remediation text directly next to each finding, the same "don't make the
reader go find the fix in a separate document" principle kube-bench's `== Remediation ==` block follows.

## Real-world scenario: a pre-production release checklist

Teams that treat Docker Bench as a recurring gate rather than a one-off audit typically fold it into a
release checklist alongside the image-content scanning this series already covers:

- [ ] Run `trivy image` (its own cheat sheet) against the release candidate image — content-level CVEs.
- [ ] Run `docker-bench-security.sh -c container_images` against the same image while it's the only thing
      running on a clean host — build/image-level CIS checks (non-root `USER`, no secrets in layers).
- [ ] Deploy the image to a staging host and re-run `docker-bench-security.sh -c container_runtime` —
      catches a misconfigured `--privileged`/socket-mount flag introduced at the orchestration layer,
      which an image scan alone can never see since it isn't part of the image itself.
- [ ] Only promote to production once both layers — image content and runtime configuration — pass.

This two-layer checklist is the concrete answer to "why do we need both a scanner and Docker Bench" —
each one is structurally blind to what the other catches.

## Running via Docker itself (containerized invocation)

```bash
docker run --rm --net host --pid host --userns host --cap-add audit_control \
  -e DOCKER_CONTENT_TRUST=$DOCKER_CONTENT_TRUST \
  -v /etc:/etc:ro \
  -v /usr/bin/containerd:/usr/bin/containerd:ro \
  -v /usr/bin/runc:/usr/bin/runc:ro \
  -v /usr/lib/systemd:/usr/lib/systemd:ro \
  -v /var/lib:/var/lib:ro \
  -v /var/run/docker.sock:/var/run/docker.sock:ro \
  --label docker_bench_security \
  docker-bench-security     # built locally via `docker build -t docker-bench-security .` from the cloned repo
```

Notice this exact invocation mounts the host's Docker socket and several host paths read-only into the
scanning container itself — a concrete, real illustration of the tension named earlier in this cheat
sheet: even a security-auditing tool needs privileged-adjacent access to do its job, which is exactly why
`5.31`'s "Docker socket not mounted inside any containers" check exists for *application* containers, not
for a deliberately-scoped, read-only, security-tooling exception like this one.

## Trusted users and Docker Content Trust

```bash
sudo sh docker-bench-security.sh -u alice,bob    # only these OS users are treated as legitimately trusted to run docker
```

Check `4.5` ("Content trust for Docker is enabled") also checks the `DOCKER_CONTENT_TRUST` environment
variable — enabling it means `docker pull`/`docker run` refuse an image whose publisher didn't sign it,
the same signature-verification principle **Cosign** (its own cheat sheet) applies at the CI/build stage,
here enforced one layer closer to the runtime pull itself.

## Exit codes and when to reach for something else

The script itself doesn't fail the shell process on a `WARN` finding — build pass/fail logic from parsing
its log output (as in the CI recipe above), the same pattern kube-bench's JSON output requires. For
Kubernetes cluster configuration hardening rather than the underlying Docker/container-runtime host, reach
for **kube-bench** instead (its own cheat sheet) — note that a Kubernetes worker node still runs a
container runtime underneath, so the two audits are complementary rather than redundant on a
self-managed Kubernetes cluster running Docker/containerd directly on its nodes. For what's actually
*inside* the images being run, pair this with **Trivy** or **Grype**, already covered in this series.
