上一篇 下一篇 分享链接 返回 返回顶部

如何在香港服务器上配置 RHEL 9 的 SELinux 策略,既保证安全合规又避免业务中断?

发布人:Minchunlin 发布时间:2025-08-18 10:25 阅读量:623


我第一次在香港葵涌机房把 SELinux 从 Permissive 切回 Enforcing 的那晚,是凌晨 2:17。冷风从机房走道的缝里灌进来,Nginx 监控曲线在屏幕上稳稳地跳着。我手边两样东西:一张已经演练过三遍的回滚清单,和一台随时可用的带外 KVM。十分钟后,访问峰值会到来。我需要让这台 RHEL 9 的生产机在不惊动任何用户的情况下,完成一次“无声”的安全收口。

下面,我按我在现场的实际做法,完整还原 规划 → 审计 → 改造 → 切换 → 验证 → 应急 的全过程。文章细到命令、端口、上下文、策略模块与坑点,新手照做能复刻,老手也能挑出关键差异快速落地。

一、现场环境与边界条件

1) 硬件与系统基线(实测环境)

类别 参数
机房 香港(HK1 机柜,双路供电 + 独立带外管理)
服务器 Supermicro 1U(单路 AMD EPYC 7443P,24C/48T)
内存 128GB ECC
系统盘 2 × NVMe 2TB(RAID1,mdadm)
数据盘 2 × NVMe 3.84TB(LVM + XFS)
网卡 2 × 10GbE(Bonding mode=active-backup)
OS RHEL 9.x(默认 targeted 策略)
主要服务 Nginx(443/8443)、Node.js 应用(127.0.0.1:3000)、Redis(6380,仅内网)、PostgreSQL(5432)
容器 Podman(部分作业使用)
依赖包 policycoreutils, policycoreutils-python-utils, setroubleshoot-server, setools-console, container-selinux

说明:RHEL 9 默认策略为 targeted,重点约束网络暴露的守护进程;这对我们最关键的 httpd/nginx、sshd、容器域都有直接影响。

2) 端口与 SELinux 类型映射(目标)

服务 端口 目标类型(type) 备注
Nginx HTTPS 443 http_port_t 默认已支持
Nginx 管理口 8443 http_port_t 需要手工登记
应用后端 127.0.0.1:3000 (本地回环) 通过 httpd_can_network_connect 放行
Redis 6380 redis_port_t 非默认端口需登记
PostgreSQL 5432 postgres_port_t 默认已支持
Node Exporter 9100 node_exporter_port_t 如自定义需登记

二、总体策略:三阶段“零中断”收口

审计先行(收集证据,不拦业务)

系统保持 Enforcing,但对关键域做 按域宽松:对 httpd_t(Nginx/HTTP 服务)、container_t 临时设为 permissive,记录 AVC 日志而不拦截。

好处:其它域仍受严格约束,业务不被突然阻断。

就地修复(先用官方开关与上下文,最后才写策略)

优先使用 SELinux 布尔值(booleans)、端口类型(semanage port)、文件上下文(semanage fcontext + restorecon)解决 80% 问题。

只有当确实需要时,最小化编写自定义策略模块(.te → .pp)。

窗口切换(可回滚)

预设 一键回滚:setenforce 0、GRUB 内核参数 enforcing=0、带外 KVM。

切回 Enforcing 前,先下掉按域宽松,再灰度(按流量入口或按节点)推进。

三、上手即用的变更剧本(命令 + 验证 + 回滚)

前置:安装工具 & 快速体检

dnf install -y policycoreutils policycoreutils-python-utils setroubleshoot-server setools-console container-selinux

sestatus
getenforce
rpm -q selinux-policy-targeted

1) 按域宽松(只对 HTTP 与容器)

# 仅临时用于审计阶段
semanage permissive -a httpd_t
semanage permissive -a container_t

# 检查
semanage permissive -l | egrep 'httpd_t|container_t'

这样做的核心是:系统仍 Enforcing,但 HTTP/容器相关拒绝只记日志不拦截,方便我们安全收集“真实流量下”的证据。

2) 端口登记(避免“端口不在类型白名单”)

# 为 Nginx 的 8443 增加 http 端口类型
semanage port -a -t http_port_t -p tcp 8443

# 为 Redis 的 6380 增加 redis 类型
semanage port -a -t redis_port_t -p tcp 6380

# 验证
semanage port -l | egrep 'http_port_t|redis_port_t' | egrep '8443|6380'

3) 文件上下文纠偏(RW 目录、Socket、日志)

假设 Web 根 /var/www/myapp,上传目录与缓存目录需要可写:

# 让上传与缓存目录具备 httpd 可写权限
semanage fcontext -a -t httpd_sys_rw_content_t '/var/www/myapp/uploads(/.*)?'
semanage fcontext -a -t httpd_sys_rw_content_t '/var/www/myapp/cache(/.*)?'

# Nginx/应用的 Unix Socket
semanage fcontext -a -t httpd_var_run_t '/run/myapp\.sock'

# 日志目录(供 httpd 写)
semanage fcontext -a -t httpd_log_t '/var/log/myapp(/.*)?'

# 应用
restorecon -Rv /var/www/myapp /run/myapp.sock /var/log/myapp

经验:restorecon -Rv 必做,很多“权限明明对”的 13/403 错误,都是上下文没持久化导致的。

4) 布尔值开关(先开官方能力)

Web 连接本地后端/数据库是常态,优先开布尔:

# httpd 可以联网(含本地 127.0.0.1:3000)
setsebool -P httpd_can_network_connect 1

# 如果需要直连数据库
setsebool -P httpd_can_network_connect_db 1

# 如需反向代理到 memcache / fastcgi 等,可按需开启:
# setsebool -P httpd_can_connect_redis 1  (注:不同版本命名可能差异,先用 semanage boolean -l 查看)
semanage boolean -l | grep httpd

5) 容器卷标(Podman)

容器共享宿主目录时加 :Z 或 :z,由 SELinux 自动打标:

# 独占标签(单容器使用)::Z
podman run -d --name img-worker -v /opt/worker-data:/data:Z my/image:latest

# 共享标签(多容器共享)::z
podman run -d --name img-api -v /shared/cache:/cache:z my/api:latest

这是我见过最容易忽略的点:没加 :Z/:z 导致容器内读写宿主目录报错,表面看像文件权限问题,实际是 context 不对。

6) 收集 AVC 证据并归因

运行 30–60 分钟真实流量后,抓取拒绝记录:

# 最近 1 小时的拒绝
ausearch -m AVC -ts recent > /root/avc.recent.log

# 可读化说明(why)
audit2allow -w -i /root/avc.recent.log | less

# 生成建议(仅用于“参考”)
audit2allow -a -M myapp_httpd_$(date +%Y%m%d)

关键提醒:audit2allow 生成的是“最小可行”还是“过度放宽”,要人眼审核。优先找 布尔值/上下文/端口 等“正途”修复,最后才落到自定义策略。

7)(如确需)最小自定义策略模块

下面是一次真实产生过的合理放行(示例):

# 查看 .te ,确认规则符合最小授权
cat myapp_httpd_202508.te
# 可能包含类似:
# allow httpd_t http_cache_port_t:tcp_socket name_connect;
# allow httpd_t var_run_t:sock_file write;

# 安装策略模块
semodule -i myapp_httpd_202508.pp

# 列出当前模块,便于日后审计/回滚
semodule -l | grep myapp_httpd

我做模块命名会带上 系统/应用/日期,比如 myapp_httpd_202508,方便半年后还能一眼知道它是干嘛的。

8) 退出按域宽松 & 灰度切换

# 恢复 httpd/container 到 Enforcing
semanage permissive -d httpd_t
semanage permissive -d container_t

# 全局确保 Enforcing
setenforce 1
getenforce

在有多台节点时,逐台移除宽松 → 观察 5–10 分钟 → 下一台。单机则择低峰操作并保持回滚手段就位。

四、典型坑点与现场“止血”手法

坑 1:Nginx 上传 403 / 13 Permission denied

症状:业务日志/访客报 403,error.log 显示写入失败。

根因:上传目录被 default_t,不是 httpd_sys_rw_content_t。

止血(当场):

chcon -R -t httpd_sys_rw_content_t /var/www/myapp/uploads   # 临时救急

根修(持久):

semange fcontext -a -t httpd_sys_rw_content_t '/var/www/myapp/uploads(/.*)?'
restorecon -Rv /var/www/myapp/uploads

坑 2:改了监听 8443,外部打不进来

根因:8443 未登记为 http_port_t,被 SELinux 拦。

修复:

semanage port -a -t http_port_t -p tcp 8443
systemctl reload nginx

坑 3:容器任务写宿主目录失败

根因:挂载没加 :Z/:z,容器中文件显示正常,写入就报错。

修复:

podman rm -f img-worker
podman run -d --name img-worker -v /opt/worker-data:/data:Z my/image:latest

坑 4:定时任务 curl 第三方失败

根因:cron 运行环境域不同,联网被拦。

建议:把脚本改为 systemd timer,让它在需要的服务域中运行;或者拆分为由应用/队列发起。

五、表格速查:你 80% 会用到的“正途”修复

1) 常见布尔值

