Linux 基础体系 · 第 8/85 篇。示例面向现代主流 Linux 发行版;发行版差异、权限和生产风险会明确说明。

Linux 主机安全加固:最小化、SELinux/AppArmor、审计与漏洞治理

Linux 主机安全加固不是“关闭几个服务、安装一个扫描器”这么简单。它要解决的是一条完整的攻击链:

  1. 攻击者如何到达主机;
  2. 到达后能调用哪些服务和程序;
  3. 服务进程能读取、修改或执行哪些对象;
  4. 异常行为能否被记录和关联;
  5. 已知漏洞能否及时修复;
  6. 加固失败或误配置后,能否验证和恢复。

本文以现代主流发行版为范围:RHEL、Fedora、Rocky Linux、AlmaLinux 通常以 SELinux 为主,Ubuntu、Debian 通常以 AppArmor 为主;具体默认策略、服务名称、包管理命令和工具版本可能不同。下文会明确区分内核机制、常见实现和运维建议。


一、先建立安全模型:最小化不是“越少越好”

1.1 攻击面、权限面和可观测性

攻击面是攻击者能够交互的入口集合,包括:

  • 监听中的网络端口;
  • 对外提供的 HTTP、SSH、数据库等服务;
  • 本地 Unix socket;
  • SUID/SGID 程序;
  • 可执行文件、脚本解释器和动态库;
  • 内核接口、设备节点和容器运行时;
  • 已安装但未使用的软件及其自动启动单元。

权限面是某个主体成功执行后可以影响的资源集合。Linux 中至少有三层相关机制:

  1. DAC(Discretionary Access Control,任意访问控制)
    传统的 UID、GID、mode、ACL。文件所有者或具有足够权限的主体可以改变权限。
  2. Capabilities
    将传统 root 的部分能力拆分,例如 CAP_NET_BIND_SERVICE 允许绑定低于 1024 的端口,CAP_DAC_OVERRIDE 可绕过部分传统文件权限检查。
  3. MAC(Mandatory Access Control,强制访问控制)
    SELinux 或 AppArmor 根据系统策略限制访问。主体自身通常不能随意放宽这类限制。

一次访问能否成功,不能只看文件的 chmod。可以用下面的抽象条件理解:

Allow=DACMACCapabilityObjectStateAllow = DAC \land MAC \land Capability \land ObjectState

其中:

  • DAC:UID/GID、mode、ACL 检查是否通过;
  • MAC:SELinux 或 AppArmor 是否允许;
  • Capability:所需内核能力是否存在;
  • ObjectState:文件系统只读、挂载选项、设备状态等是否允许。

这个公式表达的是“多个门同时通过”。例如:

  • 文件 mode 允许,但 SELinux 拒绝,访问仍失败;
  • SELinux 允许,但传统 Unix 权限拒绝,访问仍失败;
  • 进程是 root,通常可绕过部分 DAC,但不等于绕过 SELinux;
  • 进程缺少 CAP_NET_BIND_SERVICE,即使其他条件满足,也可能无法绑定低端口。

这也是为什么只执行 chmod 777 经常不能解决生产问题,反而扩大了权限面。

1.2 最小化的三个层次

最小化应分层实施,而不是只删除软件包。

第一层:最小化网络暴露

先看实际监听状态,而不是只看“安装了什么”:

sudo ss -lntup
sudo ss -lx

典型输出可能是:

LISTEN 0 128 0.0.0.0:22   0.0.0.0:* users:(("sshd",pid=842,fd=3))
LISTEN 0 4096 127.0.0.1:6379 0.0.0.0:* users:(("redis-server",pid=910,fd=6))

这里 0.0.0.0:22 表示监听所有 IPv4 地址,127.0.0.1:6379 只接受本机连接。两者的风险不同:即使 Redis 本身没有认证漏洞,错误地监听公网也会扩大攻击面。

随后确认防火墙实际规则。现代发行版可能使用 nftables、firewalld 或 ufw,不能只根据某个命令的输出推断内核最终规则:

sudo nft list ruleset

若使用 firewalld:

sudo firewall-cmd --get-active-zones
sudo firewall-cmd --list-all

防火墙通常只能限制网络路径,不能替代服务自身认证和 MAC。例如允许 443 端口并不代表 Web 应用安全;防火墙也不能阻止同机恶意进程访问一个没有正确权限保护的 Unix socket。

第二层:最小化服务和软件包

查看 systemd 单元:

systemctl list-unit-files --type=service --state=enabled
systemctl --type=service --state=running

发现不需要的服务后,先确认依赖和用途:

