Skip to content
DevOps AI ToolKit
Newsletter
All guides
AI for DevOps Security & Hardening By James Joyner IV · · 9 min read

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 denied while Unix permissions are correct.
  • apparmor="DENIED" lines appear in dmesg or journalctl -k.
  • The failure stops if the profile is set to complain mode (aa-complain).
  • aa-status shows 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-logprof to 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:

  1. A relocated data/log path is not in the profile — add it under /etc/apparmor.d/local.
  2. A missing network rule blocks a socket family the app needs.
  3. A missing capability rule blocks a privileged operation like net_bind_service.
  4. An undeclared exec transition prevents launching a helper binary.
  5. A read-only path rule blocks a write the app must make.
  6. 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.

Free download · 368-page PDF

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.