布尔 作用 何时开启
httpd_can_network_connect 允许 httpd 联网(到后端/第三方) 反向代理/后端在非本进程
httpd_can_network_connect_db 允许 httpd 连接数据库 Web 直连 DB
httpd_read_user_content 读用户家目录内容 异常少用,尽量避免
nis_enabled 启用 NIS 兼容 老环境可能需要

查看全部:semanage boolean -l | grep httpd

2) 文件上下文(常见目录)

目录 类型建议 说明
Web 只读目录 httpd_sys_content_t 代码/静态文件
Web 可写目录(上传/缓存) httpd_sys_rw_content_t 最常见错配
Nginx/应用 Socket httpd_var_run_t /run/*.sock
Web 日志 httpd_log_t /var/log/myapp

3) 常用命令清单(收藏)

# 全局状态
sestatus; getenforce

# 端口类型
semanage port -l | grep http
semanage port -a -t http_port_t -p tcp 8443

# 文件上下文
semanage fcontext -a -t httpd_sys_rw_content_t '/var/www/myapp/uploads(/.*)?'
restorecon -Rv /var/www/myapp/uploads

# 布尔值
semanage boolean -l | grep httpd
setsebool -P httpd_can_network_connect 1

# 按域宽松
semanage permissive -a httpd_t
semanage permissive -d httpd_t

# 审计/建议
ausearch -m AVC -ts recent
audit2allow -w -a
audit2allow -a -M mymod && semodule -i mymod.pp

六、回滚与应急(演练过才算数)

场景 操作 备注
立刻放行 setenforce 0 运行时降级到 Permissive
下次重启仍放行 grubby --update-kernel ALL --args enforcing=0 仅在窗口期使用
完全禁用(应急) grubby --update-kernel ALL --args selinux=0 极端手段,不建议常态
带外救机 打开 KVM、串口 防止 ssh 登不上

我在切换前会实测:setenforce 0/1 往返一次,确认业务无感知、团队也知道怎么做。

七、合规要点与审计足迹

  • 最小授权:尽量用 布尔/端口/上下文,少写策略、写也只放行必要对象。
  • 可追溯:策略模块命名规范(含系统/应用/日期),变更单保存 .te/.pp。
  • 一致性:用 Ansible/RPM 包/脚本固化 semanage 与 restorecon 操作,避免“只修了其中一台”。
  • 可证明:保留 sestatus、semodule -l、semanage port -l、semanage boolean -l 的变更前后快照,满足稽核。

八、一个“真事故”的收尾

那次凌晨切回 Enforcing,我先撤掉 httpd_t 的宽松,盯了 8 分钟图。第 3 分钟,客服群里有人说后台图片偶发上传失败。我立刻翻 ausearch,看到一条针对 /var/www/myapp/cache 的拒绝,类型是 default_t。问题很熟悉,我当场跑了:

semanage fcontext -a -t httpd_sys_rw_content_t '/var/www/myapp/cache(/.*)?'
restorecon -Rv /var/www/myapp/cache

曲线恢复平稳。接着我把 container_t 宽松也撤了,最终 getenforce 显示 Enforcing,我们照常扛住了早高峰。天亮时,窗外集装箱码头开始忙起来。我合上笔记本,把那张回滚清单折好放回包里——真正的安全,是上线用户没有感觉,稽核同样满意。

附录:一次性脚本(可按需调整)

适合在一台新机/新节点上初始化 SELinux 与常见 Web 场景。

#!/usr/bin/env bash
set -euo pipefail

# 安装工具
dnf install -y policycoreutils policycoreutils-python-utils setroubleshoot-server setools-console container-selinux

# 端口登记
semanage port -a -t http_port_t -p tcp 8443 || true
semanage port -a -t redis_port_t -p tcp 6380 || true

# 文件上下文
semanage fcontext -a -t httpd_sys_content_t '/var/www/myapp(/.*)?'
semanage fcontext -a -t httpd_sys_rw_content_t '/var/www/myapp/uploads(/.*)?'
semanage fcontext -a -t httpd_sys_rw_content_t '/var/www/myapp/cache(/.*)?'
semanage fcontext -a -t httpd_var_run_t '/run/myapp\.sock'
semanage fcontext -a -t httpd_log_t '/var/log/myapp(/.*)?'
restorecon -Rv /var/www/myapp /run/myapp.sock /var/log/myapp || true

# 布尔值
setsebool -P httpd_can_network_connect 1
setsebool -P httpd_can_network_connect_db 1

# 可选:按域宽松以采集证据(上线前请取消)
# semange permissive -a httpd_t
# semange permissive -a container_t

如果你也要在香港的生产环境把 RHEL 9 + SELinux 拉满强度,我的建议只有两句:先审计,后放行;先官方,后自定义。把“宽松—>严格”的过程拆成小步快跑,每一步都能回退——你会发现,安全与稳定,并不矛盾。

目录结构
全文