systemctl status example.service
systemctl list-dependencies --reverse example.service

停止、禁用和屏蔽含义不同:

sudo systemctl stop example.service
sudo systemctl disable example.service
sudo systemctl mask example.service
  • stop:停止当前实例,重启后可能再次启动;
  • disable:取消开机自动启动,但仍可能被手动或依赖拉起;
  • mask:把单元链接到 /dev/null,阻止常规启动请求。

生产环境不要把 mask 当作普通禁用手段。错误屏蔽可能破坏升级、监控或依赖服务。应先记录原状态,验证业务,再决定是否持久化。

包级别也要确认反向依赖:

在 RPM 系发行版中:

rpm -qf /usr/sbin/example
rpm -q --whatrequires example-package

在 Debian 系发行版中:

dpkg -S /usr/sbin/example
apt-cache rdepends example-package

删除一个软件包可能连带删除配置、库或元包。对于生产主机,更稳妥的步骤是:

  1. 记录包清单和服务状态;
  2. 在同版本测试环境验证依赖;
  3. 先停止服务并观察业务;
  4. 删除或卸载;
  5. 重新扫描监听端口、启动单元和应用健康状态;
  6. 保留可回滚的包版本或镜像。

第三层:最小化进程权限

即使服务必须存在,也不应默认以 root 运行。systemd 服务可通过以下方向减少权限:

[Service]
User=app
Group=app
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=full
ProtectHome=read-only

这些选项不是所有程序都兼容:

  • UserGroup 改变进程身份;
  • NoNewPrivileges=yes 阻止进程及其子进程通过 execve 获得新的特权,例如 SUID 提权;
  • PrivateTmp=yes 提供独立的临时目录视图;
  • ProtectSystem=full 将部分系统目录以只读方式暴露;
  • ProtectHome=read-only 限制对用户家目录的写入。

不要直接编辑发行版提供的 unit 文件,通常应使用 drop-in:

sudo systemctl edit example.service

保存后:

sudo systemctl daemon-reload
sudo systemctl restart example.service
sudo systemctl status example.service

验证不能只看服务是否“active”。还要检查:

ps -o pid,user,group,comm -C example
sudo systemctl show example.service \
  -p User -p Group -p NoNewPrivileges -p ProtectSystem -p ProtectHome

如果服务启动失败,先查看:

journalctl -u example.service -b --no-pager

进程沙箱和 SELinux/AppArmor 是不同层次:systemd 约束进程的命名空间、挂载视图和特权;MAC 约束主体对对象的安全访问。二者可以叠加,但也可能因路径、临时目录、动态生成文件等差异造成兼容问题。


二、SELinux:基于标签和策略的强制访问控制

2.1 SELinux 的基本对象模型

SELinux 将访问描述为:

(subject, object, class, permission)(subject,\ object,\ class,\ permission)

例如 Web 服务读取网页文件可以表示为:

subject: system_u:system_r:httpd_t:s0
object:  system_u:object_r:httpd_sys_content_t:s0
class:   file
permission: read

其中:

  • subject 是进程,通常具有 SELinux domain,例如 httpd_t
  • object 是文件、目录、端口、socket 等资源;
  • class 是对象类别,如 filedirtcp_socket
  • permission 是具体操作,如 readwriteexecute

SELinux 规则不是简单的“某个用户能否访问某路径”。路径只是查找对象的方式,策略主要依据对象的安全上下文和进程 domain 判断。

查看当前状态:

getenforce
sestatus

常见状态:

  • Enforcing:策略生效,拒绝访问并记录相关审计事件;
  • Permissive:不实际阻断访问,但记录本应拒绝的事件;
  • Disabled:内核不加载 SELinux。切换到启用状态通常涉及重新标记文件系统,不能当作普通运行时开关。

Permissive 适合诊断和迁移,不应被误认为“安全但只记录”。因为此时被策略拒绝的操作仍然成功,应用可能已经以不受约束的方式运行。

2.2 DAC 与 SELinux 的访问顺序

假设 Web 进程读取 /srv/www/index.html

  1. 内核执行路径解析;
  2. 检查目录和文件的 DAC 权限、ACL;
  3. 检查 SELinux domain httpd_t 是否有权读取对象标签;
  4. 检查文件系统、挂载和其他内核约束;
  5. 允许或拒绝;
  6. 若涉及 SELinux 拒绝,写入 AVC 审计事件。

因此下面两种情况都可能失败:

-rw-r--r-- 1 root root ... /srv/www/index.html

如果 Web 进程以非 root 身份运行,DAC 可能允许读取;但如果文件标签是普通用户文件类型,SELinux 仍可能拒绝。

