systemd 的 Environment、EnvironmentFile、PassEnvironment、UnsetEnvironment、LoadCredential 和 LoadCredentialEncrypted 有什么区别?
systemd 的 Environment、EnvironmentFile、PassEnvironment、UnsetEnvironment、LoadCredential 和 LoadCredentialEncrypted 有什么区别?
快速对照
Environment=:在 Unit 里直接赋值
最简单的形式是:
[Service]
Environment="APP_MODE=production" "LOG_LEVEL=info"
每个项目是 NAME=value。引号按 systemd.syntax 的规则解析,不等同于在 shell 中执行。Environment="PATH=$PATH:/opt/app/bin" 不会像 Bash 那样自动读取当前 $PATH 后拼接;它会按 systemd 的环境赋值与后续命令展开规则处理,不能把 shell 教程直接复制进 Unit。
同一变量多次设置时,后续来源和最终合并顺序会影响结果。要确认真实环境,应查看 Unit 配置、manager 环境、PAM、EnvironmentFile 与 UnsetEnvironment,而不是只 grep 一行。
Environment= 不适合密码和 Token
即使 Unit 文件权限为 0600,把秘密放进 Environment= 仍会进入进程环境,并可能被子进程继承。systemd 文档还指出环境变量通过 manager 的 D-Bus IPC 暴露风险,不适合密码、密钥等敏感数据。
秘密也可能出现在:
systemctl show或诊断输出;/proc可见环境(受系统权限与 hidepid 等影响);- 崩溃转储、支持包和进程检查工具;
- 子进程、插件与用户运行的命令;
- 自动化模板、Git 历史和配置备份。
环境变量适合模式、端口、非敏感 feature flag 等普通配置。Secret 应优先使用 Credentials 或专用 Secret Manager,并让应用从文件描述符/文件读取。
EnvironmentFile=:从文本文件加载
[Service]
EnvironmentFile=/etc/myapp/runtime.env
EnvironmentFile=-/etc/myapp/optional.env
EnvironmentFile= 让 systemd 在执行进程前读取文件中的变量赋值。路径前的 - 表示文件不存在时不报错;没有 - 时缺失通常导致启动失败。
文件语法不是任意 shell 脚本:不能可靠使用 export、命令替换、函数或从网络取值。引号、反斜线、多行与注释遵循 systemd.exec 文档定义的专用解析规则。应使用 systemd-analyze verify 和真实启动验证,而不是仅用 source file 测试。
EnvironmentFile 的变量仍会变成普通环境,因此文件权限严格也不能把它升级为 credential 安全模型。
EnvironmentFile 的覆盖与读取时机
可以指定多个 EnvironmentFile,它们按顺序读取,后面的赋值可以覆盖前面的变量。Environment= 与 EnvironmentFile 的相对优先级、manager 环境和 PAM 变量共同组成最终环境;UnsetEnvironment= 最后可删除结果。
systemd 在进程执行前读取 EnvironmentFile,而不是在 daemon-reload 时把值永久缓存。这意味着修改文件后,只有后续新启动的进程读取新值;已经运行的进程环境不会实时变化。通常需要 restart,而不仅是 daemon-reload。
若 ExecStartPre 动态生成 EnvironmentFile,systemd 文档允许利用读取时机,但这会增加时序与失败复杂度。更清晰的方案是配置管理先原子写好文件,再启动 Unit。
PassEnvironment=:从 manager 环境挑选变量
system systemd manager 通常以固定、精简环境运行,不像交互 shell 自动拥有用户所有变量。PassEnvironment= 按变量名选择 manager 当前持有的值传给 Service:
[Service]
PassEnvironment=HTTP_PROXY NO_PROXY
它不会从启动 systemctl start 的当前 shell 临时抓取变量。要修改 manager 环境,可使用 systemctl set-environment、import-environment 或由 manager/environment generator 管理,但这些操作作用域和持久性不同。
对 user manager,环境继承方式与 system manager 不同,PassEnvironment 的实际意义也不同。排障时先确认是 systemctl 还是 systemctl --user,以及变量是否真的存在于对应 manager。
Manager Environment 不是 Secret Store
systemctl set-environment API_TOKEN=... 看似不用写文件,但 manager 环境会被后续多个 Unit 选择或继承,并通过管理接口暴露。它仍不是安全 Secret Store。
Manager 环境适合代理设置、locale 或调试开关,但应控制作用域并在不需要时 unset-environment。自动化脚本不要把 token 放在命令行参数中,因为 shell history、进程列表和审计日志也可能记录。
需要短期秘密时,使用 LoadCredential= 从权限受控的 runtime file、socket 或专用生成流程供给,生命周期更清晰。
UnsetEnvironment=:在最终阶段移除变量
UnsetEnvironment= 可以按变量名删除,也可以按完整 NAME=value 只删除匹配赋值:
[Service]
UnsetEnvironment=DEBUG HTTP_PROXY
systemd.exec 说明该指令在环境列表汇总完成后的最后步骤应用,因此可以移除来自 Environment、EnvironmentFile、PassEnvironment 或 PAM 等来源的变量。
它适合硬化 Service,确保代理、调试或继承变量不进入进程。但删除变量不是擦除历史:变量若曾写入 Unit、日志或 manager 环境,UnsetEnvironment 只影响最终 exec 环境,不会清理其他副本。
若应用需要空字符串与变量不存在的区别,应确认 NAME= 和 UnsetEnvironment 的行为,不要把二者混同。
LoadCredential=:以文件方式提供 Credential
[Service]
LoadCredential=db-password:/etc/credstore/myapp-db-password
ExecStart=/usr/local/bin/myapp
systemd 在激活 Service 时取得源数据,把名为 db-password 的 credential 放入该 Unit 的只读 credential directory。应用通过 $CREDENTIALS_DIRECTORY/db-password 读取,而不是从 $DB_PASSWORD 取得明文。
Credential ID 与源路径由冒号分隔。源可以是普通文件,并根据 systemd 版本支持其他来源。源文件本身仍必须由正确权限、挂载和配置管理保护;LoadCredential 不会把磁盘上的明文自动加密。
CREDENTIALS_DIRECTORY 与 %d
systemd 向进程设置 $CREDENTIALS_DIRECTORY,其值是为当前 Unit 准备的 credential 目录。应用应拼接可信、固定 ID:
$CREDENTIALS_DIRECTORY/db-password
Unit 的某些字段还可使用 %d specifier 指向 credential directory,例如让应用参数引用 %d/db-password。Specifier 在 systemd 解析阶段展开,不是 shell 变量。
应用不应硬编码 /run/credentials/<unit>,因为具体路径与实现可变化。读取后要避免打印内容,关闭不必要的继承,并按应用要求处理末尾换行和二进制数据。
Credential 文件与环境变量的根本差异
Credential 的环境中只出现目录路径,不直接出现秘密值。Manager 在支持的系统上尽量把 credential 置于受保护、只读且与 Unit 生命周期绑定的位置,并按 Service 用户提供访问。
这种模式降低明文通过环境、D-Bus 和子进程无意扩散的风险,但不是魔法硬件保险箱。Service 进程本身必须能读取秘密;若进程被完全攻陷,攻击者通常也能读取其 credential。目标是缩小暴露面与生命周期,而不是声称不可窃取。
Credential 也不自动轮换。源 Secret 更新后,正在运行的进程不会凭空得到新字节,通常需要受控 restart 或应用支持的重载/重新获取流程。
LoadCredentialEncrypted=:启动时解密
LoadCredentialEncrypted= 接收由 systemd-creds encrypt 等方式生成的加密和认证 credential blob。systemd 在启动 Unit 时解密,并只把解密结果放进 credential directory。
加密可绑定机器、TPM2、本地 host secret,或按 systemd-creds 支持的组合与策略生成。绑定方式决定 blob 能否复制到另一台主机解密;部署前必须明确灾备、镜像克隆和主板/TPM 更换场景。
加密 blob 可以更安全地随配置分发或嵌入 Unit,但路径、credential ID、大小和使用关系仍可能泄露元数据。加密也不替代源代码权限、签名发布和访问审计。
systemd-creds encrypt/decrypt/cat
systemd-creds 用于加密、解密、查看和管理 credential 数据。典型安全输入应来自文件或标准输入的受控通道,避免把明文直接作为命令行参数。
systemd-creds encrypt secret.txt secret.cred
实际命令需按目标 systemd 版本选择 --with-key、TPM2 等选项。不同发行版的 systemd 版本和编译功能可能不同,不能在开发机生成后假设所有服务器都能解密。
上线前应在与生产相同的启动环境中验证冷启动、重启、TPM 状态、initrd/容器边界和恢复流程,并且不在测试输出中打印明文。
SetCredential= 与 SetCredentialEncrypted=
SetCredential= 把 Unit 中的内联内容作为 credential 文件提供。它改善应用读取接口,但明文仍存在于 Unit 配置,因此不应被视为保密。它更适合非敏感小配置或测试。
SetCredentialEncrypted= 则允许内联 systemd-creds 生成的加密 blob,启动时解密。这样 Unit 文件可以携带密文,但仍需考虑解密绑定、版本兼容和配置仓库中的元数据。
不要因为名字含 Credential 就默认所有 SetCredential 内容安全。判断关键是 Unit 里保存的是明文还是经过认证加密的 blob。
Credential 与 DynamicUser 的配合
使用 DynamicUser=yes 时,Service 每次运行可获得动态 UID,传统上很难预先把 Secret 文件 chown 给未知用户。systemd credentials 由 manager 在启动时准备,并以适合该 Service 的方式提供,因此与 DynamicUser、ProtectSystem 和只读文件系统硬化更容易组合。
但源 credential 的读取仍由 manager 权限完成,管理员必须限制谁能修改 Unit 和源文件。能够修改 Unit 的主体可以把 credential 指向别处或改变 ExecStart,从而读取秘密。
安全审计应同时覆盖 Unit 文件写权限、drop-in、generator、manager 控制权限和应用二进制,而不仅是 credential 文件 mode。
ExecStart 参数也不适合秘密
把 Secret 写成:
ExecStart=/usr/bin/myapp --password=secret
通常比环境变量更糟,因为命令行可能出现在进程列表、systemctl status、journal、审计和崩溃记录。正确方式是让应用支持 --password-file=%d/db-password 或约定读取 $CREDENTIALS_DIRECTORY。
如果第三方应用只支持环境变量,可使用一个极小、经过审计的 wrapper 读取 credential 文件并在 exec 前设置环境,但秘密仍会进入最终进程环境。应推动应用增加 file/FD 接口,并限制 wrapper 日志和子进程。
不要使用 shell command substitution 把 Secret 展开到命令行;systemd 也不会把 ExecStart 默认当完整 shell 脚本解释。
Unit drop-in 与覆盖优先级
管理员常用 systemctl edit myapp.service 创建 drop-in。Environment= 可以多次追加或覆盖单个变量;对列表型指令如何清空,需要先用空赋值再重设,具体依指令语义。
排障使用:
systemctl cat myapp.service
systemctl show myapp.service -p Environment -p EnvironmentFiles
systemd-analyze verify /etc/systemd/system/myapp.service
systemctl cat 展示 fragment 与 drop-ins,但 generator、manager 环境和运行时凭据还需分别检查。修改 Unit/drop-in 后需要 daemon-reload;只修改 EnvironmentFile 或源 credential 通常无需让 manager 重读 Unit,但仍需 restart 进程读取新值。
容器与便携 Service 的注意点
System service manager、user manager、systemd-nspawn、portable service 和容器内 systemd 的 credential 支持与路径可能不同。Host 加密 blob 若绑定 TPM/机器密钥,复制进另一容器或主机未必能解密。
容器把 /etc/credstore bind mount 进去不等于安全隔离;容器 root、host root 与 orchestrator 的权限模型都需评估。若由 Kubernetes 等平台管理 Secret,应明确是让平台挂载文件,还是再由 systemd credential 接管,避免两套轮换互相脱节。
部署文档应写明最低 systemd 版本和必需功能,旧发行版不能因为忽略未知指令而静默以无秘密状态启动。
轮换 Credential 的正确流程
1. 生成新 Secret 并写入新文件或新加密 blob;
2. 原子替换引用或源文件,避免半写状态;
3. 验证目标主机能解密且 ID 正确,不输出明文;
4. 受控 restart/reload 应用,使新 invocation 读取新 credential;
5. 验证连接使用新 Secret;
6. 在依赖系统撤销旧 Secret;
7. 删除临时明文和过期 blob,并保留脱敏审计。
先撤销旧 Secret 再重启全部实例会造成停机。应支持短暂双凭据窗口或滚动更新,并设计失败回滚。
一套可执行的排障清单
1. systemctl cat 确认 fragment 与所有 drop-ins;
2. 区分 system unit 与 user unit;
3. 检查 EnvironmentFile 是否存在、权限与专用语法;
4. 检查 manager 环境而非当前 shell 环境;
5. 确认 UnsetEnvironment 是否删除最终变量;
6. 检查 $CREDENTIALS_DIRECTORY 是否设置且 ID 文件存在;
7. 以 Service 的 User 身份验证只读权限;
8. 对 encrypted blob 核对 systemd 版本、key/TPM 绑定;
9. 修改后执行正确的 daemon-reload 与 restart;
10. 检查日志、命令行、core dump 和子进程是否泄密。
常见误区
EnvironmentFile 权限 0600 就适合存 Secret
错误。文件本身更受保护,但内容最终进入进程环境,仍有继承和诊断暴露风险。
PassEnvironment 读取 systemctl 当前 shell
错误。它选择 systemd manager 已有的环境变量,不是每次 start 的 shell 快照。
LoadCredential 会自动加密源文件
错误。它安全地提供 credential 文件,但普通 LoadCredential 的源若是明文,磁盘保护仍由管理员负责。
SetCredential 都是加密的
错误。SetCredential 是内联明文;SetCredentialEncrypted 才使用加密 blob。
修改 credential 源后进程会实时更新
错误。Credential 与一次 Service invocation 绑定,通常要 restart 或由应用另行支持轮换。
FAQ
1. 普通非敏感配置应该用哪个?
少量固定值可用 Environment;大量或独立维护的非敏感配置可用 EnvironmentFile。
2. 数据库密码应该放 EnvironmentFile 吗?
不建议。优先用 LoadCredential/LoadCredentialEncrypted,并让应用从 credential 文件读取。
3. 为什么 PassEnvironment 传不到刚 export 的变量?
因为 system manager 不读取当前 shell 的临时环境;需显式管理 manager 环境,且不要用它存 Secret。
4. `$CREDENTIALS_DIRECTORY` 里为什么没有预期文件?
检查 LoadCredential ID/源、Unit 是否重新启动、源权限、systemd 版本以及启动日志中的 credential 错误。
5. `%d` 与 `$CREDENTIALS_DIRECTORY` 有什么区别?
%d 是 systemd 在支持字段中展开的 specifier;后者是进程运行时读取的环境变量,二者指向 credential directory。
6. Encrypted Credential 可以复制到其他机器吗?
取决于加密时的 key/TPM/host 绑定。绑定本机的 blob 通常不能直接在另一台机器解密。
7. 使用 Credential 后进程被攻陷还安全吗?
Credential 降低环境和静态配置暴露,但进程本身需要读取 Secret;被完全攻陷后仍可能泄露,需结合沙箱与最小权限。
8. 修改 EnvironmentFile 需要 daemon-reload 吗?
只改文件内容通常无需 daemon-reload,但运行中进程不会改变,需要 restart;修改 Unit 引用则需 daemon-reload。