-今日
-本周
-本月
-累计
项目工作台运维 个人项目与实践记录

故障复盘 #005:安全加固被发行版预设静默覆盖——两次无效修复

服务中断

日期2026-09-13
环境Alibaba Cloud Linux 3.2104 / kernel 5.10
组件`sysctl` 内核参数加固 + 死人开关保护
影响1 项加固未生效(静默);2 次修复尝试无效;无服务中断
发现方式逐项验证生效值(而非只看命令输出)
严重级别中(加固静默失效,但无实际损失)
处理耗时约 25 分钟
项目内容
日期2026-09-13
环境Alibaba Cloud Linux 3.2104 / kernel 5.10
组件sysctl 内核参数加固 + 死人开关保护
影响1 项加固未生效(静默);2 次修复尝试无效;无服务中断
发现方式逐项验证生效值(而非只看命令输出)
严重级别中(加固静默失效,但无实际损失)
处理耗时约 25 分钟

1. 背景

安全审计发现 5 项内核参数未按安全基线配置:

⚠️ kernel.dmesg_restrict = 0              期望 1
⚠️ net.ipv4.conf.all.log_martians = 0     期望 1
⚠️ net.ipv4.conf.all.send_redirects = 1   期望 0
⚠️ net.ipv4.conf.all.rp_filter = 0        期望 1
⚠️ journald 未设 SystemMaxUse             期望有限制

我决定加固,但其中 rp_filter 涉及入站包过滤行为,理论上存在 "配错导致 SSH 失联"的风险。而这台机器只有一个带外通道(控制台 Workbench), 失联代价很高。

于是我用了死人开关(dead-man switch)保护。


2. 死人开关:远程做危险变更的标准做法

1. 先把"恢复脚本"用 at 排程,5 分钟后自动执行
2. 再应用新配置
3. 从外部验证 SSH 是否仍可连接
4. 通了 → 撤销 at 作业(拆掉炸弹)
   不通 → 什么都不做,5 分钟后自动恢复

恢复脚本会:还原参数 → 移出加固配置 → 记录日志。

这个机制工作得很好:我应用配置后立刻发起新 SSH 连接验证, 通了,然后在 4 分钟内拆掉了 at 作业。

当前 at 队列:  1  Sun Sep 13 15:26:00 2026 a root
已删除作业 1
删除后队列: ✅ 空,死人开关已拆除
确认 deadman 未被执行: ✅ 无日志,未触发

核心原则:每一个不可逆动作之前,先放一个自动回滚。 没有这个机制,我根本不敢在只有一个带外通道的机器上碰网络参数。


3. 问题:rp_filter 加固静默失效

应用配置后逐项检查生效值:

kernel.dmesg_restrict                = 1  ✅ 生效
net.ipv4.conf.all.log_martians       = 1  ✅ 生效
net.ipv4.conf.all.send_redirects     = 0  ✅ 生效
net.ipv4.conf.all.rp_filter          = 0  ❌ 我要设 2,读回 0

配置写了,命令没报错,但值没变。

第一次修复尝试:改文件编号(无效)

我的分析是:sysctl 按文件名字典序加载,数字大的后加载、会覆盖。 实际加载顺序里有:

50-aliyun.conf            → rp_filter = 0
60-ops-lab-hardening.conf → rp_filter = 2  ← 我的
99-sysctl.conf            → rp_filter = 0  ← 覆盖回去了

于是我把文件改名成 99-zz-ops-lab-hardening.conf,指望它排在 99-sysctl.conf 之后。

仍然无效。

真相:/etc/sysctl.conf 永远最后加载

带行号的完整加载日志揭示了顺序:

 1: Applying /usr/lib/sysctl.d/10-default-yama-scope.conf
 2: Applying /etc/sysctl.d/50-aliyun.conf
 3:   net.ipv4.conf.all.rp_filter = 0
