Skip to content
CloudOps
All prompts
AI for Linux Admins Difficulty: Intermediate ClaudeChatGPT

SELinux & AppArmor Denial Decoder Prompt

Decode SELinux AVC denials and AppArmor DENIED entries, identify the right fix (label, policy module, profile tweak), and avoid disabling LSMs as a shortcut.

Target user
Linux sysadmins on RHEL/Fedora (SELinux) and Ubuntu/Debian (AppArmor)
Difficulty
Intermediate
Tools
Claude, ChatGPT

The prompt

You are a senior Linux security engineer who can read SELinux AVC denial messages and AppArmor `DENIED` entries fluently. You know that the right answer is almost never `setenforce 0` or `aa-disable` — it's a targeted policy fix.

I will provide:
- Which LSM is involved: SELinux (`audit.log` / `journalctl _TRANSPORT=audit`) or AppArmor (`dmesg` / `journalctl -k`)
- The failing application + what operation it was trying to do
- One or more raw denial messages
- The OS / distro / kernel
- Current enforcement mode (`getenforce` / `aa-status`)

Your job:

### For SELinux:

1. **Decode the AVC record line by line**:
   - `denied { <op> }` — the denied operation (read, write, open, execute, connect, name_connect, getattr, etc.)
   - `scontext=` — source context (the process trying to do it)
   - `tcontext=` — target context (the file/socket/etc. being acted on)
   - `tclass=` — class of object (file, dir, sock_file, tcp_socket, etc.)
   - `pid=` and `comm=` — what process
2. **Identify the right fix in priority order**:
   - **Wrong label on a file or directory** (most common) → `restorecon -Rv <path>` or `semanage fcontext -a -t <type> '<path>(/.*)?'` then `restorecon`
   - **Service needs a boolean toggled** → `getsebool -a | grep <hint>`, then `setsebool -P <name> on`
   - **Custom port** the service binds to is unlabeled → `semanage port -a -t <type> -p tcp <port>`
   - **Legitimate but undocumented behavior** → generate a local module with `audit2allow -M mymodule`, review, then load with `semodule -i mymodule.pp`
   - **Bug in policy** → file with the distro, run in permissive for that domain only with `semanage permissive -a <type>`
3. **Never recommend** `setenforce 0` as the fix. If unblocking urgently is required, use `semanage permissive -a <domain>` to permissive-mode only the one domain.

### For AppArmor:

1. **Decode the DENIED line**:
   - `apparmor="DENIED"` confirms the denial
   - `operation="open" / "exec" / "connect"`
   - `profile="<name>"` — which profile applied
   - `name="<path>"` — what was accessed
   - `requested_mask=` vs `denied_mask=` (r, w, x, m for mmap-exec, a for append, l for link)
2. **Identify the right fix**:
   - **Path needed in profile** → use `aa-logprof` (interactive) to add the rule
   - **Profile too restrictive for a legitimate path** → edit `/etc/apparmor.d/<profile>` and `apparmor_parser -r`
   - **App should be unconfined** (rare; usually wrong answer) → `aa-disable <profile>` (reversible) or remove the profile symlink in `/etc/apparmor.d/disable/`
3. **Never recommend** `systemctl stop apparmor` as the fix.

### For both:

4. **Distinguish "real denial that should pass" from "real denial that should NOT pass"**: sometimes the LSM is correctly blocking suspicious behavior. Before adding allow rules, ask if the app *should* be doing this.
5. **Mark DESTRUCTIVE actions** clearly: disabling enforcement, mass-restorecon on `/`, loading audit2allow output without review.

---

LSM: [SELinux / AppArmor]
Distro + version: [e.g., RHEL 9 / Ubuntu 22.04]
Current mode: [`getenforce` output / `aa-status` summary]
Application + operation: [e.g., nginx trying to read /opt/myapp/static]
Denial messages (multiple OK, paste raw):
```
[PASTE — full audit.log AVC lines OR full dmesg/journal apparmor DENIED lines]
```
What you've already tried:
[DESCRIBE]

Why this prompt works

SELinux and AppArmor denials look opaque (“permission denied” with no obvious file-permission reason) and the path of least resistance is to disable enforcement — which is almost always the wrong answer in production. This prompt forces a targeted fix: relabel the file, toggle the boolean, or add the specific rule.

How to use it

  1. Always paste the raw audit/dmesg line. Summarized versions (“nginx can’t read /opt/…”) miss the tcontext and tclass that tell you whether the fix is a label, a port, or a boolean.
  2. For SELinux, run ausearch -m AVC -ts recent | head and paste several lines — the first denial is often masked by cascades.
  3. For AppArmor, paste from journalctl -k --grep="apparmor=\"DENIED\"" or dmesg | grep -i apparmor.
  4. Mention if you’re in permissive / complain mode — denials still log but operations succeed; useful for finding all the rules you’d need.

Useful commands

SELinux

# Status
getenforce
sestatus

# Find recent AVC denials
sudo ausearch -m AVC -ts recent
sudo ausearch -m AVC -ts today
sudo journalctl _TRANSPORT=audit --since "1 hour ago" | grep AVC

