/보안/How to Fix SELinux Permission Denied — When chmod Looks Fine but Access Is Still Blocked (avc denied)
SecuritySELinuxavc denied

How to Fix SELinux Permission Denied — When chmod Looks Fine but Access Is Still Blocked (avc denied)

If chmod and chown look fine but nginx or httpd still throw Permission denied, SELinux is the culprit. A practical, case-by-case guide to diagnosing avc denied with getenforce, ausearch, audit2allow, restorecon, and setsebool — plus copy-pa

How to Fix SELinux Permission Denied — When chmod Looks Fine but Access Is Still Blocked (avc denied)

Complete Diagnostic Guide for SELinux 'Permission denied' — When chmod Looks Fine but Access Is Still Blocked

The Real Cause of 'Permission denied' That Survives chmod 777

You've already run chmod 777 and even chown nginx:nginx, but nginx still returns 502, the page throws 403, or the app log shows Permission denied. ls -l looks perfectly fine. At this point the culprit is almost certain: SELinux.

The chmod/chown we all know is DAC (Discretionary Access Control). On RHEL, Rocky, and AlmaLinux 9/10, SELinux (MAC, Mandatory Access Control) is enabled in Enforcing mode by default. Even if DAC lets you through, the kernel blocks the request again if the MAC label (context) doesn't match. That's why you get denied even when permissions look correct.

This is not a SELinux theory lecture. It's a practical guide that organizes commands to unblock access right now, by case. If you're in a hurry, confirm the culprit in Step 1, then copy-paste only the block that matches your symptom in Step 2.

Step 1 — Confirm SELinux Is the Culprit in 30 Seconds

First, check whether SELinux is on with a single command.

Bash
getenforce
# Enforcing  → SELinux is likely blocking you
# Permissive → logs only, does not block (suspect another cause)
# Disabled   → SELinux is not the culprit

If it's Enforcing, capture the denial logs. To watch them in real time:

Bash
tail -f /var/log/audit/audit.log | grep denied
# In another terminal, retry the blocked action (e.g. refresh the page)

For logs that already happened, use ausearch.

Bash
ausearch -m avc -ts recent      # last 10 minutes
ausearch -m avc -ts today       # all of today

Then translate why it was blocked into something a human can read.

Bash
ausearch -m avc -ts recent | audit2why

If setroubleshoot is installed, sealert will even suggest a friendly fix.

Bash
dnf install -y setroubleshoot-server
sealert -a /var/log/audit/audit.log

Anatomy of a Single avc: denied Log Line

A real log looks like this. The arrows show what to look at.

CODE
type=AVC msg=audit(1718440000.123:456): avc:  denied  { read } for
  pid=2314 comm="nginx"                ← ① who (process name)
  name="index.html" dev="vda1" ino=12345
  scontext=system_u:system_r:httpd_t:s0          ← ② source label (process)
  tcontext=unconfined_u:object_r:user_home_t:s0  ← ③ target label (file) ← the culprit!
  tclass=file permissive=0              ← ④ what was accessed (file/dir/tcp_socket…)

The interpretation is simple: httpd_t (the web server) tried to read a file labeled user_home_t (home-directory label) and got blocked. Web content should be httpd_sys_content_t; the label is wrong, so this is a classic denial. The key clue is that tcontext is the wrong context.

Step 2 — Precise Fixes by Root Cause (The Core Section)

Looking at tclass and tcontext in the log, you can almost always tell which case you're in.

① Wrong file context (the most common case)

If tclass=file/dir and the label is something off like default_t, user_home_t, or var_t, it's a context problem. Check first:

Bash
ls -Z /var/www/html
# unconfined_u:object_r:default_t:s0  index.html   ← wrong

If it's a standard path, restoring the policy default is enough.

Bash
restorecon -Rv /var/www/html

If you moved the web root to a non-standard path like /data/www, register a default context rule for that path first, then restore.

Bash
semanage fcontext -a -t httpd_sys_content_t "/data/www(/.*)?"
restorecon -Rv /data/www

② You need a policy for a non-standard port or directory

If you run nginx on a non-standard port like 8080, you'll get a tclass=tcp_socket, name_bind denial. Add a port label.

Bash
semanage port -a -t http_port_t -p tcp 8080
# Use -m instead of -a when modifying an already-registered port
semanage port -l | grep http_port_t   # verify

③ A boolean is turned off

This is the easiest case to miss. For example, if PHP needs to reach an external API or DB but httpd_can_network_connect is off, network connections get blocked. Check the current value, then turn it on.

Bash
getsebool -a | grep httpd
setsebool -P httpd_can_network_connect on

Booleans that commonly get in the way:

BooleanPurposeCommand to enable
httpd_can_network_connectWeb server outbound connections to external hosts/APIssetsebool -P httpd_can_network_connect on
httpd_can_network_connect_dbWeb server connecting to a remote DB (MySQL/PostgreSQL)setsebool -P httpd_can_network_connect_db on
httpd_use_nfsUsing an NFS-mounted path as web contentsetsebool -P httpd_use_nfs on
httpd_enable_homedirsServing ~user/public_htmlsetsebool -P httpd_enable_homedirs on
nis_enabledBroad network access for NIS / external auth integrationsetsebool -P nis_enabled on

