nginx 配置全对却 403:一次 Linux SELinux 安全上下文排查复盘

nginx 配置、目录权限、文件属主全对,浏览器访问却稳定 403 Forbidden。排查梳理:Linux 文件访问要过两道门,传统 rwx 之外还有 SELinux 强制访问控制;getenforce 看模式、setenforce 0 快速确认;安全上下文的进程类型与文件类型、ls -Z 看 type;ausearch 与 sealert 读 AVC 拒绝日志;semanage fcontext + restorecon 给自定义目录注册持久规则、setsebool 调布尔开关,以及一套 SELinux 排查纪律。

2024 年,我在一台新装的 CentOS 7 上部署一个静态站点,用 nginx 把一个目录挂出去。配置文件我写得很顺手,root 指向那个目录,server_name、端口都没问题。文件我也放好了,chmod 755 给足了权限,所有者也对。可浏览器一访问,迎面一个冷冰冰的 403 Forbidden。我太熟悉 403 了——不就是权限问题嘛。于是我把 nginx 配置、目录权限、文件所有者、nginx 运行用户,前前后后查了不下五遍,每一项都对得明明白白,挑不出一丝毛病。可 403 就是雷打不动。把我逼到墙角的,是这样一个事实:按我所知道的全部 Linux 权限知识,这个文件 nginx 就是该能读到——可它偏偏读不到,像是有一道我看不见的墙,横在文件和 nginx 之间。后来才知道,那道墙真实存在,它叫 SELinux。这件事逼着我把 Linux 的 SELinux、安全上下文、审计日志这一整套彻底理清了。本文复盘这次实战。

问题背景

环境:CentOS 7 全新安装,nginx 部署静态站点
事故现象:
- nginx 配置正确,root 指向 /data/www
- 文件 chmod 755,所有者也对,nginx 用户能读
- 浏览器访问,稳定 403 Forbidden
- nginx error.log:Permission denied,可传统权限明明都对

现场排查:
# 1. 传统权限 —— 反复确认,全对
$ ls -l /data/www/index.html
-rwxr-xr-x. 1 nginx nginx 2048 ... index.html   # ★ 权限、属主都对

# 2. ★ nginx 进程以 nginx 用户跑,理论上读得到
$ ps -ef | grep nginx
nginx  8801  ...  nginx: worker process

# 3. ★ 注意 ls -l 那个权限位末尾的"点" —— 它在暗示 SELinux
$ ls -l /data/www/index.html
-rwxr-xr-x【.】 1 nginx ...                       # ★ 这个 . = 有 SELinux 上下文

# 4. ★ 看 SELinux 是不是开着
$ getenforce
Enforcing                                         # ★ 开着,且在强制模式

# 5. ★ 看 SELinux 的安全上下文 —— ls -Z
$ ls -Z /data/www/index.html
... unconfined_u:object_r:default_t:s0 index.html
#                          ^^^^^^^^^^ ★ default_t —— 不对!
$ ls -Z /usr/share/nginx/html/index.html
... system_u:object_r:httpd_sys_content_t:s0       # ★ 正常该是这个 type

根因(后来想清楚的):
1. ★ Linux 的权限有【两套】:传统的 rwx(自主访问控制),
   和 SELinux 的安全上下文(强制访问控制)。
2. 我反复查的 rwx,只是【第一道门】,它全开着。
3. ★ SELinux 是【第二道门】:它额外规定"哪个类型的
   进程,能访问哪个类型的文件"。
4. nginx 进程的类型是 httpd_t,SELinux 的策略规定
   httpd_t 只能读 httpd_sys_content_t 类型的文件。
5. ★ 我的文件在 /data/www,是手动建的目录,文件的
   type 是 default_t —— 不在 httpd_t 被允许读的清单里。
6. 于是:传统权限放行,SELinux 拦截 -> 403。
   两道门,过了第一道,卡在第二道。
ls -l 末尾有"点"、getenforce 是 Enforcing,就要想到 SELinux。

修复 1:SELinux 是什么——传统权限之上的第二道门

# === ★ 先建立模型:Linux 上,一次文件访问要过【两道门】===

