A service can be denied access even when Unix permissions allow it. AppArmor denials on Ubuntu identify the blocking profile and the access a scoped fix must allow.

AppArmor confines programs with profiles that restrict access to files and other resources. A denial does not automatically justify granting access, because an unexpected request can indicate a configuration error or a compromised program. For legitimate access, a local override keeps the added permission separate from packaged policy during updates.

Ubuntu 26.04 LTS includes a tcpdump profile that permits common capture extensions but can block a capture saved with a .dump extension. Troubleshooting requires an installed program with a loaded profile, a reproducible failure, and sudo access. The profile stays in enforce mode throughout the repair.

Steps to troubleshoot AppArmor denials on Ubuntu:

  1. Reproduce the failed capture read.
    $ tcpdump -nn -r /srv/captures/loopback.dump -c 1
    tcpdump: /srv/captures/loopback.dump: Permission denied

    The existing capture contains a harmless loopback packet and is readable under Unix permissions. The -r option reads saved packets without capturing live traffic, -nn avoids name lookups, and -c 1 limits the read to one packet. For another program, the equivalent starting point is the request that fails on your system.

  2. Inspect recent kernel messages for AppArmor denials.
    $ sudo journalctl -k --since "5 minutes ago" --no-pager -o cat --grep 'apparmor="DENIED"'
    ##### snipped #####
    audit: type=1400 audit(1793865901.376:190): apparmor="DENIED" operation="open" class="file" profile="tcpdump" name="/srv/captures/loopback.dump" pid=11823 comm="tcpdump" requested_mask="r" denied_mask="r" fsuid=1000 ouid=0

    profile is tcpdump, the loaded policy that blocked the request. Its name need not equal the executable path or profile filename.

    operation is open, so the program tried to open a file.

    name identifies the resource whose access was checked, the capture file in this event.

    requested_mask is r, meaning the program requested read access.

    denied_mask is r, meaning read access was refused. This failure does not call for write or execute permission.

    The relevant event matches the failing program, path, and time. Kernel denials may not appear in a service's own journal. No matching entry alone does not rule out AppArmor, because explicit deny rules can suppress logging and audit messages can be rate limited; Unix permissions can also cause the same application error.

  3. Confirm that the logged profile is listed under enforce mode.
    $ sudo aa-status
    apparmor module is loaded.
    180 profiles are loaded.
    104 profiles are in enforce mode.
    ##### snipped #####
       tcpdump
    ##### snipped #####

    The totals vary by system; the relevant state is tcpdump inside the enforce-mode section.

  4. Locate the source declaration for the logged profile name.
    $ sudo grep -R -n '^profile tcpdump ' /etc/apparmor.d/
    /etc/apparmor.d/usr.bin.tcpdump:4:profile tcpdump /usr/bin/tcpdump {

    The declaration connects the name tcpdump to /usr/bin/tcpdump and identifies its source file. Other profiles can use a path as their name or have an indented declaration. Generated Snap profiles are managed by snapd and need their owning interface or policy mechanism instead of this package-local edit.

  5. Identify the local override included by that source file.
    $ sudo grep -n 'local/' /etc/apparmor.d/usr.bin.tcpdump
    73:  # Site-specific additions and overrides. See local/README for details.
    74:  #include <local/usr.bin.tcpdump>

    The include line is an AppArmor directive, despite its leading #. It loads the named file under /etc/apparmor.d/local/; an include if exists directive serves the same purpose when present. A local file has no effect unless the profile includes it. At the same priority, explicit deny rules take precedence over allow rules.

  6. Open the included local override in your text editor.
    $ sudoedit /etc/apparmor.d/local/usr.bin.tcpdump

    A scoped fix comes first; disabling a profile or AppArmor removes its protection. The proposed addition lets processes confined by tcpdump read this exact file. It adds no write permission or directory wildcard. A local override is appropriate only when that read is intended; an incorrect application path calls for a configuration correction instead.

  7. Append the exact-path read rule at the end of the local override.
    /srv/captures/loopback.dump r,

    Existing contents remain in place. The trailing comma is required AppArmor syntax. For a different denial, the approved resource and permission must come from that event rather than from the tcpdump case.

  8. Save the local override.
  9. Validate the complete profile without loading it into the kernel.
    $ sudo apparmor_parser -Q -T /etc/apparmor.d/usr.bin.tcpdump

    -Q skips the kernel load and -T skips reading the compiled cache, so the edited include is parsed. Successful validation produces no output. A syntax error must be corrected before reload.

  10. Reload the matching profile.
    $ sudo apparmor_parser -r /etc/apparmor.d/usr.bin.tcpdump

    The reload applies the changed policy to processes using this profile. It produces no output on success; an error means the new rule has not been successfully applied.

  11. Confirm that the reloaded profile remains in enforce mode.
    $ sudo aa-status
    apparmor module is loaded.
    180 profiles are loaded.
    104 profiles are in enforce mode.
    ##### snipped #####
       tcpdump
    ##### snipped #####
  12. Record the beginning of the retest window in the current shell.
    $ retest_start=$(date --iso-8601=seconds)
  13. Repeat the original capture read.
    $ tcpdump -nn -r /srv/captures/loopback.dump -c 1
    reading from file /srv/captures/loopback.dump, link-type EN10MB (Ethernet), snapshot length 262144
    15:59:05.293054 IP 127.0.0.1 > 127.0.0.1: ICMP echo request, id 42671, seq 1, length 64

    The packet is now readable with enforcement active. For a service failure, the equivalent retest is the original failing service request; a running service alone does not prove that request works.

  14. Check the retest window for the same profile-and-path denial.
    $ sudo journalctl -k --since "$retest_start" --no-pager --no-hostname --grep 'apparmor="DENIED".*profile="tcpdump".*name="/srv/captures/loopback[.]dump"'
    -- No entries --

    Success requires both the packet printed during the retest and no matching denial afterward. With –grep, no matches returns status 1; a matching denial returns status 0 and means the failure remains. A journal access error is not proof of absence. The saved time excludes the earlier failure; the packet timestamp belongs to the saved capture and does not date the retest.

Share this guide