文章提出对AI生成的service文件进行声明式能力与实际运行能力差异审计的方法,防止模型生成时遗漏ProtectSystem等安全配置导致权限过度授予。附可复用审计脚本。
从候选服务出发,而非信任声明
最常见的不匹配场景出现在:自由模型生成了一份看似合理的服务定义,而非一个补丁。单元文件看起来没问题,但模型可能留下 User=root、遗漏 ProtectSystem,或者设置了 CapabilityBoundingSet 却不明白生成的解释器实际仍会接收哪些能力。
一种产生候选服务的实用方式是结合 MonkeyCode 的自由模型访问和其免费服务器选项作为观察主机。披露:本文是 MonkeyCode 产品推广的一部分。仅将该服务器用作观察服务的场所,而非跳过静态审查的理由。
在运行任何内容之前,定义一个足够小的 fixture 以保持审计的诚实:
[Unit]
Description=Demo capability fixture
[Service]
User=demo
Group=demo
ExecStart=/usr/bin/python3 -m http.server 8080 --bind 127.0.0.1 --directory /var/lib/demo
StateDirectory=demo
ProtectSystem=strict
ProtectHome=read-only
PrivateTmp=yes
ReadWritePaths=/var/lib/demo
CapabilityBoundingSet=
AmbientCapabilities=
NoNewPrivileges=yes
RestrictAddressFamilies=AF_UNIX AF_INET
PrivateDevices=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectKernelLogs=yes
ProtectControlGroups=yes
RestrictSUIDSGID=yes
MemoryDenyWriteExecute=yes
这不是你会直接投产的生成服务。你用它作为模板,比较生成单元声称的内容与进程实际接收到的内容。
首先提取声明的能力面
从服务定义本身出发。查找定义能力和文件系统面的设置:
CapabilityBoundingSet=
ProtectSystem=、ProtectHome= 和 ReadWritePaths=
PrivateTmp= 和 PrivateDevices=
RestrictAddressFamilies=
你可以打印有效的单元值,而无需手动读取文件:
systemctl show demo.service -p User -p Group -p CapabilityBoundingSet -p AmbientCapabilities -p NoNewPrivileges
如果输出为空,要具体。空的 CapabilityBoundingSet= 表示没有边界能力,但空的 AmbientCapabilities= 不同于缺失字段。将两者都记录为精确字符串,而不是把空白当作安全。
systemd-analyze security demo.service
该评分作为分类信号有用,而非真相。评分可能因为一个弱的默认值而下降,即使其他设置很强。用它来定位差距,然后手动验证这些差距。
在服务启动后观察进程
然后启动 fixture 并从 /proc 读取进程信息:
systemctl start demo.service
pid="$(systemctl show -p MainPID --value demo.service)"
grep -E '^(Uid|Gid|Cap(Inh|Prm|Eff|Bnd|Amb))' "/proc/$pid/status"
CapEff 行告诉你当前哪些能力是有效的。空的位掩码是令人安心的:
capsh --decode=0000000000000000
接下来检查监听套接字,因为单元并未显式声明它:
ss -ltnp | grep "pid=$pid"
你期望从 fixture 看到 127.0.0.1:8080。如果输出显示 0.0.0.0:8080,则服务可在每个接口访问,这是一个应在暴露端口前拒绝或明确说明的行为。
还要检查挂载暴露,因为服务可能读取或写入其单元文件从未提及的路径:
findmnt --task "$pid" -o TARGET,SOURCE,FSTYPE,OPTIONS
一个关键检查是:当声明策略说它们应该是 ro 或隐藏在命名空间后面时,/home、/var 或 / 是否以 rw 出现。
对比声明面与观察面
只有当你主动比较这两个集合时,审计才有用。下表是我使用的最小决策面。
你可以通过编程方式读取相同的数据源,将这些检查变成一个可重复运行的脚本:
#!/usr/bin/env bash
set -euo pipefail
unit="${1:?usage: capability_surface_diff.sh demo.service}"
pid="$(systemctl show -p MainPID --value "$unit")"
test "$pid" != 0 || { echo "no main pid for $unit" >&2; exit 1; }
echo "### Declared hardening"
systemctl show "$unit" -p User -p CapabilityBoundingSet -p AmbientCapabilities -p NoNewPrivileges
echo "### Observed process status"
grep -E '^(Uid|Gid|Cap(Inh|Prm|Eff|Bnd|Amb))' "/proc/$pid/status"
echo "### Listening sockets"
ss -ltnp | grep "pid=$pid" || true
echo "### Mount exposure"
findmnt --task "$pid" -o TARGET,SOURCE,FSTYPE,OPTIONS
echo "### systemd-analyze security"
systemd-analyze security "$unit"
这个脚本特意做得很小,这样你在免费服务器上运行之前可以先阅读它。它不修改任何内容;只打印声明的和观察到的状态。在非 systemd 主机上,你可以调整 /proc 和 ss 检查,并跳过 systemctl 和 systemd-analyze 部分。
局限性和谁不应该使用这种方法
这种审计在服务启动后才能捕获不匹配,因此它不能替代默认拒绝策略,也不能替代在候选服务的单元文件被审查前拒绝运行它。Python 或 Node 进程可以在你的检查之后打开新的文件描述符,因此时间点的 /proc 读取不是最大权限的证明。如果服务生成了 worker,请检查 cgroup 而不是仅检查 MainPID:
systemd-cgls -u demo.service
你不应该将这种方法作为受监管工作负载、多租户服务或未经过静态审查的代码的唯一控制手段。在这些情况下,把生成的服务放在更强的沙箱后面,在声明面被接受之前移除网络访问,并使用 VM 或容器运行时而不是共享主机。该方法对于那些基本无害但在接受端口或持久目录之前需要最后一次检查的生成服务最有用。
因此,你可以在 MonkeyCode 自由模型访问的候选服务上运行此审计,在免费服务器选项上观察它,并把打印出的差异视为草稿审查产物而非证明。价值不在于评分;而是在你停止把生成的 YAML 当作许可、开始把 /proc 当作证据的那一刻。