# === 第一道门:传统权限(DAC,自主访问控制)===
# 就是我们最熟的 rwx + 属主/属组。
# 它的逻辑:文件的【拥有者】决定谁能读写。
$ ls -l index.html
-rwxr-xr-x. 1 nginx nginx ...
# ★ 这道门,我反复查的就是它,它全是放行的。

# === ★ 第二道门:SELinux(MAC,强制访问控制)===
# 它【不看】属主,而是给【每个进程】和【每个文件】
# 都贴一个"标签"(安全上下文),然后由【系统策略】
# 统一规定:带 A 标签的进程,能不能碰带 B 标签的文件。
# ★ 关键区别:这道门【文件拥有者说了不算】,
#   策略说了算 —— 这就是"强制(Mandatory)"的含义。

# === 为什么要有第二道门 ===
# 传统权限有个软肋:一个进程被攻破后,它能访问的东西,
#   就是它运行用户能访问的【全部】。
# ★ SELinux 把进程"关进笼子":就算 nginx 被攻破,
#   它也只能碰 SELinux 允许 httpd_t 碰的那一小撮文件,
#   碰不了 /etc/shadow、碰不了别人的数据。

# === ★ 一次访问的完整判定 ===
# 进程访问文件 ->【先过传统 rwx】-> 通过
#             ->【再过 SELinux 策略】-> 通过 -> 真正放行
# ★ 两道门【都】点头才行。任何一道拒绝,访问就失败。
# 我这次:第一道点头,第二道摇头 —— 结果就是 403。

# === ls -l 那个不起眼的"点",就是 SELinux 的指示灯 ===
$ ls -l index.html
-rwxr-xr-x. 1 ...        # ★ 末尾的 . = 这个文件【有】SELinux 上下文
# 看到这个点,就该意识到:这台机器上,第二道门是开着的。

修复 2:getenforce 与三种模式——先确认是不是 SELinux

# === ★ 排查时,第一步先搞清楚 SELinux 现在是什么状态 ===

# === 看当前模式 ===
$ getenforce
Enforcing
# SELinux 有三种模式:
#   Enforcing   ★ 强制:策略不允许的,真的【拦截】
#   Permissive  宽容:不允许的【只记日志、不拦截】(排查神器)
#   Disabled    彻底关闭

# === 看更详细的状态 ===
$ sestatus
SELinux status:        enabled
Current mode:          enforcing
Mode from config file: enforcing
Loaded policy name:    targeted

# === ★ 排查核心技巧:临时切到 Permissive,看问题消不消失 ===
$ setenforce 0            # 0 = 临时切到 Permissive
$ setenforce 1            # 1 = 切回 Enforcing
# ★ 切到 0 之后,如果 403 立刻好了 —— 实锤就是 SELinux 拦的。
#   切到 0 还是 403 -> 跟 SELinux 无关,别冤枉它。
# ★ setenforce 是【临时】的,重启就回到配置文件的值。

# === 这是个【精准的判断手段】,不是解决方案 ===
# 很多人查到这一步,图省事就把 SELinux 关了完事 ——
# ★ 强烈不建议。SELinux 是一道真实的安全防线,
#   关掉它等于拆掉第二道门。正确做法是【修上下文】(下文)。

# === 永久改模式(真要改才动它)===
$ vi /etc/selinux/config
SELINUX=enforcing         # enforcing / permissive / disabled
# ★ 改 Disabled 需要重启;且从 Disabled 重新启用比较麻烦。
# 我的建议:用 setenforce 0 临时确认问题,然后老老实实
#   修上下文,最后 setenforce 1 切回来 —— 别永久关。

修复 3:安全上下文——进程和文件身上的"标签"

# === ★ SELinux 的核心,就是给一切贴"安全上下文"标签 ===

# === 看文件的上下文:ls -Z ===
$ ls -Z /data/www/index.html
unconfined_u:object_r:default_t:s0  index.html
#    (1)用户    (2)角色   (3)类型   (4)级别
# ★ 这四段里,日常排查【最关键的是第(3)段:类型(type)】。

# === 看进程的上下文:ps -Z ===
$ ps -eZ | grep nginx
system_u:system_r:httpd_t:s0  8801  nginx: worker process
#                  ^^^^^^^ ★ nginx 进程的类型 = httpd_t