The -P flag is critical. Running setsebool without -P applies the change in memory only, so it reverts after a reboot. About 90% of "I turned it on, but it blocked again after restart" incidents are a missing -P. Permanent changes always need -P.

④ You actually need a custom policy

If it's a legitimate action that doesn't match any of the three cases above, generate a dedicated policy module from the denial logs.

Bash
# Collect only nginx denials and generate a module
ausearch -c 'nginx' --raw | audit2allow -M nginxlocal
# Install the generated .pp
semodule -i nginxlocal.pp

# Read what would be allowed first, then decide
ausearch -c 'nginx' --raw | audit2allow

After installing, retry the action and verify that no new avc: denied entries appear. Check the module list with semodule -l | grep nginxlocal.

Step 3 — The Trap of Temporarily Bypassing with 'setenforce 0', and How to Use It Correctly

In a panic, setenforce 0 makes the problem vanish like magic. That's because Permissive mode logs denials without blocking them. Leaving a production server in Permissive, though, is the same as dropping your security controls entirely. Lateral movement and file access that SELinux would have stopped during a compromise all become open.

The correct workflow is not "turn it off" — it's "open it briefly for diagnosis."

Bash
setenforce 0                 # briefly Permissive for diagnosis
# Reproduce every blocked action once → collect all denial logs
ausearch -m avc -ts recent | audit2allow -M myapp
semodule -i myapp.pp         # apply the collected policy for real
setenforce 1                 # return to Enforcing

Permanently disabling it with SELINUX=disabled in /etc/selinux/config is a last resort. Once you boot disabled, file labels stop being updated, so turning it back on later requires a full filesystem relabel (touch /.autorelabel then reboot) — a heavy operational cost. Given that SELinux is increasingly enabled on cloud images and container hosts, the right approach is "handle it, don't disable it."

A common case: Intermittent 403s after every deploy often happen because CI rsyncs files and overwrites the context to default_t. Adding a single restorecon -Rv /var/www/html at the end of the deploy script stopped the recurrence. A large share of SELinux problems aren't "grand policy work" — they end with one line of context restore.

Conclusion: Copy-Paste Command Cheatsheet

When permissions look fine but you still get denied, just follow this order.

SymptomDiagnosisFix
403 / Permission denied (permissions look fine)getenforce → Enforcing?Continue below
Don't know which action was blockedausearch -m avc -ts recent | audit2whyIdentify the cause branch
File label is wrong (ls -Z)tcontext is default_t or similarrestorecon -Rv /var/www/html
Non-standard web rootNo label rulesemanage fcontext -a -t httpd_sys_content_t "/data/www(/.*)?"restorecon -Rv
Non-standard port (8080)tclass=tcp_socketsemanage port -a -t http_port_t -p tcp 8080
External connection / DB blockedgetsebool -a | grep httpdsetsebool -P httpd_can_network_connect on
None of the aboveLegitimate action still deniedausearch -c 'nginx' --raw | audit2allow -M nginxlocal && semodule -i nginxlocal.pp

Closing checklist: ① Confirm the culprit with getenforce → ② Collect logs with ausearch → ③ Apply the command for your case → ④ Always use -P with setsebool → ⑤ After diagnosis, return to setenforce 1.

References: Official Docs

The primary source for the behavior, settings, and errors covered in this post is the following official documentation. Check version-specific options and exact behavior there.

FAQ

Q. Can I just allow everything in the policy that audit2allow generates? A. That's dangerous. audit2allow only generates rules that "allow every blocked action"; it does not judge whether those actions are legitimate. Always pipe to | audit2allow first, read which permissions it would open, and module only what your legitimate workload needs. Allowing denials that came from a compromise creates a security hole.

Q. I enabled it with setsebool, but it blocks again after reboot. A. You most likely omitted -P. Without -P it applies only in runtime memory and resets on reboot. Make it permanent with setsebool -P ....

Q. Can't I just turn SELinux off? A. It's convenient in the moment, but not recommended. RHEL 9, Rocky, and AlmaLinux 9/10 default to Enforcing, and SELinux use is growing in cloud and container environments. If you need to diagnose, briefly collect logs with setenforce 0, apply a policy, then return to setenforce 1. That's the safer workflow.

확인 정보
✦ ✦ ✦
편집 검토 · Editorial Review

Nodelog는 모든 콘텐츠의 내용과 출처를 공개 전에 검토합니다. 환경(OS·버전)에 따라 결과가 달라질 수 있는 기술 정보는 공식 문서와 함께 확인하며, 검토 기준과 정정 원칙은 편집 정책에서 안내합니다. 오류를 발견하시면 이메일로 제보해 주세요 — 확인 후 신속히 정정합니다.

편집 책임 · Nodelog 기술 편집팀·발행 · ·업데이트 ·

Comments

Be the first to comment.