Diagnosing SELinux AVC Denials on AlmaLinux 8 and 9
Permission denied despite correct file permissions on AlmaLinux usually means SELinux. Learn to read AVC denials, run ordered checks, and apply the right fix — boolean, context, port, or module.
01 Aug 2026, 08:15 UTC

The recognizable condition
An application fails with "Permission denied" even though file ownership and mode bits are correct. You run chmod 755, double-check group membership, and the error persists. On AlmaLinux 8/9 with SELinux in enforcing mode (the default), this pattern almost always means SELinux — not traditional Unix permissions (DAC) — is blocking the action.
The confirming evidence lives in /var/log/audit/audit.log. A denial record looks like:
type=AVC msg=audit(1697123456.789:412): avc: denied { name_bind } for pid=2214 comm="httpd" src=8080 scontext=system_u:system_r:httpd_t:s0 tcontext=system_u:object_r:unreserved_port_t:s0 tclass=udp_socket permissive=0The fields that matter: denied { ... } is the attempted operation, scontext is the process's SELinux domain, and tcontext is the target object's type. Every fix below is about reconciling those two labels with policy.
Common causes at a glance
| Cause | Typical signature | Fix class |
|---|---|---|
| Boolean disabled | audit2why suggests "you can turn on a boolean" | setsebool -P |
| Wrong file context | File under a non-default path (e.g., /srv/www) lacks the expected type | semanage fcontext + restorecon |
| Unlabeled port | denied { name_bind } on a non-standard port | semanage port -a |
| Policy gap | audit2why offers no boolean or context suggestion | Custom module (audit2allow), with review |
| Non-persistent boolean | Works until reboot, then fails again | Re-set with -P flag |
Ordered diagnostic checks
Run these as root (or via sudo) on the affected host. If semanage, audit2why, or audit2allow are missing, install them first: dnf install policycoreutils-python-utils. No subscription is required on AlmaLinux.
- Translate the denial.
ausearch -m AVC -ts recent | audit2why. This prints each recent denial with a plain-language explanation and often names the exact boolean or context fix. Start here; it resolves the majority of cases. - Confirm enforcing mode.
sestatus. Verify "Current mode: enforcing" and note the policy version. If the mode is permissive, denials are logged but not blocked — the failure has a different cause. - Check the file context.
ls -Z /path/to/file. Compare the type against what the policy expects (e.g.,httpd_sys_content_tfor web content).matchpathcon /path/to/fileshows what the default context should be for that path. - List relevant booleans.
semanage boolean -l | grep httpd(substitute your service). Note both the current value and whether it is persistent. Boolean names can differ slightly between AlmaLinux 8 (SELinux userspace 3.3) and 9.4+ (userspace 3.6), so always check the live list rather than assuming a name from older documentation. - Check port labeling. If the denial mentions
name_bindorname_connect:semanage port -l | grep 8080. An unlisted port falls into a generic type such asunreserved_port_t, which confined services cannot use.
Fixes tied to each finding
Finding: audit2why names a boolean
Enable it persistently:
setsebool -P httpd_can_network_connect=on
getsebool httpd_can_network_connect # expect: httpd_can_network_connect --> onWithout -P, the change is runtime-only and silently reverts on reboot — a classic "it worked yesterday" failure.
Finding: file has the wrong type
Do not use chcon for permanent fixes; it does not survive a relabel. Register the path in policy, then apply:
semanage fcontext -a -t httpd_sys_content_t '/srv/www(/.*)?'
restorecon -Rv /srv/www
ls -Z /srv/www/index.html # verify the new typeScope restorecon to the specific tree. Running restorecon -R / can overwrite intentional custom contexts elsewhere on the system.
Finding: unlabeled port
semanage port -a -t http_port_t -p tcp 8080
semanage port -l | grep 8080 # verify the mappingIf the port is already labeled with a conflicting type, use -m (modify) instead of -a.
Finding: no boolean or context fix offered
Generate a local module, but review it before loading:
ausearch -m AVC -ts recent | audit2allow -M localfix
cat localfix.te # review the rules BEFORE loading
semodule -i localfix.pp
semodule -l | grep localfix # confirm it loadedaudit2allow grants exactly what was denied — nothing more, nothing less — which means it can over-permit if the denial itself was a symptom of a mislabeled file. Prefer boolean/context fixes first and treat modules as the last resort.
Diagnostic-only: permissive domain
To confirm SELinux is the blocker without disabling it system-wide, put one domain in permissive mode: semanage permissive -a httpd_t. Denials are logged but not enforced for that domain only. Remove it afterward with semanage permissive -d httpd_t. Never use setenforce 0 as a fix — it exposes every service, not just the one you are debugging.
Verifying the fix
Re-run the failing operation, then confirm no new denials were logged:
ausearch -m AVC -ts recentAn empty result for your scenario — plus the application working — is the success criterion. Also re-run the specific verification for your fix class: getsebool for booleans, ls -Z or matchpathcon -V for contexts, semanage port -l for ports, semodule -l for modules.
When to escalate
- The denial persists after boolean, context, and port fixes — the policy gap is structural.
- audit2allow generates rules granting broad capabilities (
self:capabilitysets) orexecute_no_transon generic types — a sign the module would punch a large hole; a hand-written, minimal policy is warranted. - Multiple domains are involved in one denial chain, suggesting a cross-domain interface is missing.
- The software is vendor-supplied with no upstream SELinux policy — engage the vendor, or build a maintained custom module (for containers, consider
udicato generate one). - The audit log shows
type=SELINUX_ERR— possible policy corruption; rebuild or reinstall the policy packages rather than layering modules on top.
One container-specific note: Podman containers run as container_t by default. Host directory access requires the :Z (private) or :z (shared) volume label, which relabels the content automatically — a frequent source of "denials that booleans can't fix."
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.