# === ★ SELinux 的判定,本质就一句话 ===
# "类型为 httpd_t 的进程,能访问类型为 X 的文件吗?"
# 系统策略里写死了:httpd_t 能读 httpd_sys_content_t,
#   能写 httpd_sys_rw_content_t …… 但【不包括 default_t】。

# === ★ 对比正常的和出问题的,差异一目了然 ===
$ ls -Z /usr/share/nginx/html/index.html      # nginx 默认目录
... httpd_sys_content_t ...                    # ★ 类型对
$ ls -Z /data/www/index.html                   # 我手建的目录
... default_t ...                               # ★ 类型错!
# ★ 同样的 rwx,一个能被 nginx 读,一个不能 ——
#   差别【只】在这个 SELinux 类型上。

# === 几个 web 场景常见的文件类型 ===
# httpd_sys_content_t       网站静态文件(只读)
# httpd_sys_rw_content_t    网站需要写的目录(上传等)
# httpd_log_t               日志目录
# ★ 文件【放在哪个目录】,默认会继承那个目录的类型规则 ——
#   这就是为什么手建的 /data/www 里文件都是 default_t。

# === 查某进程/某文件该用什么上下文 ===
$ id -Z                          # 看当前用户的 SELinux 上下文
$ matchpathcon /data/www         # ★ 看某路径【应该】是什么上下文

修复 4:看 SELinux 拒绝日志——别再瞎猜

# === ★ SELinux 拦了什么,不用猜 —— 它每一次拦截都记日志 ===

# === 拒绝记录在审计日志里 ===
$ tail -f /var/log/audit/audit.log | grep AVC
type=AVC ... denied { read } for pid=8801 comm="nginx" \
  name="index.html" ... scontext=...:httpd_t:... \
  tcontext=...:default_t:...
# ★ 一行 AVC denied 把一切说清了:
#   denied { read }   —— 被拒的动作是 read
#   comm="nginx"      —— 谁被拒了
#   scontext httpd_t  —— 发起方(源)的类型
#   tcontext default_t —— 被访问对象(目标)的类型
# 翻译:httpd_t 想 read 一个 default_t 的文件,策略不允许。

# === ★ 更好用:ausearch 专门查 SELinux 拒绝 ===
$ ausearch -m AVC -ts recent          # 最近的 AVC 拒绝记录
$ ausearch -m AVC -ts today           # 今天的
$ ausearch -m AVC -c nginx            # 只看 nginx 相关的

# === ★ 最贴心的:sealert 直接给"人话解释 + 修复建议" ===
$ sealert -a /var/log/audit/audit.log
# 它会逐条分析 AVC,并【直接给出该执行的修复命令】——
# 比如告诉你 "run restorecon ..." 或 "setsebool ..."。
# ★ 排查 SELinux,养成习惯:先 sealert,照它的建议来。
# (sealert 来自 setroubleshoot-server 包,可能要先装)

# === 一个坑:Permissive 模式下也照样记 AVC ===
# ★ 这其实是好事:可以先 setenforce 0(不拦截),
#   把功能跑一遍,让 audit.log 把【所有】会被拦的点
#   都记下来,再一次性修完 —— 避免改一个发现还有下一个。

修复 5:修复上下文与布尔值——正确的解法

# === ★ 找到原因后,正确的修法有两类:改上下文、调布尔值 ===

# === 解法 A:restorecon —— 把文件恢复成"该有的"上下文 ===
$ restorecon -Rv /data/www
# restorecon = 按系统策略,把路径恢复成它【应该】的上下文。
# ★ 但注意:/data/www 是我【自定义】的目录,系统默认策略里
#   没有它的规则 —— 直接 restorecon 它还是会变回 default_t。

# === ★ 解法 B(本场景正解):semanage 注册自定义路径规则 ===
$ semanage fcontext -a -t httpd_sys_content_t "/data/www(/.*)?"
# ★ 这一句的意思:告诉 SELinux —— "/data/www 及其下所有文件,
#   今后【应该】是 httpd_sys_content_t 类型"。
$ restorecon -Rv /data/www
# ★ 再 restorecon,这次它就按新规则,把整个目录刷成对的类型。
$ ls -Z /data/www/index.html
... httpd_sys_content_t ...        # ★ 类型对了,403 消失!
# semanage + restorecon 是【持久】的:重打标签、系统重启都不丢。

