How to Fix Docker 'Permission Denied' on Volume Mounts When SELinux Is Enforcing
Threat/Impact Level: HIGH | Downtime Risk: HIGH | Time to Fix: 5–15 mins
TL;DR
- What broke: SELinux denied the container process access to the host path because the bind-mount lacks a matching MCS/type label (
container_file_t). The kernel AVC audit fires and the syscall returnsEACCES. - How to fix it: Append
:z(shared) or:Z(private, unshared) to the volume mount flag, or runchcon -Rt container_file_t /host/pathon the host before mounting. - Shortcut: Use our Client-Side Sandbox below to auto-refactor your failing
docker runcommand or Compose file without leaking secrets.
The Incident (What Does the Error Mean?)
Raw kernel audit log and Docker stderr you will see:
type=AVC msg=audit(1700000000.000:512): avc: denied { read write } for pid=3821
comm="app" name="data" dev="sda1" ino=131073
scontext=system_u:system_r:container_t:s0:c123,c456
tcontext=unconfined_u:object_r:default_t:s0
tclass=dir permissive=0
docker: Error response from daemon: error while creating mount source path
'/host/path': mkdir /host/path: permission denied.
Immediate consequence: The container process (container_t) attempted to access a host directory labeled default_t. SELinux enforcing mode hard-blocks the syscall. The container exits immediately — no graceful degradation, no retry. If this is a stateful workload (database, log aggregator, secrets store), data is not written and the service is dead.
The Attack Vector / Blast Radius
This is a security control working correctly — but engineers under pressure will disable it incorrectly, creating a real attack surface:
The dangerous "fix" engineers reach for first:
# DO NOT DO THIS IN PRODUCTION
setenforce 0
Setting SELinux to permissive globally disables mandatory access control for every process on the host, not just Docker. A container escape (e.g., CVE-2019-5736 runc breakout, or a misconfigured privileged container) now has no kernel-level MAC enforcement standing between the attacker and host root. On a multi-tenant node running multiple workloads, this is a lateral movement highway.
Blast radius of setenforce 0:
- All containers on the node lose MCS isolation between each other.
- Host paths previously protected by
svirt_sandbox_file_tlabeling are now readable by any container process. - Compliance frameworks (PCI-DSS 2.2.6, STIG RHEL-07-020210) flag this as a critical finding. Audit trails show the change.
How to Fix It (The Solution)
Basic Fix — Relabeling Flag on the Mount
Append :z or :Z to the -v flag. Docker will call chcon on the host path automatically.
- docker run --rm -v /host/path:/container/path myimage
+ docker run --rm -v /host/path:/container/path:Z myimage
:Z vs :z — get this right:
| Flag | SELinux Label Applied | Use Case |
|---|---|---|
:z |
container_file_t shared (s0) |
Multiple containers need access to same path |
:Z |
container_file_t private MCS (s0:cX,cY) |
Single container, strongest isolation |
⚠️ Never use
:Zon system directories like/etc,/usr,/var/lib. It will relabel them tocontainer_file_tand break the host. Use only on application-owned data directories.
Manual chcon Fix (When You Can't Modify the Run Command)
# Apply the correct type label to the host directory recursively
chcon -Rt container_file_t /host/path
# Verify
ls -lZ /host/path
# Expected: unconfined_u:object_r:container_file_t:s0
Enterprise Best Practice — Persistent Label via semanage fcontext
chcon is not persistent across restorecon runs or filesystem relabels. In production, use semanage:
- chcon -Rt container_file_t /host/path
+ semanage fcontext -a -t container_file_t "/host/path(/.*)?"
+ restorecon -Rv /host/path
This writes the rule to the SELinux policy database. A full filesystem relabel (touch /.autorelabel; reboot) will not revert it.
Docker Compose (v3) Fix
volumes:
- - /host/path:/container/path
+ - /host/path:/container/path:Z
💡 Tired of pasting proprietary configs into ChatGPT? Generic AI tools log your company's ARNs, DB strings, and private keys. StackEngine is a zero-backend, pure Client-Side WASM utility. Drop your failing config into the sandbox above. We redact your secrets locally in the browser and auto-generate the refactored code using your own API key.
Prevention in CI/CD
1. Checkov — Catch Missing SELinux Labels in Compose Files
Add to your pipeline:
# .checkov.yml
checks:
- CKV_DOCKER_*
custom_checks:
- name: REQUIRE_SELINUX_VOLUME_LABEL
resource: docker-compose.yml
check: volumes[*] matches ":z" or ":Z"
severity: HIGH
2. OPA/Gatekeeper — Enforce on Kubernetes (if migrating)
If you are moving these workloads to Kubernetes on an SELinux host, enforce via admission controller:
package selinux_volume
deny[msg] {
input.request.kind.kind == "Pod"
vol := input.request.object.spec.volumes[_]
vol.hostPath
not input.request.object.spec.securityContext.seLinuxOptions
msg := sprintf("Pod '%v' uses hostPath volume without seLinuxOptions", [input.request.object.metadata.name])
}
3. Pre-flight Shell Check in Deployment Scripts
#!/usr/bin/env bash
set -euo pipefail
HOST_PATH="/host/path"
EXPECTED_CONTEXT="container_file_t"
ACTUAL=$(stat -c %C "${HOST_PATH}" | awk -F: '{print $3}')
if [[ "${ACTUAL}" != "${EXPECTED_CONTEXT}" ]]; then
echo "[FATAL] SELinux context on ${HOST_PATH} is '${ACTUAL}', expected '${EXPECTED_CONTEXT}'. Run: chcon -Rt container_file_t ${HOST_PATH}"
exit 1
fi
Plug this into your pre-deploy hook in Jenkins, GitHub Actions, or ArgoCD sync waves. It fails fast before the container ever starts.