TLS & Security Error Guide: 'apparmor="DENIED"' (AppArmor Profile Block)
Fix AppArmor DENIED errors: read apparmor=DENIED in dmesg/journalctl, identify the profile and operation, add file/network rules, and reload with apparmor_parser.
- #security
- #troubleshooting
- #errors
- #apparmor
Overview
An AppArmor DENIED means a confined process tried an operation its profile does not allow, and the kernel’s Mandatory Access Control layer blocked it. The application receives a generic Permission denied (EACCES), but the kernel logs an apparmor="DENIED" audit line naming the profile, the requested mask, and the target. Unlike SELinux’s context labels, AppArmor is path-based: a profile lists exactly which files, capabilities, and network operations a program may use, and anything not listed is denied (in enforce mode).
A denial in the kernel/audit log looks like:
audit: type=1400 audit(1750684931.412:1187): apparmor="DENIED" operation="open" profile="/usr/sbin/mysqld" name="/data/mysql/ibdata1" pid=2244 comm="mysqld" requested_mask="r" denied_mask="r" fsuid=27 ouid=27
The fields that matter: profile (which AppArmor profile is confining the process), operation/denied_mask (what it tried — r/w/x, open, connect, etc.), and name (the target path). profile="..." in enforce mode means the action was blocked; a profile in complain mode would log ALLOWED instead.
Symptoms
- A confined service fails with
Permission deniedwhile Unix permissions are correct. apparmor="DENIED"lines appear indmesgorjournalctl -k.- The failure stops if the profile is set to complain mode (
aa-complain). aa-statusshows the program’s profile loaded in enforce mode.
sudo journalctl -k --no-pager | grep 'apparmor="DENIED"' | tail -3
audit: type=1400 ... apparmor="DENIED" operation="open" profile="/usr/sbin/mysqld" name="/data/mysql/ibdata1" requested_mask="r" denied_mask="r" comm="mysqld"
sudo aa-status | grep -A2 'enforce mode'
42 profiles are in enforce mode.
/usr/sbin/mysqld
Common Root Causes
1. Data moved to a non-default path the profile does not allow
The profile permits the package’s default data directory, but you relocated the data and never added the new path.
sudo journalctl -k --no-pager | grep 'apparmor="DENIED"' | grep mysqld | tail -1
grep -E '/var/lib/mysql|/data/mysql' /etc/apparmor.d/usr.sbin.mysqld
... profile="/usr/sbin/mysqld" name="/data/mysql/ibdata1" denied_mask="r"
/var/lib/mysql/ r,
/var/lib/mysql/** rwk,
The profile only lists /var/lib/mysql, not /data/mysql.
2. Missing network rule
The profile lacks a network rule for the socket family the process needs (e.g. it tries to open a TCP socket but the profile has no network inet stream).
sudo journalctl -k --no-pager | grep 'apparmor="DENIED"' | grep 'operation="create"' | tail -1
... apparmor="DENIED" operation="create" profile="/usr/bin/myapp" family="inet" sock_type="stream" comm="myapp"
family="inet" sock_type="stream" denied means no matching network inet stream rule.
3. Missing capability
The process needs a Linux capability (e.g. CAP_NET_BIND_SERVICE, CAP_DAC_OVERRIDE) the profile does not grant.
sudo journalctl -k --no-pager | grep 'apparmor="DENIED"' | grep capability | tail -1
... apparmor="DENIED" operation="capable" profile="/usr/bin/myapp" capability=10 capname="net_bind_service"
capname="net_bind_service" denied means the profile needs capability net_bind_service,.
4. Execute transition not declared
The program execs a helper binary, but the profile has no exec rule (ix/Px/Cx) for it.
sudo journalctl -k --no-pager | grep 'apparmor="DENIED"' | grep 'operation="exec"' | tail -1
... apparmor="DENIED" operation="exec" profile="/usr/bin/myapp" name="/usr/bin/openssl" requested_mask="x" denied_mask="x"
Without an exec rule the child cannot launch.
5. Write to a log/runtime path the profile only allows read on
The profile grants read but the app needs write to a runtime or log directory.
sudo journalctl -k --no-pager | grep 'apparmor="DENIED"' | grep 'denied_mask="w' | tail -1
... apparmor="DENIED" operation="mknod" profile="/usr/sbin/nginx" name="/run/nginx.pid" requested_mask="w" denied_mask="w"
The PID/log path needs a w (or rw) rule.
6. A snap/Docker default profile too restrictive for the workload
The container runtime’s default AppArmor profile (docker-default) or a snap confinement blocks a legitimate syscall/path.
sudo journalctl -k --no-pager | grep 'apparmor="DENIED"' | grep 'docker-default' | tail -1
... apparmor="DENIED" operation="mount" profile="docker-default" name="/proc/..." comm="entrypoint"
The workload needs a custom profile or --security-opt apparmor=unconfined for a specific, audited container.
Diagnostic Workflow
Step 1: Confirm AppArmor is enabled and the profile is enforcing
sudo aa-status | grep -E 'profiles are loaded|enforce mode'
If the failing program’s profile is listed under enforce mode, AppArmor is in play.
Step 2: Pull the exact denial(s) from the kernel log
sudo journalctl -k --no-pager | grep 'apparmor="DENIED"' | tail -10
# or
sudo dmesg | grep 'apparmor="DENIED"' | tail -10
Note profile, operation, denied_mask, and name/family/capability.
Step 3: Confirm it is AppArmor by switching the profile to complain mode
sudo aa-complain /usr/sbin/mysqld
# retry the action; if it now works, AppArmor was the cause
Complain mode logs ALLOWED instead of blocking, so the app proceeds while you collect every needed rule.
Step 4: Capture the needed rules with aa-logprof
sudo aa-logprof
# walk through each logged event; accept the suggested file/network/capability rules
aa-logprof reads the audit log and proposes the precise rules to add to the profile.
Step 5: Re-enforce and verify no new denials
sudo apparmor_parser -r /etc/apparmor.d/usr.sbin.mysqld
sudo aa-enforce /usr/sbin/mysqld
sudo journalctl -k --no-pager | grep 'apparmor="DENIED"' | tail
Example Root Cause Analysis
After moving MySQL’s data directory from /var/lib/mysql to a larger volume at /data/mysql, mysqld fails to start. The error log just says it cannot open ibdata1, and the files are owned by mysql:mysql with correct modes. The kernel log reveals the block:
sudo journalctl -k --no-pager | grep 'apparmor="DENIED"' | grep mysqld | tail -1
apparmor="DENIED" operation="open" profile="/usr/sbin/mysqld" name="/data/mysql/ibdata1" requested_mask="r" denied_mask="r" comm="mysqld"
The stock usr.sbin.mysqld profile only permits /var/lib/mysql/**. Because AppArmor is path-based, the new /data/mysql path is implicitly denied even though Unix permissions are perfect.
Fix: add the new data path to the profile (via a local override so a package update will not clobber it) and reload:
# add to /etc/apparmor.d/local/usr.sbin.mysqld:
# /data/mysql/ r,
# /data/mysql/** rwk,
sudo apparmor_parser -r /etc/apparmor.d/usr.sbin.mysqld
sudo systemctl start mysql
sudo journalctl -k --no-pager | grep 'apparmor="DENIED"' | grep mysqld | tail
(no output)
mysqld starts and the denials stop, with confinement still enforced.
Prevention Best Practices
- Add custom rules in
/etc/apparmor.d/local/<profile>so package updates do not overwrite your changes. - Use complain mode +
aa-logprofto derive the minimal set of rules instead of disabling the profile or running unconfined. - Keep file rules path-specific (
/data/mysql/**, not broad globs) so confinement stays meaningful. - When relocating data/log/runtime paths, update the AppArmor profile as part of the change, not after the outage.
- For containers, prefer a tailored AppArmor profile over
apparmor=unconfined; reserve unconfined for audited, isolated workloads. See the security hardening guides for a profile-authoring baseline. - For triaging a burst of denials after a deploy, the free incident assistant can group
apparmor="DENIED"lines by profile/operation and suggest the file or network rule to add.
Quick Command Reference
# Is AppArmor enforcing the relevant profile?
sudo aa-status | grep -E 'enforce mode'
# Recent denials with profile/operation/target
sudo journalctl -k --no-pager | grep 'apparmor="DENIED"' | tail
sudo dmesg | grep 'apparmor="DENIED"' | tail
# Confirm by switching to complain mode
sudo aa-complain /path/to/binary
# Derive the needed rules from the log
sudo aa-logprof
# Reload a profile after editing
sudo apparmor_parser -r /etc/apparmor.d/<profile>
# Re-enforce
sudo aa-enforce /path/to/binary
Conclusion
An apparmor="DENIED" is a path/operation the program’s profile does not permit, surfacing as a generic Permission denied. Read the kernel log, not just the app’s error:
- A relocated data/log path is not in the profile — add it under
/etc/apparmor.d/local. - A missing
networkrule blocks a socket family the app needs. - A missing
capabilityrule blocks a privileged operation likenet_bind_service. - An undeclared exec transition prevents launching a helper binary.
- A read-only path rule blocks a write the app must make.
- A restrictive default container/snap profile blocks a legitimate syscall.
Confirm with complain mode, let aa-logprof derive the exact rules, then re-enforce — keeping confinement intact beats disabling the profile or running unconfined.
Download the Free 500-Prompt DevOps AI Toolkit
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.