# === chcon:临时改一个文件的上下文(不推荐长期用)===
$ chcon -t httpd_sys_content_t /data/www/index.html
# ★ chcon 改动【不写入策略】,下次 restorecon 或 relabel
#   就被打回原形。只适合临时验证,正解还是 semanage。

# === ★ 解法 C:有些功能不是上下文问题,是"布尔开关"关着 ===
# SELinux 有一堆"布尔值(boolean)",是策略里的开关。
$ getsebool -a | grep httpd        # 看 httpd 相关的所有开关
httpd_can_network_connect --> off  # ★ 比如这个:nginx 能否对外发起连接
# 典型场景:nginx 做反向代理 proxy_pass,会被这个开关拦。
$ setsebool -P httpd_can_network_connect on
# ★ -P = 持久化(写进策略,重启不丢)。不加 -P 只是临时。

# === 怎么判断该用哪个解法 ===
# AVC 里是"文件类型不对(tcontext)"  -> semanage fcontext + restorecon
# 功能是"建网络连接/发邮件/写某目录" -> 多半是某个 boolean -> setsebool
# ★ 拿不准就跑 sealert,它会直接告诉你该用哪条命令。

修复 6:SELinux 排查纪律

# === 这次事故暴露的认知盲区,定几条纪律 ===

# === 1. ★ 权限对却访问失败,先想到 SELinux 这第二道门 ===
$ ls -l 文件           # 末尾有 . = 有 SELinux 上下文
$ getenforce           # Enforcing = 第二道门开着

# === 2. ★ 用 setenforce 0 快速确认是不是 SELinux ===
$ setenforce 0         # 切宽容,问题没了 -> 实锤是 SELinux
$ setenforce 1         # ★ 确认完切回来,别图省事永久关

# === 3. ★ 别猜,看 AVC 日志 ===
$ ausearch -m AVC -ts recent       # 看拒绝记录
$ sealert -a /var/log/audit/audit.log  # 看人话解释 + 修复建议

# === 4. 对比上下文找差异:ls -Z 比正常的和异常的 ===
$ ls -Z 异常文件; ls -Z 正常文件   # 差异几乎全在 type 段

# === 5. ★ 自定义目录用 semanage + restorecon,持久 ===
$ semanage fcontext -a -t 类型 "/路径(/.*)?"
$ restorecon -Rv /路径
# chcon 只适合临时验证,会被打回原形。

# === 6. 功能类问题(网络连接等)查 boolean ===
$ getsebool -a | grep 关键词
$ setsebool -P 开关名 on            # -P 持久化

# === 7. 排查 SELinux 问题的命令链 ===
$ getenforce                       # ① 第二道门开着吗
$ setenforce 0                     # ② 临时关,问题消失=实锤
$ ausearch -m AVC -ts recent       # ③ 看它拦了什么
$ ls -Z 文件                       # ④ 看文件当前上下文
$ sealert -a /var/log/audit/audit.log  # ⑤ 看修复建议
$ semanage fcontext / setsebool    # ⑥ 持久化修复
# 按这个顺序,SELinux 问题基本能定位。

命令速查

需求                        命令
=============================================================
看 SELinux 当前模式         getenforce  /  sestatus
临时切宽容/强制             setenforce 0  /  setenforce 1
看文件的安全上下文          ls -Z 文件
看进程的安全上下文          ps -eZ | grep 进程
看路径应该是什么上下文      matchpathcon 路径
查 SELinux 拒绝记录         ausearch -m AVC -ts recent
看人话解释和修复建议        sealert -a /var/log/audit/audit.log
注册自定义路径上下文规则    semanage fcontext -a -t 类型 "/路径(/.*)?"
按规则恢复上下文            restorecon -Rv /路径
看/改 SELinux 布尔开关      getsebool -a | grep X  /  setsebool -P 开关 on

口诀:权限全对却访问失败 -> setenforce 0 试,好了就是 SELinux
      文件类型不对用 semanage+restorecon;功能被拦查 setsebool

