技术指南

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 参数或历史;编辑器的临时文件、交换文件和备份策略仍需按所用工具检查。应用代码和部署清单只记录凭据名称与路径,不包含值。

用户已存在时跳过 useradd;stat 只显示元数据,不输出凭据
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。

/etc/systemd/system/vpscope-demo.service;按应用所需最小化可写路径
[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 前,应验证能从外部密钥库重新供应并在新主机重新加密。

执行前确认主机绑定和灾难恢复设计;命令可能创建持久 host credential key
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.cred
替换 unit 中的 LoadCredential=;daemon-reload、restart 和健康检查仍不可省略
LoadCredentialEncrypted=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 密文。

返回知识库