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
- Always paste the raw audit/dmesg line. Summarized versions (“nginx can’t read /opt/…”) miss the
tcontextandtclassthat tell you whether the fix is a label, a port, or a boolean. - For SELinux, run
ausearch -m AVC -ts recent | headand paste several lines — the first denial is often masked by cascades. - For AppArmor, paste from
journalctl -k --grep="apparmor=\"DENIED\""ordmesg | grep -i apparmor. - Mention if you’re in
permissive/complainmode — 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 blockedcomm="nginx"— what was doing itscontext=...httpd_t— nginx is running ashttpd_t(correct)tcontext=...default_t— the file has thedefault_tlabel (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"— confirmedoperation="open"+requested_mask="r"— read openprofile="/usr/sbin/nginx"— the nginx profile appliedname="/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
cpfrom/tmpto/var/www/html—cppreserves the source label by default. Usecp --no-preserve=contextorrestorecon -vafter. - Service can’t bind to port 8081 — SELinux port label only allows 80 and 443 for
http_port_t. Add 8081 viasemanage port. - Container runtime denials on a new mount — container needs SELinux context
container_file_t. Use:Zor:zmount 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
nginxbe reading files in/home/<user>/? Probably not. - Should
mysqldbe 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
-
Linux Server Hardening Prompt
Walk an AI through a CIS-style hardening review of a Linux server — services, users, SSH, kernel parameters, file permissions — with safe, ordered remediation.
-
Linux Server Troubleshooting Prompt
Help diagnose CPU, memory, disk, network, and service issues on Ubuntu or RHEL servers from raw command output.
-
Sudoers & Systemd Services Review Prompt
AI review of /etc/sudoers (and /etc/sudoers.d/*) and systemd service unit files for privilege escalation, unsafe defaults, and hardening gaps.