AI 生成的 systemd 服务常因权限过大(开放网络族、写 /etc、保留冗余特权)产生安全风险,提出扫描三大信号并生成安全 drop-in 的审查门禁流程。
AI 生成的服务代码通常只审查它能否启动,而不审查它在启动后要求内核允许什么。这一点在小型的免费服务器上恰恰是真正的危险所在——由免费模型编写的服务往往会"失败开放"(fail open):打开额外的网络协议族、写到自己目录之外、保留从未使用过的权限。你可以将这种模糊的担忧转化为可重复的审查门控:扫描生成代码中的三个具体信号,将这些信号转换为 systemd sandbox drop-in,用 systemd-analyze security 验证结果,而不是信任 prompt 自带的 unit 文件。当你使用 MonkeyCode 的免费模型访问来起草一个面向免费服务器的服务时,这是首次 systemctl start 之前必须运行的审查步骤。
披露:本文是作为 MonkeyCode 产品推广的一部分撰写的。
从生成的代码仓库开始,而不是从模型对服务功能的解释开始。模型可以生成一份看似合理的 README,写着"以低权限用户运行",而实际代码却调用了 chmod、写入 /etc,或绑定了宽泛访问权限的 socket。在免费服务器上,这些错误不是抽象的供应链问题,而是直接影响你仅有的那点 RAM、磁盘和公共网络暴露面。
编写任何加固配置之前,你需要三份清单:
初扫工具可以在不执行任何代码的情况下收集这些信号。扫描器的设计刻意简单,它不是安全证明,而是一个用于构建 systemd unit 的提示词。
#!/usr/bin/env bash
set -euo pipefail
REPO_DIR="${1:?usage: derive-sandbox.sh /path/to/repo}"
printf '%s\n' "## writes"
# Files opened for append/truncate/write in common languages.
grep -RIlE \
-e "open\\([^,]+,[^)]*['\"][wa]" \
-e 'fs\.writeFile|fs\.writeFileSync|fs\.createWriteStream' \
-e 'open\\([^,]+,[^)]*O_WRONLY|O_RDWR' \
-e 'Path\..*\.write_(text|bytes)' \
-e '\.to_csv\(|joblib\.dump|pickle\.dump' \
"$REPO_DIR" 2>/dev/null | sort -u || true
printf '%s\n' "## network"
grep -RIoE \
-e 'https?://[a-zA-Z0-9./_-]+' \
-e 'socket\.?\(|createConnection\(|connect\(' \
-e 'requests\.(get|post|put|patch|delete)|urllib\.request|urlopen' \
-e 'fetch\(|axios\.(get|post|put)' \
"$REPO_DIR" 2>/dev/null | sort -u || true
printf '%s\n' "## privilege transitions"
grep -RIoE \
-e 'sudo|chmod|chown|setuid|setgid|setcap' \
-e 'subprocess|os\.system|exec\(|system\(' \
"$REPO_DIR" 2>/dev/null | sort -u || true
对它运行生成的仓库:
./derive-sandbox.sh ~/repo
将输出视为你需要约束的命名空间列表,而不是完整的审计。许多危险模式仍然隐藏在字符串拼接、动态导入、生成的配置或 shell 展开背后。这正是下一步至关重要的原因:不是手动检查每一行代码,而是缩小内核犯错的余地。
一旦扫描返回了一组写入、socket 和进程调用,将每个条目转换为狭窄的 sandbox 规则。保持规则最小化,然后在服务失败后才按需添加它实际需要的具体路径或目标地址。
关键操作是从默认拒绝(deny-by-default)立场出发。生成的代码可能已经包含了一个 unit 文件,但如果服务写入 /etc 或调用 sudo,你不应该接受其 ProtectSystem=full 的声明。从观察到的行为构建 sandbox,然后让 systemd 强制执行它。
假设服务以 myapp 运行,只写入 /var/lib/myapp 和 /run/myapp,需要到单个 API(203.0.113.10)的出站 HTTPS,且不能绑定特权端口。创建一个精确编码这些限制的 drop-in:
# /etc/systemd/system/myapp.service.d/10-sandbox.conf
[Service]
User=myapp
Group=myapp
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=tmpfs
ReadWritePaths=/var/lib/myapp /run/myapp
UMask=0077
CapabilityBoundingSet=
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
IPAddressDeny=any
IPAddressAllow=203.0.113.10
RestrictNamespaces=yes
LockPersonality=yes
MemoryDenyWriteExecute=yes
RestrictRealtime=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
ProtectHostname=yes
ProtectClock=yes
如果服务需要直接绑定 80 或 443 端口,只需添加必要的 capability:
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
AmbientCapabilities=CAP_NET_BIND_SERVICE
否则保持 capability 集为空,并将进程运行在本地反向代理之后的高端口上。如果需要出站 DNS 解析,也将解析器地址添加到 IPAddressAllow=,不要因为 DNS 失败一次就开放所有流量。
重新加载并测量 unit,然后再信任它:
sudo systemctl daemon-reload
sudo systemctl restart myapp
systemd-analyze security myapp
systemd-analyze security 返回一个暴露评分。仅将其作为 drop-in 前后的对比值使用,而不是作为通过/失败的证书。该评分即使在生成的服务仍然隐藏着危险的 eval(在字符串中)时也可能改善;评分不能替代对进程的观察。
静态扫描器会遗漏任何在运行时组装的内容。生成的 Python 服务可以用 fmt 构建路径、根据环境变量导入模块,或执行存储在 base64 字符串中的 payload。systemd drop-in 无法看到这些字符串,它只能阻止生成的进程写入文件系统的很大一部分、打开任意 socket 或调用特权 syscall。如果服务后来因你未允许的路径或地址而失败,这个失败本身就是有用的信息:添加狭窄的例外并重新运行检查。
第二次扫描时,在授予任何出站访问权限之前,先在无网络命名空间下运行服务。这样你可以获得写入和进程尝试的动态跟踪,而不会暴露公共网络。这也正是免费服务器成为良好测试平台的原因——sandbox 失败只意味着一次重启,而非生产事故。
如果你处理的受监管数据、为他人管理凭据、或运行必须持续可用的服务,请不要将此工作流作为唯一审计手段。在这些情况下,应使用签名镜像、策略检查和真实代码审查。此方法适用于 AI 生成服务可能以过多默认权限落在主机上的小型免费服务器场景。
如果你使用 MonkeyCode 的免费模型访问生成初始服务草稿,请将此 systemd drop-in 作为生成的审查产物的一部分,而不是让模型给出权限摘要。有价值的答案是你可以阅读和测试的内核级规则集,而不是那句"服务是安全的"陈述。