物理主机服务密钥隔离:用 systemd 凭据完成轮换验收

物理主机服务密钥隔离:用 systemd 凭据完成轮换验收

同一台物理主机上运行接口服务、同步任务与监控采集器时,服务令牌经常散落在启动脚本、环境文件和部署记录中。进程参数检查、故障日志或一次配置归档,都可能扩大暴露范围。本文在 Debian 12 上使用 systemd 的 LoadCredential,把密钥源文件留在管理员控制的目录,只向指定服务交付运行时副本,并用可重复的检查验证轮换流程。

一、先明确凭据隔离的边界

这里解决的是“服务怎样取得秘密”,不是密钥签发、集中保管或远程认证协议。CloudDDR 物理主机的系统与数据由用户管理;付款后在控制台查看连接凭据,通过 SSH 进入系统。以下操作需要管理员权限,先在测试服务上完成,不直接修改业务启动方式。

环境变量不适合作为默认秘密载体:它可能进入调试输出、子进程环境或诊断归档。启动参数也可能被进程检查工具记录。LoadCredential 则由系统服务管理器读取源文件,在服务启动时提供凭据目录,程序根据 CREDENTIALS_DIRECTORY 找到文件。

凭据文件隔离不能抵御已取得管理员权限的进程,也不能阻止应用把秘密写进日志。保护边界必须同时包含文件权限、服务账户和应用输出行为。

源文件在本例中仍是明文,不因使用 LoadCredential 自动获得静态加密。整机备份、管理员访问与密钥签发记录,也应按照敏感数据处理。

二、建立专用账户与受限源文件

准备独立检查环境

确认 systemctl --version,本例使用 Debian 12 提供的 systemd,并安装 Python 3。检查账户名没有被已有业务占用;下列命令仅在账户尚不存在时创建它。

sudo apt-get update
sudo apt-get install python3
id credential-check
sudo useradd --system --no-create-home --shell /usr/sbin/nologin credential-check
sudo install -d -o root -g root -m 0700 /etc/service-secrets
sudo sh -c 'umask 077; printf "%s" "fixture-token-alpha" > /etc/service-secrets/api-token'
sudo stat -c '%U %G %a %n' /etc/service-secrets /etc/service-secrets/api-token

id 返回账户不存在后才执行 useradd;若已存在,应检查用途,而不是复用未知账户。预期目录权限为 700,文件为 600,属主与属组均为 root。示例字符串是无权限的测试数据,可以进入命令历史;真实令牌不得按此方式直接写在命令中。

真实值应通过受控文件交接取得,再以管理员权限安装到目标位置。不要使用编辑器临时文件、终端回显或开启追踪的 Shell 搬运秘密。服务账户应无法读取源文件:

sudo -u credential-check test -r /etc/service-secrets/api-token
printf '%s\n' "$?"

这里预期退出码为 1。若为 0,先排查目录权限、文件 ACL 与账户组成员关系,不继续部署。

三、让程序只读取运行时凭据

将以下程序保存为 /usr/local/libexec/credential-check.py,创建父目录,并设为 root 所有、权限 755。目录及程序都不得允许服务账户写入。程序只报告固定错误,不打印令牌、长度或摘要,避免为了验收又制造泄露渠道。

import os
from pathlib import Path

directory = os.environ.get("CREDENTIALS_DIRECTORY")
if not directory:
    raise SystemExit("Credential directory missing")

try:
    token = (Path(directory) / "api-token").read_bytes()
except OSError:
    raise SystemExit("Credential unavailable") from None

if not token or b"\n" in token or b"\r" in token:
    raise SystemExit("Credential format rejected")

本例约定令牌非空且不含换行;这不是所有密钥格式的通用规则。真实程序应按上游协议校验,例如允许 PEM 多行内容时就不能沿用这项检查。

创建 /etc/systemd/system/credential-check.service:

[Unit]
Description=Service credential validation