...
32:   net.ipv4.conf.all.rp_filter = 1     ← /usr/lib/sysctl.d/50-default.conf
...
42: Applying /etc/sysctl.d/99-sysctl.conf
46:   net.ipv4.conf.all.rp_filter = 0
...
55: Applying /etc/sysctl.d/99-zz-ops-lab-hardening.conf
59:   net.ipv4.conf.all.rp_filter = 2     ← 我的,确实排到了 99-sysctl.conf 之后
...
63: Applying /etc/sysctl.conf              ← ★ 在这里,永远最后
67:   net.ipv4.conf.all.rp_filter = 0     ← 最终值被设回 0

/etc/sysctl.conf 不受 sysctl.d 编号体系约束,它在所有 sysctl.d 文件之后单独加载。

/etc/sysctl.d/99-sysctl.conf 只是个指向 /etc/sysctl.conf 的副本/链接, 真正的"最后一张牌"是 /etc/sysctl.conf 本身。

所以调整 sysctl.d 里的文件名编号永远无法覆盖 /etc/sysctl.conf 的设置。 我的两次修复尝试(60-99-zz-都是建立在这个错误认知上的

4. 第二个隐藏陷阱:接口级 max() 语义

即使 conf/all 设成功,也不一定生效。检查发现:

net.ipv4.conf.all.rp_filter      = 0
net.ipv4.conf.default.rp_filter  = 0
net.ipv4.conf.eth0.rp_filter     = 0
net.ipv4.conf.lo.rp_filter       = 0
net.ipv4.conf.docker0.rp_filter  = 0

内核实际使用的是 max(conf/all, conf/<interface>)

只设 conf/all = 2conf/eth0 仍是 0 时,内核看到的是 max(2, 0) = 2, 表面上是生效的。但如果反过来只设接口不设 all,就会被 all 压住。 这个 max() 语义是 rp_filter 最容易配错的地方。


5. 决策:不强行修改,记录为"经评估接受的偏差"

面对 /etc/sysctl.conf 的覆盖,有两个选择:

方案优点缺点
A. 直接编辑 /etc/sysctl.conf能生效修改发行版文件,yum 更新可能被覆盖;阿里云预设此值可能有其考虑
B. 接受现状,评估后记录偏差不动发行版配置该项加固缺失

我选择 B,理由是:

  1. rp_filter=0 的风险是 IP 源地址欺骗
  2. 本机是单网卡 Web 服务器,不承担路由/转发角色

(虽然 Docker 开了 ip_forward,但容器流量走独立 bridge, 不经过 eth0 的 rp_filter 判定)

  1. 入口流量已被阿里云安全组过滤(仅 22 端口放行)
  2. 攻击者需要先能发包到本机,才谈得上利用源地址欺骗

这是重要的运维判断:安全加固不等于"把所有 CIS 基准项都改成推荐值"。

盲目照抄基准常常导致服务故障。典型例子:

(本机 ip_forward=1 是 Docker 的正常需求,我明确标注了不改

(这也是我最初选 2 而非 CIS 推荐的 1 的原因)

正确做法:理解每一项的作用 → 评估本机场景的实际风险 → 做出有依据的决策, 并对"不修改"这个决定本身负责。


6. 最终加固结果

项目原值新值状态
kernel.dmesg_restrict01✅ 生效
net.ipv4.conf.all.log_martians01✅ 生效
net.ipv4.conf.all.send_redirects10✅ 生效
net.ipv4.conf.default.send_redirects10✅ 生效
journald SystemMaxUse无限制(默认≈4G)200M✅ 生效
net.ipv4.conf.all.rp_filter00⚠️ 经评估接受偏差

连通性验证:SSH active、nginx active、站点 HTTP 200、 默认路由正常、DNS 正常。零服务中断。


7. 沉淀的经验

7.1 配置"写进去"和"生效"是两件事

✅ 命令执行成功     ≠  配置生效
✅ 配置文件存在     ≠  配置生效
✅ sysctl -w 返回 0 ≠  最终值正确

唯一可靠的验证是读回实际生效值,而且要读最终值,不是命令的返回值。

本次之所以能发现,是因为我在每一步都做了"应用前 / 应用后"的值对比。

7.2 /etc/sysctl.conf 在加载顺序中永远最后

这是 RHEL/CentOS 系的实现细节,且不受 sysctl.d 编号影响

实际顺序:
  /run/sysctl.d/*.conf
  /etc/sysctl.d/*.conf          (按字典序)
  /usr/local/lib/sysctl.d/*.conf
  /usr/lib/sysctl.d/*.conf
  /etc/sysctl.conf              ← 永远最后

推论:想覆盖 /etc/sysctl.conf 的值,只能改 /etc/sysctl.conf 本身, 或者在运行时用 sysctl -w 强制写(但重启后会丢)。

7.3 rp_filter 是 max() 语义,不是简单覆盖

内核实际用 max(conf/all, conf/<iface>)。配置时必须同时考虑 conf/allconf/default 和具体接口,否则可能"看起来设了但没生效"。

7.4 危险变更要用死人开关

远程改网络参数的标准流程:

# 1. 排程自动回滚(先埋炸弹的引信)
echo "/path/to/revert.sh" | at now + 5 minutes

# 2. 应用变更
sysctl --system

# 3. 从【外部】验证连通性(不能只在本机验证!)
ssh -o ConnectTimeout=5 user@host 'echo OK'

# 4. 验证通过 → 拆除回滚作业
atq | cut -f1 | xargs -r atrm

关键点:第 3 步必须从外部发起新连接。 本机 systemctl is-active sshd 返回 active 不代表外部连得进来——防火墙/路由问题在本机是看不出来的。

7.5 安全加固要做风险权衡,不是照抄清单

基准建议本机决策原因
ip_forward = 0不改(保持 1)Docker 需要,改了就瘫痪
rp_filter = 1(严格)选 2(松散)/ 或接受 0避免不对称路由丢包;评估后风险可接受
各种"推荐值"逐项评估有的有实际收益,有的不适用于本场景

"我不改这一项,因为……" 是成熟的运维判断; "基准说改我就改" 是危险的行为。

7.6 自己的诊断动作会污染系统

本次我又清理了一批自己产生的调试残留:

(来自我压测时触发的限流,属于我的测试流量而非真实问题)

运维诊断要控制副作用。 尤其在生产环境, 诊断命令的输出、临时文件、额外负载都应可回收。


附:可复用的 sysctl 加固检查清单

#!/bin/bash
# sysctl 加固的安全流程

TARGET_FILE=/etc/sysctl.conf          # 注意:想覆盖必须用这个文件
BACKUP=/root/ops-lab/backups/sysctl.conf.$(date +%s)

# 1. 备份
cp "$TARGET_FILE" "$BACKUP"

# 2. 记录原值
declare -A BEFORE
for p in $(grep -oE '^[a-z0-9_.]+' "$TARGET_FILE" | sort -u); do
    BEFORE[$p]=$(sysctl -n "$p" 2>/dev/null || echo N/A)
done

# 3. 修改(此处替换为实际改动)

# 4. 应用
sysctl --system >/dev/null 2>&1

# 5. 【关键】验证最终生效值,不是命令返回值
FAIL=0
for p in "${!BEFORE[@]}"; do
    NOW=$(sysctl -n "$p" 2>/dev/null || echo N/A)
    WANT=$(grep -E "^$p\s*=" "$TARGET_FILE" | tail -1 | awk -F= '{gsub(/ /,"",$2); print $2}')
    if [ -n "$WANT" ] && [ "$NOW" != "$WANT" ]; then
        echo "❌ $p 期望 $WANT 实际 $NOW(被其他文件覆盖?)"
        FAIL=1
    fi
done

# 6. 找出覆盖者
if [ "$FAIL" -eq 1 ]; then
    echo "--- 搜索所有设置该参数的文件 ---"
    grep -rn "$p" /etc/sysctl.conf /etc/sysctl.d/ /usr/lib/sysctl.d/ 2>/dev/null
fi

# 7. 连通性验证(必须从外部!)
echo "请从另一台机器验证: ssh -o ConnectTimeout=5 root@<本机IP> 'echo OK'"

核心思想:配置管理中最危险的不是"改错了",而是"以为改了"。