# ADR-0009: Linux 主机加固扩展 Check 架构（Phase 7e.1）

**状态**: 已采纳
**日期**: 2026-05-08
**参与者**: roger
**关联**: ADR-0008（Phase 7e Linux 主机加固 4 Check 架构）

## 背景

Phase 7e 在 `agentsec server-audit` 中落地了 4 类 Linux 主机加固
Check（`sysctl_audit` / `sshd_config_audit` / `sudoers_audit` /
`suid_audit`），覆盖了内核 / sshd / sudoers / SUID 漂移四个高价值
CIS 项。7e plan 显式列出 4 项 out-of-scope，留 7e.1 跟进：

- world-writable 文件扫描
- auditd 规则审计
- firewall（iptables / nftables / ufw）
- systemd 单元枚举

## 决策

7e.1 一波覆盖前两项 + 把 systemd 拆成两条 Check（黑名单 + unit lint），
共 4 个新 Check，与 7e 同规模。firewall 留作 7e.2。

新增 4 个 Check 类，全部沿用 ADR-0008 模式：

1. **`WorldWritableCheck`（`world_writable_audit`）** —
   `find <path> -maxdepth 4 -type f -perm /002` 跑 7 条路径
   （3 条 OpenClaw 路径 + 4 条 cron 目录），每命中一个文件一条 Finding。
   其中 OpenClaw 路径分两类：1 条静态（`/tmp/openclaw`，绝对路径写在
   baseline yaml）+ 2 条动态（`openclaw_home` / `log_dir`，运行时从
   `ctx.profile.paths` 注入；`LINUX_USER_MODE` 不含 `log_dir` 时该路径
   静默跳过）。
2. **`AuditdRulesCheck`（`auditd_rules_audit`）** —
   `auditctl -s` 探测状态（未装 → not_applicable；enabled=0 → 1 条
   high finding），`auditctl -l` 拉规则列表，按 8 条 CIS baseline
   做 stable-substring 匹配。
3. **`SystemdServiceBlacklistCheck`（`systemd_service_blacklist`）** —
   `systemctl list-unit-files --type=service,socket --state=enabled
   --no-legend --plain`，对照 10 条高风险服务黑名单。
   （`--type=service,socket`：黑名单中有 5 条 `.socket` 单元，仅枚举
   `service` 会漏报；逗号分隔是 systemctl 的 native 多类型语法。）
4. **`SystemdUnitLintCheck`（`systemd_unit_lint`）** —
   `find /etc/systemd/system -maxdepth 1 -name '*.service' -type f`
   + 每文件 cat，三类 regex（可写 ExecStart / shell-wrap / User=root
   叠加升级 critical）。

## 理由

- **范围选择**：firewall 三后端（iptables / nft / ufw）每个都需要独立
  parser + policy，工作量 ≈ 7e 总和；硬塞进 7e.1 会让单 Phase 失焦。
  systemd 拆成两条 Check 是因为「黑名单匹配」与「unit 文件 lint」
  命令面、数据来源、解析逻辑都不同，合在一个 Check 会让该文件超
  400 行，违反 ADR-0008 的"单 Check 一目的"原则。
- **regex 边界**：unit-lint 的 3 条 regex（writable_execstart /
  shellwrap_execstart / root_writable_combo）使用稳定的高信号模式，
  显式避开"Restart=always 全开 / ConditionPathExists 指向隐藏路径"
  这种各发行版误报多的字段。
- **auditd 未装判定**：用 `auditctl -s` 退出码 + stderr 字段二次区分
  「未装（127 + command not found）」与「装了但 daemon 未跑（其它
  非 0 退出码）」。前者归 `not_applicable`，后者归 `skipped`。不
  引入 `which auditctl` 多一次 SSH 往返。
- **TI ID 共享**：`SystemdServiceBlacklistCheck` 与 `SystemdUnitLintCheck`
  都引 `TI-LINUX-CIS-SYSTEMD`。两个 Check 的 finding 都属于"systemd
  侧 CIS 加固"语义，运维查表时一条 TI 行覆盖两类 finding 更连贯，
  也避免 `threat_intel.yaml` 出现"systemd_blacklist / systemd_lint"
  这种实现细节级 ID。
- **baseline 单文件不分裂**：3 个新 section（world_writable / auditd /
  systemd）继续追加到 `linux_hardening_baseline.yaml`，与 ADR-0008
  的"单文件好检索"决策一致。systemd 用 `service_blacklist` /
  `unit_lint` 两个二级 key 而非两个顶层 section，避免顶层膨胀到 9。

## 备选方案

1. **单 Check 处理 systemd（黑名单 + lint 合一）**：*放弃*：单文件
   超 400 行；测试矩阵爆炸；违反 ADR-0008 单一目的原则。
2. **firewall 也塞进 7e.1**：*放弃*：工作量 ≈ 7e 总和；单 Phase
   太大；CI / review 都不利。留作 7e.2。
3. **每条 Check 独立 baseline 文件**：*放弃*：会把
   `linux_hardening_baseline.yaml`、`linux_world_writable.yaml`、
   `linux_auditd.yaml`、`linux_systemd.yaml` 变成 5 个文件，运维
   查表困难。ADR-0008 单文件路线已验证好用。

## 后果

- 真机 server-audit 的 finding 总数会再升高 —— Lightsail prod 实跑
  预计 +5–15 finding（多数 cron 目录通常 0；auditd 未装通常进
  not_applicable；systemd 黑名单通常 0；unit-lint 取决于 OpenClaw
  自身 unit 文件是否落地，≤ 5 条）。
- `ssh_policy.yaml` 新增 5 条 allowed_paths prefix（4 cron +
  systemd system unit 目录）+ 2 类新命令（`auditctl -s` / `-l`，
  `systemctl list-unit-files ...` 一行追加到现有 systemctl
  allowed_argv）。每条都按 v1 严格 argv schema 落地，不放
  `allowed_argv` 通配。
- `/etc/systemd/system/` 作为 prefix 是**比 7e 现有针对 openclaw
  专用 service 文件的 glob 更宽泛的放行**——任何 `cat
  /etc/systemd/system/<unit>.service` 现在都会被策略允许。Ubuntu
  默认 unit 文件 world-readable，无机密泄露顾虑；但语义上是有意
  的边界放宽，记录在案以便后续 lint 类 Check（如 7f）做扩展时能
  借用同一 prefix。如果将来 unit 文件包含 secret 引用（不该有，
  但 systemd `Environment=` 等理论可放），需要重新评估。
- macOS profile 的 server-audit 会再多 4 条 `not_applicable`，
  sample drift guard 会随之刷新。
- `threat_intel.yaml` +3 条 internal TI stub；`cve_database.json`
  随脚本重生，diff 仅是结构性追加。
- 后续 7e.2（firewall）/ 7f（容器） 可以直接借这条
  baseline-YAML + CLI-register-list 模式扩展，结构成熟。