反过来,即使标签正确:

-rw------- 1 root root ... /srv/www/index.html

SELinux 允许也不能替代 DAC,Web 进程仍可能因传统权限不足而失败。

2.3 文件上下文:临时修改与持久配置

查看上下文:

ls -lZ /srv/www/index.html
ps -eZ | grep httpd

常见 Web 内容类型:

  • httpd_sys_content_t:允许 Web 服务读取;
  • httpd_sys_rw_content_t:允许特定 Web 服务写入,但扩大写权限,不能随意使用。

临时修改:

sudo chcon -t httpd_sys_content_t /srv/www/index.html

chcon 直接改变当前标签,但不一定被文件上下文数据库记住。文件重新创建、恢复标签或某些 relabel 操作后可能丢失,因此它适合实验,不适合作为持久配置。

持久配置:

sudo semanage fcontext -a -t httpd_sys_content_t '/srv/www(/.*)?'
sudo restorecon -Rv /srv/www

这里分两步:

  1. semanage fcontext 把正则路径与目标类型写入持久映射;
  2. restorecon 根据映射实际设置文件标签。

验证:

matchpathcon /srv/www/index.html
ls -lZ /srv/www/index.html

matchpathcon 显示策略期望的上下文,ls -Z 显示当前实际上下文。两者不一致通常意味着没有执行 restorecon,或者存在更具体的规则覆盖。

2.4 端口标签和布尔开关

SELinux 不只标记文件,也可以标记网络端口。服务尝试监听非默认端口时,不能只修改防火墙:

sudo semanage port -l | grep http_port_t
sudo semanage port -a -t http_port_t -p tcp 8080

如果端口已经存在于其他类型,应使用 -m 修改,而不是 -a

sudo semanage port -m -t http_port_t -p tcp 8080

具体是否允许某服务绑定该类型,还取决于策略。

布尔值是发行版策略暴露的可配置开关。例如:

getsebool -a | grep httpd
sudo setsebool -P httpd_can_network_connect on
  • 不带 -P:只改变当前运行状态;
  • -P:更新持久策略,可能需要较长时间并修改策略存储。

开启布尔值前要确认它放宽了什么。httpd_can_network_connect 可能允许 Web 服务主动连接更多网络目标;如果应用只需连接数据库,最好再通过网络分段、防火墙和应用配置限制目标,而不是把“所有网络连接能力”视为无成本开关。

2.5 AVC 拒绝的诊断流程

看到应用报“Permission denied”时,不要直接执行 audit2allow。完整流程如下:

第一步:确认是否为 SELinux 拒绝

sudo ausearch -m AVC -ts recent

或:

sudo journalctl -k -g 'avc:.*denied'

典型事件会包含:

avc:  denied  { read } for  pid=1234 comm="httpd"
path="/srv/www/index.html"
scontext=system_u:system_r:httpd_t:s0
tcontext=unconfined_u:object_r:default_t:s0
tclass=file

这里真正有用的信息是:

  • scontext:发起访问的进程 domain;
  • tcontext:目标对象标签;
  • tclass:对象类别;
  • { read }:被拒绝的权限;
  • path:辅助定位路径。

第二步:检查 DAC 和实际标签

namei -l /srv/www/index.html
ls -ldZ /srv /srv/www
ls -lZ /srv/www/index.html

namei -l 能逐级展开路径权限。很多问题并不是文件本身,而是父目录缺少执行权限,或目录标签错误。

第三步:判断是配置错误还是确实需要扩权

如果文件应当是 Web 静态内容,通常应修正标签:

sudo semanage fcontext -a -t httpd_sys_content_t '/srv/www(/.*)?'
sudo restorecon -Rv /srv/www

如果应用需要写入上传目录,应只给专用目录配置可写类型,而不是把整个应用树标记为可写。

第四步:验证最小修复

sudo systemctl restart httpd
curl -f http://127.0.0.1/
sudo ausearch -m AVC -ts recent

如果业务成功且没有新的相关拒绝,才说明修复路径正确。

audit2why 可以解释拒绝原因:

sudo ausearch -m AVC -ts recent | audit2why

audit2allow 可以根据拒绝事件生成本地策略,但它只回答“如何允许曾经发生的访问”,不回答“这个访问是否应该发生”。直接安装其生成的规则,可能把入侵行为永久加入白名单。只有确认访问是业务必需、路径和主体范围已收敛时,才应审查后使用本地策略包。

