Remediation Guides

How to Enforce readOnlyRootFilesystem for Containers (Kubernetes, ECS, Docker)

26 September 2026 8 min read

The finding Immutable (read-only) root filesystem should be enforced for containers means a container runs without readOnlyRootFilesystem, so its processes can write anywhere in the image filesystem. Fix it by setting securityContext.readOnlyRootFilesystem: true in Kubernetes, “readonlyRootFilesystem”: true in ECS, or docker run –read-only, and mount writable volumes for /tmp and cache paths.

What the scanner is actually detecting

Several tools raise the same misconfiguration under different names. All of them read configuration, not runtime behavior, and all of them treat a missing field as a failure, because every platform defaults to a writable root filesystem.

Scanner Finding title What fails Severity
Microsoft Defender for Cloud (Kubernetes) Immutable (read-only) root filesystem should be enforced for containers Pod containers without readOnlyRootFilesystem: true, evaluated by Azure Policy Kubernetes cluster containers should run with a read only root file system (ID df49d893-a74c-421d-bc95-c663042e5b80) Medium
Microsoft Defender for Cloud (AWS) Read-only root filesystem should be enabled for ECS Containers ECS task definitions with writable root filesystems Medium
Trivy KSV-0014 (alias KSV014): Root file system is not read-only Any container or init container in a Pod, Deployment, StatefulSet, DaemonSet, ReplicaSet, Job or CronJob where the field is false or absent High
AWS Security Hub CSPM ECS.5: ECS task definitions should configure containers to be limited to read-only access to root filesystems The latest active task definition revision, when readonlyRootFilesystem is false or missing (Config rule ecs-containers-readonly-access) High
docker-bench-security 5.13: Ensure that the container’s root filesystem is mounted as read only Running containers where .HostConfig.ReadonlyRootfs is false WARN

One detail catches teams off guard: the Kubernetes Pod Security Standards restricted profile does not include this control. A namespace that passes restricted admission can still fail every check above.

Real-world risk

This is a hardening gap, not an exploitable vulnerability, which is why the ratings range from Medium (Defender) to High (Trivy, Security Hub). It matters after something else has gone wrong. If an attacker gets code execution in the container through an application bug, a writable root filesystem lets them drop tools into directories on PATH, replace application code or config the app reads later, and stage files for the rest of the container’s life.

Be clear about the limits. A read-only root does not stop in-memory payloads, does not stop abuse of binaries already in the image, and does nothing for paths you mount writable. Containers also discard their writable layer when replaced, so the benefit is mainly during a running container’s lifetime. Shipping fewer binaries helps just as much; our comparison of distroless vs Alpine vs Debian base images covers that side.

How to confirm it on the host or cluster

List every running Kubernetes container, including init containers, that does not set the field to true:

kubectl get pods -A -o json | jq -r '
  .items[] | .metadata.namespace as $ns | .metadata.name as $pod
  | (.spec.containers[]?, .spec.initContainers[]?)
  | select(.securityContext.readOnlyRootFilesystem != true)
  | "($ns)/($pod)  container=(.name)"'

Check the mount inside a running container. The first option on the / line is ro or rw. Distroless images have no shell or grep, so rely on the manifest there:

kubectl -n <namespace> exec <pod> -c <container> -- grep ' / ' /proc/mounts

On a Docker host, print the setting for every running container:

docker ps --quiet | xargs docker inspect --format '{{.Name}} ReadonlyRootfs={{.HostConfig.ReadonlyRootfs}}'

For ECS, read the container definitions of the revision a service uses. A missing field shows as None:

aws ecs describe-task-definition --task-definition web 
  --query 'taskDefinition.containerDefinitions[].[name,readonlyRootFilesystem]' --output table

Before you enforce anything, find out where the application writes. Run the image locally, exercise it, then list what changed. Lines starting with A (added) or C (changed) are the paths that need a writable mount:

docker diff <container>

How to fix it

Kubernetes

readOnlyRootFilesystem exists only in the container-level securityContext, so set it on every container and init container in the pod template of the owning Deployment, StatefulSet, DaemonSet, Job or CronJob. Give each write path from docker diff its own emptyDir:

spec:
  template:
    spec:
      containers:
      - name: web
        image: registry.example.com/web:1.4.2
        securityContext:
          readOnlyRootFilesystem: true
        volumeMounts:
        - name: tmp
          mountPath: /tmp
        - name: cache
          mountPath: /var/cache/app
      volumes:
      - name: tmp
        emptyDir:
          sizeLimit: 100Mi
      - name: cache
        emptyDir:
          medium: Memory
          sizeLimit: 64Mi

With medium: Memory, Kubernetes mounts a tmpfs and the files count against the container’s memory limit. If Helm, Argo CD or Flux owns the object, change the chart values or source manifest, or the next sync reverts a manual edit.

Enforcing it in the cluster

Because Pod Security Admission cannot enforce this field, use a policy engine. On AKS and Arc-enabled clusters, assign the built-in Azure Policy (default effect Audit; it also accepts Deny and Disabled, plus excludedContainers and excludedImages parameters):

az policy assignment create 
  --name ro-rootfs 
  --policy df49d893-a74c-421d-bc95-c663042e5b80 
  --scope /subscriptions/<sub-id>/resourceGroups/<rg>/providers/Microsoft.ContainerService/managedClusters/<cluster> 
  --params '{"effect":{"value":"Deny"}}'

