技术指南
systemd 服务密钥:按需加载、轮换与回滚
用 LoadCredential 把密钥作为只读文件交给单个 systemd 服务,并验证版本、权限、轮换、回滚与泄露处置。
长期运行的 systemd 服务经常通过 `Environment=` 或 `EnvironmentFile=` 接收 API Key。这样虽然比把密钥写进代码好,却仍把密钥放进进程环境,可能被子进程、调试输出或错误报告继承。systemd credentials 把密钥在服务启动时复制成只读文件,只交给指定服务;应用读取文件路径,而不是把密钥值放进环境变量。
先画清一把密钥的边界
| 问题 | 需要确认的答案 | 不能省略的记录 |
|---|---|---|
| 谁读取 | 单个 systemd 服务与专用服务用户 | unit 名、User/Group、应用版本 |
| 怎样读取 | 应用原生支持 token-file / password-file | 凭据文件参数或配置键 |
| 源文件在哪 | root 可读、仓库之外的固定路径 | 所有者、模式、备份/重建方式 |
| 如何轮换 | 新旧凭据短时并存或维护窗口 | 健康检查、撤销点、回滚人 |
这套方法缩小服务之间和子进程环境的暴露面,但不是外部密钥管理器,也不能阻止 root、被攻陷的目标服务或应用自己记录密钥。每个项目、环境和用途仍应使用独立、最小权限、可撤销的凭据。
先确认 systemd 版本和更新状态
`LoadCredential=` 从 systemd 247 起提供;`systemd-creds` 和加密凭据从 250 起提供。发行版可能回移功能或安全修复,因此既要记录 `systemd --version`,也要检查当前发行版更新。下面的主流程只依赖 `LoadCredential=`;版本不足时可暂用 root-only `EnvironmentFile=`,但应记录环境变量的继承和日志风险。
systemd --version
systemd-creds --version
# Debian / Ubuntu
apt-cache policy systemd| 方式 | 适用情况 | 主要边界 |
|---|---|---|
| LoadCredential= | 应用支持从文件读取密钥 | 源文件仍需 root-only;服务重启才载入新值 |
| LoadCredentialEncrypted= | 需要降低磁盘静态明文风险 | 通常绑定 TPM2、主机密钥或两者;迁移需重新供应 |
| EnvironmentFile= | 旧 systemd 或应用只读环境变量 | 密钥进入进程环境并可能向子进程传播 |
| 外部 Secret Manager | 多主机、集中审计或动态凭据 | 还需设计认证、网络故障和本地缓存 |
把源文件留在仓库之外
先创建专用服务用户、可写状态目录和仅 root 可读的凭据目录。`sudoedit` 避免把值放进 shell 参数或历史;编辑器的临时文件、交换文件和备份策略仍需按所用工具检查。应用代码和部署清单只记录凭据名称与路径,不包含值。
sudo useradd --system --home /var/lib/vpscope-demo \
--shell /usr/sbin/nologin vpscope-demo
sudo install -d -m 0750 -o vpscope-demo -g vpscope-demo \
/var/lib/vpscope-demo
sudo install -d -m 0700 -o root -g root /etc/vpscope-demo
sudo install -m 0600 -o root -g root /dev/null \
/etc/vpscope-demo/api-token
sudoedit /etc/vpscope-demo/api-token
sudo stat -c '%a %U:%G %n' /etc/vpscope-demo/api-token让应用读取凭据文件
应用最好原生接受 `--token-file`、`*_FILE` 或等价配置。`%d` 在 unit 中解析为该服务的凭据目录,不应硬编码 `/run/credentials/...`。下面的二进制和健康检查路径是占位示例;先安装并验证实际程序,再启用 unit。
[Unit]
Description=VPScope credential example
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=vpscope-demo
Group=vpscope-demo
LoadCredential=api_token:/etc/vpscope-demo/api-token
ExecStart=/usr/local/bin/vpscope-demo --token-file %d/api_token
Restart=on-failure
RestartSec=5s
UMask=0077
NoNewPrivileges=yes
PrivateTmp=yes
PrivateMounts=yes
ProtectSystem=strict
ProtectHome=yes
ReadWritePaths=/var/lib/vpscope-demo
[Install]
WantedBy=multi-user.target若应用只能读环境变量,包装脚本把文件内容再 `export` 出去会重新引入环境泄露边界。短期兼容可以使用单独的 root-only `EnvironmentFile=`,同时创建迁移任务;不要为了表面使用 credentials 而在 wrapper 中把密钥重新放回环境。
先用无秘密探针验证机制
以下 transient service 把公开的 `/etc/hostname` 当作测试凭据,不会安装持久 unit,也不会输出真实密钥。成功结果应显示凭据可读、只读文件元数据和内容哈希。`PrivateMounts=yes` 给服务独立挂载命名空间,结束后 `--collect` 清理 transient unit。
sudo systemd-run --wait --collect --pipe \
--property=DynamicUser=yes \
--property=PrivateMounts=yes \
--property=LoadCredential=probe:/etc/hostname \
/bin/sh -c '
set -eu
file="$CREDENTIALS_DIRECTORY/probe"
test -r "$file"
stat -c "mode=%a owner=%U:%G bytes=%s" "$file"
sha256sum "$file"
'校验 unit,再启动服务
sudo systemd-analyze verify \
/etc/systemd/system/vpscope-demo.service
sudo systemctl daemon-reload
sudo systemctl enable --now vpscope-demo.service
sudo systemctl is-active --quiet vpscope-demo.service
sudo systemctl show vpscope-demo.service \
--property=User,Group,FragmentPath
sudo journalctl -u vpscope-demo.service -n 50 --no-pager- 以服务用户运行,不以 root 常驻。
- 源文件保持 0600 root:root,代码仓库和镜像中没有副本。
- 应用从文件读取且启动成功;错误日志不包含文件内容。
- 健康检查验证真实依赖,而不是只看 systemctl active。
- 另一个普通服务或用户不能读取源文件;root 权限仍属于信任边界。
轮换需要原子替换和服务重启
systemd 在激活服务时取得凭据快照,运行中的服务不会因源文件变化自动看到新值。先让提供方保持旧、新凭据都有效,在 `.next` 文件中准备新值,原子替换源文件并重启服务;只有健康检查、错误率和用量归属都正常后,才撤销旧值。
sudo install -m 0600 -o root -g root /dev/null \
/etc/vpscope-demo/api-token.next
sudoedit /etc/vpscope-demo/api-token.next
sudo mv /etc/vpscope-demo/api-token.next \
/etc/vpscope-demo/api-token
sudo systemctl restart vpscope-demo.service
sudo systemctl is-active --quiet vpscope-demo.service
curl --fail --silent --show-error \
http://127.0.0.1:8080/health >/dev/null加密凭据是可选的静态保护
`LoadCredentialEncrypted=` 在启动时解密并验证密文,服务仍只收到运行时明文文件。`systemd-creds encrypt` 默认可能使用 TPM2、`/var/lib/systemd/credential.secret` 或两者;密文通常会绑定原主机或系统安装。它不能替代权限、轮换和外部恢复来源。迁移 VPS 前,应验证能从外部密钥库重新供应并在新主机重新加密。
systemd-creds has-tpm2
sudo install -d -m 0700 /etc/credstore.encrypted
systemd-ask-password -n | sudo systemd-creds encrypt \
--name=api_token - \
/etc/credstore.encrypted/vpscope-demo-api-token.credLoadCredentialEncrypted=api_token:/etc/credstore.encrypted/vpscope-demo-api-token.cred泄露时先撤销,不先改 Git 历史
若凭据进入仓库、日志、截图或工单,应把它视为已泄露。GitHub 官方文档明确把撤销或轮换放在历史清理之前,因为删除最新提交或改写历史不会让仍有效的密钥失效,旧克隆、fork 和缓存引用也可能继续保留内容。
- 在提供方撤销或轮换凭据,并暂停仍在持续输出秘密的任务。
- 为受影响服务写入新值、重启并验证健康检查和异常用量。
- 限定日志、Artifact、工单和备份访问,记录暴露时间与读取范围。
- 再评估仓库历史清理,并协调所有 clone、fork 和分支避免重新污染。
- 补充 push protection、最小权限和下一次轮换演练,但不在事件记录中保存密钥值。
常见失败与定位
| 现象 | 原因方向 | 处理 |
|---|---|---|
| unit 启动时报找不到凭据 | 绝对源路径错误或 manager 无法读取 | 检查路径、0600 root 所有权和 journal;不要放宽到全员可读 |
| 应用仍报缺少 token | 应用不支持文件参数或参数名错误 | 核对当前应用文档;不要在 wrapper 中无声 export |
| 替换文件后值没变 | 运行中凭据是激活快照 | 重启服务并执行真实健康检查 |
| 密文在新 VPS 无法解密 | 绑定原 TPM2/host key/安装 | 从外部恢复源重新供应并在新主机加密 |
| 服务正常但密钥出现在日志 | 应用或调试工具回显内容 | 立即轮换,收紧日志字段并处理已上传副本 |
可交付的验收记录
- 记录发行版、systemd 和应用版本,当前安全更新已处理。
- unit 通过 `systemd-analyze verify`,普通值不在 Environment/命令行/unit 明文中。
- 合成凭据探针、服务启动、真实依赖健康检查和失败告警均通过。
- 源文件权限、服务用户、可写目录和 PrivateMounts 边界已经复核。
- 完成一次新旧凭据轮换与回滚演练;旧值只在验收后撤销。
- 替换主机时能从独立来源重新供应凭据,不依赖单份 host-bound 密文。