Docker audit fix

How to Configure Read-Only Root Filesystems in Docker

Docker gives every container a writable root filesystem unless it is started with --read-only, so anything that gains code execution inside can overwrite binaries and persist there. Turn it on and mount tmpfs for the paths the app genuinely writes.

Hiren KalariyaLast reviewed: Oct 4, 2026Check read-only-filesystem
Low
severity
Yes
needs root or sudo
3
results it can return
5 of 5
checks in this audit

Every threshold on this page is transcribed from the docker-security-container-hardening audit script that CtrlOps runs, and a build check fails if the two ever disagree. See all 5 Container Hardening checks, or how the audit runs.

Low severityNeeds sudo

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 thresholds this check applies
ResultWhenWhat it means
SKIPNo running containersNothing was verified: no filesystem setting was read, so treat every container as unchecked.
PASSAll use `--read-only`No running container can write to its own root filesystem, though mounted volumes may still be writable.
WARNSome have a writable root fsThe 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-only without the matching --tmpfs mounts 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
  - /run

Use 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

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
Audit your fleet

Find every one of these on every server, in one click

CtrlOps runs this audit over your existing SSH connection - no agents, no scripts to manage. $7/user/month after a 1 month free trial - no credit card required.

Windows

✓ Start instantly·✓ No credit card·✓ No sneaky autorenewals