Elsewhere, use the Gatekeeper library template K8sPSPReadOnlyRootFilesystem (it supports exemptImages) or the Kyverno policy require-ro-rootfs. The Kyverno policy ships in Audit mode; move it to Enforce once its reports are clean.

Amazon ECS

Set the flag in each Linux container definition and add a volume for every write path. A volume with only a name is an ephemeral bind mount, which works on both Fargate and EC2:

"volumes": [ { "name": "tmp" } ],
"containerDefinitions": [
  {
    "name": "web",
    "image": "registry.example.com/web:1.4.2",
    "essential": true,
    "readonlyRootFilesystem": true,
    "mountPoints": [
      { "sourceVolume": "tmp", "containerPath": "/tmp", "readOnly": false }
    ]
  }
]

On the EC2 launch type you can use linuxParameters.tmpfs instead. Fargate does not support the tmpfs parameter, so use bind mounts there. Register a new revision and roll the service to it:

aws ecs describe-task-definition --task-definition web --query taskDefinition > web.json
# edit web.json, then strip the read-only metadata fields
jq 'del(.taskDefinitionArn,.revision,.status,.requiresAttributes,.compatibilities,.registeredAt,.registeredBy)' web.json > web-new.json
aws ecs register-task-definition --cli-input-json file://web-new.json
aws ecs update-service --cluster prod --service web --task-definition web

Docker and Docker Compose

docker run -d --read-only 
  --tmpfs /tmp:rw,noexec,nosuid,size=64m 
  -v app-data:/var/lib/app 
  registry.example.com/web:1.4.2

In Compose, the equivalent keys are read_only and tmpfs:

services:
  web:
    image: registry.example.com/web:1.4.2
    read_only: true
    tmpfs:
      - /tmp

How to verify the fix and rescan

  1. Re-run the jq, docker inspect or describe-task-definition check above. The fixed workload should no longer appear, or should show true.
  2. Exec into a new container and confirm ro on the / line of /proc/mounts. A write such as touch /usr/probe should fail with Read-only file system, while writes to /tmp still succeed.
  3. Run trivy config ./k8s/ on the manifests and trivy k8s –scanners misconfig –include-namespaces <namespace> –report all on the cluster. KSV-0014 should be gone for that resource.
  4. On Docker hosts, run sudo sh docker-bench-security.sh -c check_5_13 to repeat only that check.
  5. ECS.5 is change triggered, so registering the new revision prompts re-evaluation. Defender for Cloud assesses periodically, so a fixed workload can stay unhealthy for a while after rollout.

What can break and how to roll back

  • Undeclared write paths. Temp files, PID files, caches, local log files and entrypoint scripts that render config at startup all fail with Read-only file system. Each needs a volume, or the app needs to write to stdout or a mounted path instead.
  • Mounts hide image content. An emptyDir or tmpfs mounted over a directory hides whatever the image shipped there. Mount only directories that start empty, or copy defaults in at startup.
  • Memory pressure. Memory-backed emptyDir volumes and Docker tmpfs mounts count against the container memory limit, so an unbounded cache can trigger OOM kills. Set a size.
  • Deny policies stall rollouts. A policy that evaluates only Pods, as the Azure Policy does, lets the Deployment through but blocks its pods. Check kubectl -n <namespace> get events.

To roll back, revert the manifest or run kubectl -n <namespace> rollout undo deployment/<name>. In ECS, point the service back at the previous revision with aws ecs update-service –cluster prod –service web –task-definition web:<previous>. On Docker, recreate the container without –read-only. Set Azure Policy or Kyverno back to Audit if admission is the problem.

Common false positive reasons

  • Windows workloads. Kubernetes forbids this field when spec.os.name is windows, and ECS does not support it for Windows containers (ECS.5 marks those NOT_APPLICABLE). Trivy KSV-0014 does not check the OS, so it can still flag them.
  • Mutation at admission. A mutating policy may add the field at pod creation. The live pod is compliant while trivy config on the source manifest still fails.
  • Stale ECS revisions. ECS.5 reads only the latest active revision. A compliant new revision clears it even if the service still runs an older one, so confirm which revision the service actually uses.
  • Infrastructure agents. CNI, CSI and some monitoring DaemonSets genuinely need a writable root. Treat them as documented exceptions using the policy’s exclusion parameters.

More patterns are covered in our guide to container scanner false positives.

FAQ

Can I set readOnlyRootFilesystem once for the whole pod?

No. It is a container-level field only. Set it on each container and init container, since Trivy checks both.

Does a read-only root filesystem break logging?

Not if the app logs to stdout and stderr, which the runtime collects outside the container. Apps that write log files need a mounted volume for that directory.

Is the restricted Pod Security Standard enough to pass this check?

No. Restricted covers privilege escalation, non-root, capabilities and seccomp, but not readOnlyRootFilesystem. Enforce it with Azure Policy, Gatekeeper or Kyverno.

Why does ECS.5 still fail after I updated the service?

The control checks the latest active task definition revision, not the running service. Make sure the newest active revision in that family sets the flag on every container.

Tracking this finding across many hosts

This finding tends to repeat across every namespace, task family and Docker host you run. If your container scanners are among the 16 that SITEY imports findings from, the results can sit in one self-hosted list next to your other findings. Duplicates are merged per scanner, not across scanners, so a workload flagged by both Trivy and Defender appears twice, and AI triage suggests likely false positives with evidence while a human makes the final call.

Sources

SITEY closes the loop, not just the report.Discover, validate, fix and verify in your own infrastructure.

See pricing