[Service]
Type=oneshot
User=credential-check
Group=credential-check
LoadCredential=api-token:/etc/service-secrets/api-token
ExecStart=/usr/bin/python3 /usr/local/libexec/credential-check.py
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=yes
UMask=0077

这是一次性检查单元,成功后处于 inactive 状态属于正常结果。无需设置开机启用,也不应为了测试套用业务服务的重启策略。真实应用可按相同方式读取文件,但文件系统限制必须结合业务写入目录验证,不能盲目复制。

四、分别验收可读性与认证结果

sudo systemctl daemon-reload
sudo systemctl start credential-check.service
sudo systemctl show credential-check.service -p Result -p ExecMainStatus
sudo journalctl -u credential-check.service -n 20 --no-pager

预期 Result=success、ExecMainStatus=0。这说明服务账户能通过运行时目录读取凭据,而前一节已经验证它不能直接读取源文件。不要用 cat、环境变量全集或进程内存转储证明这一点。

这项检查不代表令牌有效。接入真实服务后,还需执行一项不会改变业务数据的认证请求,核对响应状态与预期权限。认证失败时分别检查凭据格式、上游是否已启用新令牌、应用是否实际重启,以及请求是否使用了旧连接或缓存。

验收记录只保存单元版本、操作顺序、退出码与认证结论,不保存令牌值。若必须关联一次轮换,可使用内部变更编号,而不是令牌摘要。观察日志时还要确认异常路径没有打印请求头或完整配置。

五、用原子替换完成轮换与失败回退

更新源文件不会修改正在运行的凭据副本。轮换顺序应是:取得新值、验证格式、原子替换、重启服务、验证认证,最后按照上游规则撤销旧值。只执行 daemon-reload 不会刷新运行中的秘密。

以下命令仅轮换测试数据;暂存文件与目标文件位于同一目录,以便重命名在同一文件系统内完成。

sudo sh -c 'umask 077; printf "%s" "fixture-token-beta" > /etc/service-secrets/api-token.next'
sudo chown root:root /etc/service-secrets/api-token.next
sudo chmod 0600 /etc/service-secrets/api-token.next
sudo mv -f /etc/service-secrets/api-token.next /etc/service-secrets/api-token
sudo systemctl restart credential-check.service
sudo systemctl show credential-check.service -p Result -p ExecMainStatus

随后进行失败演练:将测试源文件替换为空文件,再启动检查单元,预期得到非零退出码与固定格式错误;恢复非空测试值后重新启动,预期成功。这样可以确认应用不会在秘密缺失时悄悄继续运行。不要在生产令牌上进行破坏性演练。

真实轮换前应确认上游能否同时接受新旧令牌。若不能,必须设计协调切换;若旧值已被撤销,恢复旧文件也无法恢复认证。旧值应仅在受控回退期限内保留,并纳入同等权限保护,不能长期复制在业务目录。

完成验收后,停用并移除测试单元、脚本与测试凭据,执行 daemon-reload,确认没有遗留暂存文件。最终交付物应是一条可复核的流程:普通账户读不到源文件,目标服务能读取运行时凭据,错误值导致明确失败,轮换后的实际认证通过,日志中没有敏感值。

常见问题

LoadCredential 能防止管理员读取服务密钥吗?

不能。系统管理员和获得相应权限的进程仍可能读取源文件或服务内存。它用于减少密钥在启动参数、环境变量与普通账户之间的暴露,不是针对管理员的保密边界。

替换凭据源文件后,运行中的服务会自动取得新密钥吗?

不会。LoadCredential 在服务启动时装载凭据。本文采用原子替换源文件后重新启动服务,再验证新密钥对应的认证结果;仅执行 daemon-reload 不会更新运行中的凭据。

AMD EPYC 7543P 独享物理主机,$447/月起。

物理主机配置,从内存档位开始

AMD EPYC 7543P,128GB 与 256GB DDR4-3200 ECC 两档。