避坑清单

  1. Linux 文件访问要过两道门:传统 rwx 权限和 SELinux 策略,都放行才通
  2. ls -l 权限位末尾有点表示文件带 SELinux 上下文,getenforce 看门开没开
  3. 权限全对却 403/Permission denied,用 setenforce 0 临时切宽容快速确认
  4. setenforce 0 是临时排查手段,确认后要切回 1,别图省事永久关 SELinux
  5. SELinux 判定看的是进程类型和文件类型,ls -Z 第三段 type 最关键
  6. 手动建的目录里文件默认是 default_t,不在 httpd_t 允许读的清单里
  7. SELinux 每次拦截都记 AVC 日志,ausearch 查记录、sealert 给修复建议
  8. 自定义目录用 semanage fcontext 注册规则再 restorecon,改动才持久
  9. chcon 改的上下文不写入策略,下次 restorecon 或重打标签会被打回原形
  10. 功能类问题如反代发起网络连接,多半是某个 boolean 关着,用 setsebool -P 打开

总结

这次"nginx 配置全对、文件权限全对,却死活 403"的事故,纠正了我一个根深蒂固的认知盲区——在我脑子里,Linux 的"权限"一直是一个完整的、不留缝隙的整体。我所理解的权限,就是 ls -l 打印出来的那一行:那九个 rwx 位,加上文件的属主和属组。一个进程能不能读一个文件,在我看来,就是这一行信息说了算的、唯一的、终局的判定。正是这个"权限只有一套"的假设,把我牢牢困在了原地:我对着 rwx、对着属主查了一遍又一遍,每一次的结论都是"按规则,nginx 完全应该读得到这个文件"。可现实偏偏是它读不到。当一个系统的行为,和你脑中那套自认为完备的规则发生了正面冲突,问题往往不在规则的细节,而在于——你脑中那套规则,它本身就是不完整的。复盘到根上,我才真正理解,Linux 上一次文件访问要闯的,从来不是一道门,而是两道。第一道门,就是我无比熟悉的传统权限,它有个学名叫 DAC,自主访问控制——"自主"的意思是,这道门的开合,由文件的拥有者自己决定,他想给谁权限就给谁。这道门,我反复确认过,它对 nginx 是敞开的。可在它之后,还立着第二道我从来不知道其存在的门,它叫 SELinux,它的学名是 MAC,强制访问控制。这道门的逻辑和第一道截然不同:它根本不看文件属于谁,它给系统里的每一个进程、每一个文件,都贴上一张叫"安全上下文"的标签,然后由一套全局的、文件拥有者无权干涉的策略来裁决——带着这种标签的进程,到底能不能碰带着那种标签的文件。"强制"二字的分量正在于此:它不容文件主人讨价还价。我的 nginx 进程,带着 httpd_t 这张标签;而我亲手在 /data/www 里建起来的文件,因为那是个系统策略并不认识的自定义目录,继承到的是一张 default_t 的标签。系统策略里清清楚楚地写着:httpd_t 的进程,可以去读 httpd_sys_content_t 的文件——但这张允许清单里,从来没有 default_t 的名字。于是事情就成了这样:nginx 拿着第一道门的通行证,顺利地推开了它,却结结实实地撞在了第二道门上;那道门不关心它的属主是不是 nginx、权限是不是 755,它只问一句"你这个 httpd_t,凭什么读我这个 default_t",然后给出了否定的答案。那个让我百思不解的 403,就是第二道门的回声。这次从一个"配置全对却 403"的死局里走出来,我最大的收获,是在脑子里那张"Linux 权限"的地图上,郑重地添上了过去完全空白的另一半。ls -l 那一行 rwx,它真实、它重要,但它只是两道门里的第一道;它说"可以",不等于访问就一定能成。每当 ls -l 末尾静静躺着的那个小小的点,它就是在提醒我:这台机器上,还有第二道门开着。排查问题最危险的处境,从来不是规则太复杂,而是你以为自己已经掌握了全部规则——而真正的答案,恰恰藏在你那张自以为完整的地图之外,那片你从未标注过的疆域里。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
Linux教程

服务跑着跑着报 Too many open files:一次 Linux 文件描述符限制排查复盘

2026-5-20 19:52:00

Linux教程

能 ping 通服务却连不上:一次 Linux 网络分层排查复盘

2026-5-20 20:02:31

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索