What this check reads
The check inspects one boolean, .HostConfig.ReadonlyRootfs, on every container returned by docker ps -q, using a template that emits the name of any container where that field is false. Those names are then counted. A count of zero is a PASS; anything above zero is a WARN that reports N of M containers with a writable root filesystem and names up to five of them.
When it applies
Needs a reachable daemon and at least one running container; otherwise SKIP. It reads .HostConfig.ReadonlyRootfs for each running container - a single boolean, set by --read-only or Compose read_only: true. It says nothing about mounted volumes: a container with a read-only root filesystem and a writable bind mount still passes, which is correct (that is the intended pattern) but means PASS does not imply "nothing is writable". Severity is LOW because this is a tampering-resistance measure, not a containment boundary.
What each result means
| Result | When | What it means |
|---|---|---|
| SKIP | No running containers | Nothing was verified: no filesystem setting was read, so treat every container as unchecked. |
| PASS | All use `--read-only` | No running container can write to its own root filesystem, though mounted volumes may still be writable. |
| WARN | Some have a writable root fs | The containers named have a writable root filesystem, so a compromised process can overwrite binaries and persist inside them. |
Why it matters
A read-only root filesystem stops an attacker who gains code execution from dropping tools, modifying binaries, or persisting inside the container. Writable scratch space is provided explicitly with --tmpfs or named volumes. OWASP Docker Cheat Sheet Rule #8 "Set filesystem and volumes to read-only"; CIS Docker 5.12 "Ensure that the container's root filesystem is mounted as read only"; NIST SP 800-190 4.4.
Why it fails, and when it is wrong
- Most images expect to write to
/tmp,/var/run,/var/cache, log directories, or PID files. Turning on--read-onlywithout the matching--tmpfsmounts causes startup failures ("Read-only file system"). - Advisory (LOW). Expect a WARN on typical deployments; it is a hardening target, not a bug.
- A near-duplicate check exists (Read-Only Volume Enforcement, MEDIUM) that evaluates all containers, not just running ones; one fix satisfies both.
How to fix it
Find out what the container writes
Run this against a working copy started without --read-only. Every A or C line is a path that needs a writable mount.
docker diff <name>Turn on --read-only with writable mounts for those paths
docker run --read-only --tmpfs /tmp --tmpfs /run -v data:/var/lib/app ...In Compose:
read_only: true
tmpfs:
- /tmp
- /runUse a tmpfs for scratch space that can vanish on restart, and a named volume for anything the app must keep.
Recreate and watch the logs
docker compose up -d --force-recreate
docker logs <name> 2>&1 | grep -iE 'read-only file system|EROFS'Each hit is a path you missed. Add a tmpfs or volume for it and recreate again until the logs are clean.
Verify the fix
docker inspect -f '{{.Name}} readonly={{.HostConfig.ReadonlyRootfs}}' $(docker ps -q)
# expected: true
# Prove it from inside:
docker exec <name> sh -c 'touch /test-write' 2>&1
# expected: Read-only file system
# And that the scratch space the app needs is still writable:
docker exec <name> sh -c 'touch /tmp/ok && echo tmpfs writable'
docker inspect -f '{{.HostConfig.Tmpfs}} {{range .Mounts}}{{.Destination}}:{{.RW}} {{end}}' <name>Debugging
This one message covers two different cases: the audit could not reach the Docker daemon, or it reached it and nothing was running. Run docker ps as the audit user, then sudo docker ps. If both fail, the daemon is unreachable: check systemctl status docker, then run the audit as root or give the account sudo. If they work but list nothing, there is genuinely nothing to evaluate, and a stopped container is never checked. The Privileged Containers check reports the two cases as separate messages, so its result tells you which one you hit.
Truncated with head -5; the N of M count is complete. Full list: docker inspect -f '{{if not .HostConfig.ReadonlyRootfs}}{{.Name}}{{end}}' $(docker ps -q).
It needs somewhere to write. Add --tmpfs /tmp --tmpfs /run (Compose tmpfs:), or a named volume for genuine state. Find out what it writes with docker diff <name> on a running copy without --read-only - every A/C line is a path that needs a writable mount.
The container needs recreating for a HostConfig change; docker compose up -d --force-recreate.
The root filesystem is read-only, mounted volumes are not. Check .Mounts with the command above and mount read-only wherever the container does not need to write; see the Writable Volume Mounts check.
Sources
- Docker: docker run
--read-only - Docker: tmpfs mounts
- OWASP Docker Security Cheat Sheet (Rule #8)
- Docker Compose: read_only / tmpfs
How the script reads this
Next
Re-run the Container Hardening audit after applying the fix and confirm this check moves to PASS. CtrlOps runs all 5 checks over your existing SSH connection and scores the result, so the change is visible without reading another config file.
All 5 Container Hardening fixes