# Friendly summary with hints
sudo sealert -l "*"          # if setroubleshoot-server installed
sudo sealert -a /var/log/audit/audit.log

# Inspect labels
ls -Z /path/to/file
ps -eZ | grep <process>
ss -tnlpZ                    # socket labels

# Common fixes
# 1. Relabel a path to the correct default type
sudo restorecon -Rv /path/to/dir

# 2. Add a custom path with a specific label persistently
sudo semanage fcontext -a -t httpd_sys_content_t '/opt/myapp/static(/.*)?'
sudo restorecon -Rv /opt/myapp/static

# 3. Allow a service on a custom port
sudo semanage port -a -t http_port_t -p tcp 8081

# 4. Toggle a boolean
getsebool -a | grep -i httpd
sudo setsebool -P httpd_can_network_connect on

# 5. Generate a local module from denials (REVIEW THE .te FILE FIRST)
sudo ausearch -m AVC -ts recent | audit2allow -M mymodule
cat mymodule.te                       # REVIEW
sudo semodule -i mymodule.pp

# 6. Permissive a single domain (better than setenforce 0)
sudo semanage permissive -a httpd_t

# Inspect a domain
sudo seinfo -tnginx_t -x        # if setools installed

AppArmor

# Status
sudo aa-status

# Find denials
sudo journalctl -k --since "1 hour ago" | grep -i apparmor
sudo dmesg | grep -i apparmor

# Friendly view + interactive fix
sudo aa-logprof                # asks you per denial what to do; updates profile

# Profile management
ls /etc/apparmor.d/
sudo apparmor_parser -r /etc/apparmor.d/usr.bin.myapp    # reload after edit
sudo aa-enforce /etc/apparmor.d/usr.bin.myapp
sudo aa-complain /etc/apparmor.d/usr.bin.myapp           # complain (still log, allow)
sudo aa-disable /etc/apparmor.d/usr.bin.myapp            # reversible; symlinks to disable/

# Generate a new profile (start in complain)
sudo aa-genprof /usr/bin/myapp

Decoding a typical SELinux AVC

type=AVC msg=audit(1700000000.123:456): avc:  denied  { read } for  pid=12345 comm="nginx"
  name="config.json" dev="dm-0" ino=12345
  scontext=system_u:system_r:httpd_t:s0
  tcontext=unconfined_u:object_r:default_t:s0
  tclass=file permissive=0

Reading:

  • denied { read } — operation blocked
  • comm="nginx" — what was doing it
  • scontext=...httpd_t — nginx is running as httpd_t (correct)
  • tcontext=...default_t — the file has the default_t label (this is the problem — nginx can’t read default_t files)
  • tclass=file — it’s a regular file

Fix: relabel the file to httpd_sys_content_t:

sudo semanage fcontext -a -t httpd_sys_content_t '/opt/myapp/config.json'
sudo restorecon -v /opt/myapp/config.json

Decoding a typical AppArmor DENIED

audit: type=1400 audit(1700000000.123:456): apparmor="DENIED"
  operation="open" profile="/usr/sbin/nginx"
  name="/opt/myapp/config.json" pid=12345 comm="nginx"
  requested_mask="r" denied_mask="r" fsuid=33 ouid=33

Reading:

  • apparmor="DENIED" — confirmed
  • operation="open" + requested_mask="r" — read open
  • profile="/usr/sbin/nginx" — the nginx profile applied
  • name="/opt/myapp/config.json" — the path it tried to read

Fix: add /opt/myapp/** r, to the nginx profile:

sudo nano /etc/apparmor.d/usr.sbin.nginx
# add inside the profile braces:
/opt/myapp/** r,
sudo apparmor_parser -r /etc/apparmor.d/usr.sbin.nginx

Common findings this catches

  • App reads a file copied with cp from /tmp to /var/www/htmlcp preserves the source label by default. Use cp --no-preserve=context or restorecon -v after.
  • Service can’t bind to port 8081 — SELinux port label only allows 80 and 443 for http_port_t. Add 8081 via semanage port.
  • Container runtime denials on a new mount — container needs SELinux context container_file_t. Use :Z or :z mount option in Docker/Podman.
  • AppArmor breaks after binary upgrade — profile path no longer matches; profile needs regenerating with aa-genprof.
  • SELinux booleans you forgot existed: httpd_can_network_connect, samba_export_all_rw, nfs_export_all_rw. Toggling beats relabeling for these.

When the LSM is correctly blocking

Before adding allow rules, ask:

  • Should nginx be reading files in /home/<user>/? Probably not.
  • Should mysqld be writing to /etc/? No.
  • Should an SSH daemon be executing files in /tmp? Suspicious.

A denial is sometimes the LSM doing its job. Investigate before you allow.

When to escalate

  • A flurry of denials right after a CVE-fix package update — possible policy regression upstream; check distro bug tracker.
  • Repeated AVCs from a domain you don’t recognize — possible compromise; engage security.
  • Recommendations to load large auto-generated audit2allow modules — review module by module with a second engineer.

Related prompts

Newsletter

Get weekly AI workflows for DevOps engineers

Practical prompts, automation ideas, and tool reviews for infrastructure engineers. One email per week. No spam.