2.6 SELinux 的常见故障边界

  • 文件从临时目录复制到目标目录后标签可能不符合目标路径;
  • 应用通过符号链接访问时,必须同时检查实际目标和父目录;
  • 容器挂载主机目录时,容器运行时可能需要 :Z:z 等标签处理方式,具体取决于 Podman/Docker 和发行版集成;
  • 仅修改 chmod 无法修复 SELinux 类型错误;
  • 关闭 SELinux 可能让问题暂时消失,但同时移除了一个独立防线,也会改变后续排障条件。

三、AppArmor:基于程序路径的强制访问控制

3.1 AppArmor 的模型

AppArmor 主要把程序路径作为 profile 关联依据,并在 profile 中声明该程序可以访问哪些路径、网络能力和 Linux 能力。

一个简化 profile:

#include <tunables/global>

 /usr/local/bin/report-worker {
     #include <abstractions/base>

     /etc/report-worker/config.yml r,
     /var/lib/report-worker/** rw,
     /var/log/report-worker/** w,

     network inet stream,
     capability net_bind_service,
 }

语义要点:

  • profile 通常绑定 /usr/local/bin/report-worker
  • r 表示读取,w 表示写入;
  • /var/lib/report-worker/** 覆盖目录下的层级路径;
  • network inet stream 允许 IPv4/IPv6 流式网络;
  • capability net_bind_service 允许使用对应 Linux capability;
  • 未声明的访问默认拒绝或记录,取决于 profile 当前模式。

AppArmor 与 SELinux 的关键差异不是“一个更安全”,而是策略表达方式不同:

方面 SELinux AppArmor
主要关联方式 进程 domain 与对象安全标签 程序路径与路径规则
文件迁移处理 标签可随对象继承或恢复 规则通常直接匹配访问路径
策略表达 类型、domain、class、permission 路径、能力、网络、规则
常见默认发行版 RHEL、Fedora Ubuntu、部分 Debian
诊断事件 AVC DENIED,常见于内核日志和 audit 日志

两者都属于 MAC,通常不应在同一主机上把它们当成两个独立 profile 系统叠加管理;应以发行版默认安全框架为主。

3.2 查看、加载和切换 profile

查看状态:

sudo aa-status

查看已加载 profile:

sudo apparmor_status

工具名称和安装包在发行版之间可能不同。

常见 profile 操作:

sudo aa-complain /etc/apparmor.d/usr.local.bin.report-worker
sudo aa-enforce /etc/apparmor.d/usr.local.bin.report-worker
sudo apparmor_parser -r /etc/apparmor.d/usr.local.bin.report-worker
  • complain:记录违规但通常不阻断;
  • enforce:记录并阻断;
  • apparmor_parser -r:重新加载 profile。

从 complain 切换到 enforce 前,要覆盖真实业务路径:启动、读取配置、写日志、读证书、访问 socket、连接数据库、处理异常和升级。只测试“服务能启动”不足以发现运行中路径。

3.3 AppArmor 拒绝的诊断

先查看内核日志或 audit 记录:

sudo journalctl -k -g 'apparmor="DENIED"'
sudo ausearch -m APPARMOR -ts recent

典型事件会显示:

apparmor="DENIED" operation="open"
profile="/usr/local/bin/report-worker"
name="/etc/report-worker/config.yml"
requested_mask="r"
denied_mask="r"

诊断时要区分:

  • profile:哪个程序 profile 拒绝;
  • name:实际访问路径;
  • requested_mask:程序请求的操作;
  • denied_mask:被拒绝的操作;
  • operation:open、exec、capable、network 等。

如果配置文件路径确实是业务需要,可在 profile 中加入最小规则并重新加载。不要把整个目录写成:

/** rw,

这种写法会让 profile 失去大部分约束价值。即使使用 aa-logprof 自动生成规则,也应逐条审查,因为自动工具只能根据观察到的行为推断“曾经发生过”,不能判断“是否应当允许”。

3.4 路径模型的边界

AppArmor 的路径模型带来几个实际边界:

  • 程序通过不同路径、硬链接或解释器启动时,可能命中不同 profile;
  • 临时文件名、插件目录和动态生成路径容易导致拒绝;
  • 规则写得过宽会扩大访问面,写得过窄则在特定功能路径失败;
  • profile 只保护已被 profile 约束的程序。一个未受约束的辅助程序仍可能执行同样的危险操作;
  • 即使 AppArmor 允许访问,DAC、capabilities、只读挂载和 seccomp 等其他机制仍可能拒绝。

因此,看到 AppArmor 拒绝时同样要检查传统权限:

namei -l /etc/report-worker/config.yml
ls -l /etc/report-worker/config.yml
ps -o pid,user,group,comm -C report-worker

四、SELinux 与 AppArmor 的选择、迁移和故障处理

4.1 选择原则

优先使用发行版默认和已集成的 MAC 框架:

  • RHEL 系统通常已有大量 SELinux policy、文档和运维工具;
  • Ubuntu 系统通常已有 AppArmor profile 和 systemd 集成;
  • 强行切换框架不仅是安装软件,还涉及启动参数、文件标签、策略覆盖和已有运维流程。

安全能力最终取决于:

EffectiveSecurity=Mechanism×PolicyQuality×CoverageEffectiveSecurity = Mechanism \times PolicyQuality \times Coverage

其中任一项接近零,整体效果都会显著下降。安装了 SELinux 但处于 disabled,或安装了 AppArmor 但关键服务没有 profile,都不能称为有效控制。

4.2 从宽松状态进入强制状态

迁移的正确顺序是:

  1. 盘点关键服务和文件路径;
  2. 在测试环境启用或加载策略;
  3. 先使用 permissive/complain 收集合法业务访问;
  4. 检查拒绝事件和异常扩权;
  5. 修正标签、profile 和应用配置;
  6. 在维护窗口切换 enforcing/enforce;
  7. 进行业务验证和回滚演练。

不要把“日志里没有拒绝”直接理解为“策略完整”。如果测试流量没有覆盖备份、故障切换、证书轮换、日志切割和升级流程,策略可能只是在未测试路径上保持沉默。

4.3 强制模式下服务突然失败的恢复路径

当切换强制模式后服务失败:

  1. 先判断是应用错误、DAC 错误还是 MAC 拒绝;
  2. 保留拒绝事件,不要先清空日志;
  3. 恢复到已知安全的 permissive/complain 或回滚最近策略;
  4. 修正具体标签或规则;
  5. 在测试流量下再次验证;
  6. 重新进入强制模式。

SELinux 可临时执行:

sudo setenforce 0
# 只用于诊断或紧急恢复
sudo setenforce 1

这不是长期修复。AppArmor 则应针对具体 profile 使用 complain,而不是关闭整个 AppArmor 服务。


五、审计:记录“谁在什么时候做了什么”

5.1 审计与普通日志不是一回事

普通应用日志回答:

用户 alice 登录失败

内核审计更关注:

哪个进程、以什么 UID、对哪个 inode 或路径、执行了什么系统调用、结果如何

审计事件通常由内核 audit 子系统生成,再由 auditd 写入日志。它适合记录安全相关行为和调查证据,但不是万能的业务审计系统:

  • 它不能自动理解业务语义;
  • 事件量过大可能影响存储和分析;
  • 容器、用户命名空间和多层日志转发会增加归属判断难度;
  • 关闭或篡改审计的攻击者可能破坏本机证据,因此日志应尽量远程转发。

查看服务:

sudo systemctl status auditd
sudo auditctl -s

auditctl -s 可看到启用状态、丢失事件数量等信息。重点关注 lost;如果大于零,说明已有事件丢失,不能把当前日志当作完整证据。

5.2 规则的粒度与成本

审计规则可以按系统调用、路径、用户 ID、架构等条件过滤。比如记录 root 身份执行程序:

sudo auditctl -a always,exit -F arch=b64 \
  -S execve -F euid=0 -k root-exec

查询:

sudo ausearch -k root-exec -i

这里:

  • arch=b64 表示 64 位系统调用;
  • execve 记录程序执行;
  • euid=0 过滤有效 UID 为 root;
  • -k root-exec 添加查询标签;
  • -i 将 UID、syscall 等数值转换成人类可读形式。

在支持 32 位程序的 64 位系统上,还需评估是否增加 arch=b32 规则。规则越宽,事件越多;不能只复制网上规则而不测量。

针对关键文件的系统调用审计示例:

sudo auditctl -w /etc/ssh/sshd_config -p wa -k sshd-config

含义:

  • -w 监控路径;
  • -p wa 关注写入和属性变化;
  • -k 添加查询关键字。

某些新版本文档更推荐使用基于 syscall 的规则,而不是大量 path watch;具体语法和性能取舍取决于内核与 audit 工具版本。生产中应把规则写入 /etc/audit/rules.d/,由发行版提供的加载机制持久化,而不是只执行一次 auditctl

例如:

sudo tee /etc/audit/rules.d/50-security.rules >/dev/null <<'EOF'
-a always,exit -F arch=b64 -S execve -F euid=0 -k root-exec
-w /etc/ssh/sshd_config -p wa -k sshd-config
EOF

sudo augenrules --load
sudo auditctl -l

augenrules 是否存在以及加载方式受发行版影响,应先确认:

command -v augenrules

5.3 审计规则的持久性和不可变模式

审计规则本身也是安全配置。可以在规则末尾使用:

-e 2

这会使规则不可变,修改通常需要重启。它能防止运行中的进程轻易清空或改写规则,但也会增加误配置恢复成本。实践中应:

  1. 在测试机验证完整规则;
  2. 确认磁盘空间、日志轮转和远程转发;
  3. 再启用不可变模式;
  4. 准备带外控制台或重启恢复路径。

auditd 日志路径通常为:

sudo ls -lh /var/log/audit/

检查轮转和空间策略,避免磁盘写满后影响系统。不同 disk_full_actionspace_left_action 配置可能导致记录、暂停、单用户模式或关机等行为,生产取值必须结合可用性要求审查,不能盲目追求“日志绝不丢失”。

5.4 读取审计事件

常用查询:

sudo ausearch -m USER_LOGIN -ts today -i
sudo ausearch -m AVC -ts today -i
sudo ausearch -k sshd-config -i
sudo aureport --auth --success
sudo aureport --auth --failed

审计事件可能由多个记录组成,例如一次系统调用关联 SYSCALLPATHCWDPROCTITLE 等记录。不要只解析单行;应使用 ausearch 或集中式解析器按事件 ID 关联。

对高价值行为建议重点关注:

  • SSH 登录成功和失败;
  • root 或高权限账户执行;
  • 身份、sudo、SSH 配置变更;
  • 关键系统文件修改;
  • 服务启停和 unit 文件修改;
  • SELinux AVC 或 AppArmor DENIED;
  • 新建用户、修改组和授权文件;
  • 防火墙规则变更。

审计记录“发生了什么”,检测系统还要回答“是否异常”。例如凌晨一次合法自动化部署修改了 sshd 配置,单看事件无法判定恶意;需要结合变更单、执行主体、来源 IP、命令行和时间窗口。


六、漏洞治理:从清单到验证的闭环

6.1 漏洞、补丁和风险不是同一个概念

CVE 是漏洞标识,不是风险分数,也不保证所有受影响系统都可被利用。实际风险至少取决于:

Risk=Exposure×Exploitability×Impact×UncertaintyRisk = Exposure \times Exploitability \times Impact \times Uncertainty

  • Exposure:服务是否可达、是否暴露公网;
  • Exploitability:是否需要认证、特殊配置或本地访问;
  • Impact:权限提升、数据泄露、远程执行还是拒绝服务;
  • Uncertainty:资产版本、补丁回移和实际配置是否已确认。

发行版可能把上游修复回移到旧版本,因此不能只根据上游软件的版本号判断是否有漏洞。应查看发行版安全公告和包 changelog。

6.2 资产清单必须包含“运行状态”

仅记录“安装了 openssl 3.x”不够,还应记录:

  • 主机、镜像或虚拟机身份;
  • 发行版和支持生命周期;
  • 已安装包及版本;
  • 实际运行进程和监听端口;
  • 服务是否暴露公网;
  • 配置变更和例外;
  • 最近补丁时间;
  • 内核和需要重启的用户态服务;
  • 是否存在容器内重复的脆弱依赖。

包清单示例:

# Debian/Ubuntu
dpkg-query -W -f='${binary:Package}\t${Version}\n' > packages-debian.txt

# RPM 系
rpm -qa --qf '%{NAME}\t%{VERSION}-%{RELEASE}\n' > packages-rpm.txt

运行态清单:

uname -a
cat /etc/os-release
ps -eo user,pid,ppid,comm,args --sort=user
ss -lntup
systemctl list-units --type=service --state=running

这些输出应进入资产管理或配置管理系统,而不是只保存在管理员笔记本上。

6.3 更新、重启和验证

Debian/Ubuntu:

sudo apt update
apt list --upgradable
sudo apt upgrade

RHEL/Fedora:

sudo dnf check-update
sudo dnf updateinfo list updates security
sudo dnf upgrade

check-update 返回非零状态有时只表示存在可更新包,不应简单当作命令失败;自动化脚本需要按工具文档处理返回码。

更新后至少验证:

systemctl --failed
sudo ss -lntup
sudo journalctl -p warning -b --no-pager

内核更新通常需要重启才能使用新内核。用户态库更新则可能需要重启仍在使用旧库的进程。可用发行版工具辅助判断,例如某些 RPM 系发行版提供 needs-restarting,但工具包和判断能力有版本差异,不能把命令不存在解释为“无需重启”。

重启前要确认:

  • SSH 密钥或带外控制台可用;
  • 防火墙、网络和时间同步配置已持久化;
  • 数据库和文件系统有正确的停止流程;
  • 负载均衡和集群有迁移路径;
  • 回滚镜像、旧内核或控制台恢复方案可用。

重启后验证的不只是 uptime

uname -r
getenforce 2>/dev/null || true
sudo aa-status 2>/dev/null || true
systemctl --failed
sudo ss -lntup

6.4 扫描器的边界

漏洞扫描器常见三类误差:

  1. 误报:只按版本匹配,没有考虑发行版回移补丁;
  2. 漏报:无法识别自编译软件、静态链接组件、容器内依赖或运行时下载的模块;
  3. 配置盲区:知道包版本,却不知道服务是否启用、端口是否暴露、认证是否开启。

因此治理闭环应是:

flowchart LR
    A[资产发现] --> B[软件与配置清单]
    B --> C[公告与漏洞匹配]
    C --> D[按暴露面和影响排序]
    D --> E[测试补丁与配置修复]
    E --> F[生产发布]
    F --> G[服务、端口、日志验证]
    G --> H[重新扫描与关闭工单]
    H --> B

关键路径是:扫描结果不能直接等同于修复结果。修复后需要确认实际运行进程已加载新库、服务已恢复预期安全配置,且没有因为修复引入新的监听端口或策略错误。

6.5 例外治理

无法立即修复时,例外必须记录:

  • 受影响资产和软件版本;
  • CVE 或公告编号;
  • 暴露路径和业务影响;
  • 临时缓解措施;
  • 责任人和到期时间;
  • 验证方式;
  • 下一次复审条件。

临时缓解可以包括:

  • 防火墙限制来源;
  • 关闭非必要功能;
  • 降低服务权限;
  • 使用 SELinux/AppArmor 限制文件和网络访问;
  • 将服务迁移到隔离网络;
  • 增加监控和入侵检测。

但缓解措施不应被写成“已修复漏洞”。例如防火墙阻断公网只能降低远程利用暴露面,不能修复本地提权漏洞。


七、把最小化、MAC、审计和漏洞治理连成一条数据流

一个 Web 服务请求的安全路径可以抽象为:

sequenceDiagram
    participant C as 客户端
    participant FW as 防火墙/nftables
    participant S as Web 服务
    participant DAC as DAC/ACL
    participant MAC as SELinux/AppArmor
    participant FS as 文件系统
    participant AU as auditd/日志
    C->>FW: TCP 连接
    FW-->>C: 允许或丢弃
    C->>S: HTTP 请求
    S->>DAC: 请求读取/写入对象
    DAC-->>S: 允许或拒绝
    S->>MAC: 进行 MAC 检查
    MAC-->>S: 允许或拒绝
    S->>FS: 执行实际 I/O
    MAC-->>AU: 记录拒绝事件
    DAC-->>AU: 记录部分安全事件
    S-->>C: 响应或错误

这条路径说明了几个常见误判:

  • 端口开放只代表网络层到达,不代表应用访问文件一定成功;
  • 文件权限正确不代表 MAC 一定允许;
  • MAC 允许不代表应用身份有足够 DAC 权限;
  • 日志中出现拒绝不代表攻击,也可能是配置错误;
  • 没有日志不代表没有攻击,可能是审计规则缺失、事件丢失或攻击发生在未覆盖组件中。

八、生产环境的验证、回滚与证据保留

8.1 加固前后进行可比较验证

加固前记录基线:

sudo ss -lntup > before-ports.txt
systemctl list-unit-files --type=service --state=enabled > before-enabled.txt
sudo getenforce > before-selinux.txt 2>/dev/null || true
sudo auditctl -l > before-audit-rules.txt

变更后生成同类输出并比较:

sudo ss -lntup > after-ports.txt
systemctl list-unit-files --type=service --state=enabled > after-enabled.txt
sudo diff -u before-ports.txt after-ports.txt
sudo diff -u before-enabled.txt after-enabled.txt

基线不是越严格越好,而是要能解释每个差异:哪个端口关闭了,哪个服务被禁用,哪个新端口因业务发布出现,哪些差异经过批准。

8.2 失败路径要提前设计

典型失败包括:

  • 误禁用网络管理或远程管理服务;
  • SSH 配置语法错误导致无法登录;
  • SELinux 标签错误导致服务无法读取配置;
  • AppArmor enforce 后阻断证书、日志或插件访问;
  • 审计规则过宽导致磁盘快速耗尽;
  • 更新后新内核无法启动;
  • systemd 沙箱使服务无法创建必要目录。

修改 SSH 配置后先检查语法:

sudo sshd -t

确认无输出且退出码为 0,再重载:

sudo systemctl reload sshd

Debian/Ubuntu 的服务单元可能名为 ssh,RHEL 系通常为 sshd,应先用:

systemctl list-unit-files | grep -E '^(ssh|sshd)\.'

远程主机变更前,保持现有会话不关闭,并用第二个会话验证新连接。对于 SELinux、AppArmor 和审计规则,准备带外控制台比准备一条“关闭安全功能”的命令更可靠。

8.3 证据的完整性与时间

审计调查依赖准确时间和完整事件链:

timedatectl status

应确保时区、NTP 和日志时间一致。日志最好转发到权限隔离的集中系统,避免攻击者取得主机 root 后同时修改本地应用日志和审计日志。远程日志本身也要考虑:

  • TLS 或受保护的传输;
  • 接收端访问控制;
  • 时间同步;
  • 保留周期;
  • 丢包和断链告警;
  • 敏感数据脱敏。

集中日志不能阻止攻击,只能提高事后调查和检测能力。


九、常见误解与边界

误解一:root 可以访问一切

root 通常能绕过大量 DAC 检查,但 capability、SELinux/AppArmor、只读挂载、namespace、seccomp 和设备权限仍可能限制它。更准确的说法是:root 的传统 DAC 权限很强,不等于拥有所有内核安全能力。

误解二:chmod 777 能解决服务访问失败

它只放宽 DAC,而且没有解决:

  • 父目录搜索权限;
  • ACL;
  • SELinux 标签;
  • AppArmor profile;
  • 文件系统只读;
  • 应用自身拒绝;
  • 进程缺少 capability。

它还可能把敏感文件暴露给同机任意用户。

误解三:audit2allowaa-logprof 生成的规则可以直接安装

自动工具根据已观察到的拒绝事件生成“允许规则”。它们不了解业务意图、攻击阶段和最小权限边界。正确流程是确认主体、对象、操作和业务必要性,再收窄路径和权限。

误解四:没有 AVC 或 DENIED 就说明没有安全问题

可能是:

  • 相关服务没有被 MAC 约束;
  • 审计规则没有覆盖;
  • 日志被轮转或丢失;
  • 测试流量没有经过危险路径;
  • 问题发生在应用层或网络层,而不是 MAC 层。

误解五:漏洞扫描显示“无漏洞”就代表主机安全

扫描通常不能证明没有零日漏洞、错误配置、弱凭据、业务逻辑缺陷或已被入侵。漏洞治理解决的是已知缺陷风险,不是全部安全风险。

误解六:关闭安全框架可以作为永久兼容方案

关闭 SELinux 或 AppArmor 会减少排障表面上的错误,但同时删除独立隔离层。正确做法是查明实际访问需求,修正标签、profile、服务用户、目录权限或应用配置,并在强制模式下重新验证。


十、一个可执行的加固顺序

对一台已有业务的 Linux 主机,可以按以下顺序实施,每一步都保留输出和回滚信息:

  1. 资产和基线:记录发行版、内核、包、服务、监听端口、账户、MAC 状态和审计状态;
  2. 网络最小化:关闭不需要的监听,使用 nftables/firewalld 限制来源和目标;
  3. 服务最小化:停止并禁用未使用服务,确认反向依赖后再卸载软件包;
  4. 身份最小化:为服务设置专用 UID/GID,避免无必要的 root 运行;
  5. 文件最小化:使用正确的 mode、ACL、目录布局和 capabilities,而不是全局放权;
  6. 启用或收紧 MAC:SELinux 使用正确文件类型、端口类型和布尔值;AppArmor 使用最小路径规则;
  7. 建立审计:优先记录身份变化、特权执行、关键配置修改和 MAC 拒绝,监控事件丢失与磁盘空间;
  8. 补丁治理:建立包和运行态资产清单,按暴露面和影响排序更新;
  9. 验证与回滚:检查服务、端口、日志、策略命中和业务关键路径;
  10. 持续复审:新服务、新包、新例外和新漏洞都回到同一闭环中。

安全加固的核心不是某一个命令,而是让攻击面、权限面、强制策略、审计证据和补丁状态彼此对应:不必要的入口被移除,必要的服务被限制,异常访问有证据,已知缺陷有期限明确的治理过程,并且每次变更都能验证和恢复。


系列导航与关联阅读

官方资料

本文依据 Linux 内核、systemd 与主流发行版官方文档重新梳理;正文与实验由 WR BLOG 编写。