Initializing Enclave...

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 returns EACCES.
  • How to fix it: Append :z (shared) or :Z (private, unshared) to the volume mount flag, or run chcon -Rt container_file_t /host/path on the host before mounting.
  • Shortcut: Use our Client-Side Sandbox below to auto-refactor your failing docker run command 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_t labeling 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 :Z on system directories like /etc, /usr, /var/lib. It will relabel them to container_file_t and 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.

Related Diagnostics

"Part of the Security Utility Matrix."

View all 140 Security Tools →