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

我第一次在香港葵涌机房把 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 拉满强度,我的建议只有两句:先审计,后放行;先官方,后自定义。把“宽松—>严格”的过程拆成小步快跑,每一步都能回退——你会发现,安全与稳定,并不矛盾。