Overview
AppArmor (Ubuntu/Debian) and SELinux (Fedora/RHEL/CentOS) can block Docker containers from reading files, mounting paths, or performing syscalls. Typical errors:
- AppArmor: apparmor="DENIED" operation="..." profile="docker-default" ...
- SELinux: AVC denial messages in audit logs; apps see EACCES or 13 Permission denied.
This guide shows how to quickly diagnose and fix denials safely, focusing on Docker.
Quickstart (TL;DR)
- Identify the active LSM:
- SELinux: getenforce => Enforcing/Permissive/Disabled
- AppArmor: aa-status or cat /sys/module/apparmor/parameters/enabled
- Check logs:
- SELinux: journalctl -t setroubleshoot -b or ausearch -m avc -ts recent
- AppArmor: journalctl -k | grep DENIED or dmesg | grep DENIED
- Common SELinux fix for bind mounts: add :Z or :z to Docker volume options.
- Common AppArmor fix in dev: run with --security-opt apparmor=unconfined or temporarily set docker-default to complain mode; then refine a profile.
- Prefer targeted fixes (labels/profiles) over disabling enforcement.
Minimal working example (SELinux host)
This example shows a denial from a bind mount and fixes it with :Z labeling.
# 1) Reproduce: bind-mount a host dir into nginx
mkdir -p "$PWD/html" && echo "hello" > "$PWD/html/index.html"
# On SELinux Enforcing hosts, this may trigger AVC denials without labels
docker run --rm -p 8080:80 \
-v "$PWD/html":/usr/share/nginx/html:ro \
nginx:alpine
# In another shell, check for SELinux AVC denials
ausearch -m avc -ts recent | tail -n 20 || true
# 2) Fix: relabel the mount for container use
# :Z gives a private label for this container
docker run --rm -p 8080:80 \
-v "$PWD/html":/usr/share/nginx/html:ro,Z \
nginx:alpine
# Now nginx can read the files; AVC denials should stop.
Notes:
- Use :Z for a private label (safer isolation). Use :z to share the label with multiple containers that mount the same path.
- Alternative: chcon -Rt container_file_t ./html to persistently relabel the directory.
Step-by-step diagnosis
- Determine the platform and LSM
- SELinux: getenforce and sestatus
- AppArmor: aa-status and ls -l /etc/apparmor.d
- Capture the denial
- Re-run the container and immediately inspect logs:
# SELinux
ausearch -m avc -ts recent | audit2why
journalctl -t setroubleshoot -b | tail -n 50 || true
# AppArmor
dmesg | grep -i apparmor | tail -n 50
journalctl -k | grep -i "apparmor=\"DENIED\"" | tail -n 50
- Identify the resource
- Look for the path, capability, or syscall in the denial (e.g., denied r access to /host/path, operation="mount", cap=sys_admin).
- Apply the least-privilege fix
- SELinux: label volumes (:z/:Z) or relabel paths; only if needed, adjust SELinux policy.
- AppArmor: adjust the profile or use an appropriate security-opt override.
- Verify
- Re-run the container; confirm no new denials in logs.
Fixes on SELinux hosts
Common container issues and fixes:
Bind mount denials (read/write)
- Preferred: use volume flags
# private label (container-specific) docker run ... -v /path:/container/path:Z ... # shared label (multiple containers share) docker run ... -v /path:/container/path:z ... - Persistent relabel (affects the host path):
# Temporary change (does not survive restorecon) chcon -Rt container_file_t /path # Persistent mapping (survives relabels) semanage fcontext -a -t container_file_t "/path(/.*)?" restorecon -Rv /path
- Preferred: use volume flags
Capability or syscall denials
- If the app needs extra capabilities, add only what’s required:
docker run ... --cap-add SYS_ADMIN ... # narrow if possible - For rare cases where SELinux blocks specific actions, derive a minimal policy from audit logs:
Caution: review the generated rules; avoid broad allows.# Generate a type enforcement module from recent AVCs ausearch -m avc -ts recent | audit2allow -M mycontainer semodule -i mycontainer.pp
- If the app needs extra capabilities, add only what’s required:
Last-resort development overrides (avoid in production)
# Disable SELinux separation for the container (Docker-specific) docker run ... --security-opt label=disable ... # Temporarily set host to permissive (global) sudo setenforce 0 # setenforce 1 to re-enable Enforcing
Fixes on AppArmor hosts
Docker uses the docker-default AppArmor profile by default.
Quick dev workaround
# Unconfine a single container (dev/testing only) docker run ... --security-opt apparmor=unconfined ...Temporarily collect denials and update the profile
# Put docker-default into complain mode to log rather than block sudo aa-complain docker-default # Reproduce the issue, then update the profile interactively sudo aa-logprof # Return to enforcing sudo aa-enforce docker-defaultUse a custom profile per container
- Create /etc/apparmor.d/docker-myapp with the minimal needed rules.
- Load it: sudo apparmor_parser -r /etc/apparmor.d/docker-myapp
- Run: docker run ... --security-opt apparmor=docker-myapp ...
If a capability is blocked (e.g., mount), consider using a precise capability add and profile rule rather than unconfined.
Docker Compose equivalents
- Volume labels
services: web: image: nginx:alpine ports: ["8080:80"] volumes: - ./html:/usr/share/nginx/html:Z # or :z - Security options
services: app: image: myimage security_opt: - label=disable # SELinux (dev-only) - apparmor:unconfined # AppArmor (dev-only)
Pitfalls
- Forgetting :z/:Z on SELinux bind mounts; the container may start but fail at runtime.
- Using chcon without semanage: labels may revert after restorecon or relabel operations.
- Overbroad policy generated by audit2allow; always review and minimize.
- Running unconfined or label=disable in production increases risk surface.
- Mixing shared (:z) and private (:Z) labels inconsistently across containers can cause access conflicts.
Performance notes
- Relabeling large directory trees with :Z or chcon can be slow; prefer targeting only required paths.
- Frequent SELinux AVCs or AppArmor denials increase audit log volume; fix root causes to avoid log overhead.
- Granting extra capabilities (e.g., SYS_ADMIN) can enable expensive operations inside containers—measure impact.
- Custom AppArmor profiles have negligible runtime overhead; keep them minimal to reduce parsing/load time.
FAQ
How do I know which module blocked me?
- Check getenforce (SELinux) and aa-status (AppArmor). Review kernel/audit logs for the denial source.
What’s the difference between :z and :Z in Docker volumes?
- :z applies a shared label so multiple containers can access the path; :Z applies a private label for one container.
Is it safe to run apparmor=unconfined or label=disable?
- Use only for development or short-term debugging. Prefer targeted labels/profiles in production.
Do rootless Docker setups change this?
- Rootless reduces privileges but SELinux/AppArmor still apply on the host. Labeling and profiles remain relevant.
Can I fix this from inside the container?
- No. Labels and profiles are managed on the host. Apply fixes on the host side and restart the container.