在Systemd服务单元中安全管理环境变量
使用Environment、EnvironmentFile、drop-in以及更安全的秘密处理方式来配置systemd环境变量。
在Systemd服务单元中安全管理环境变量
环境变量很方便,但它们并非自动私有。在systemd服务中,它们可能出现在单元文件、drop-in、systemctl show、进程检查工具、崩溃报告、调试日志或复制的支持包中。这并不意味着你不能使用它们。这意味着你应该慎重考虑哪些内容放入其中,以及谁可以读取定义这些变量的文件。
本指南涵盖了两种常见指令:Environment=和EnvironmentFile=,然后展示了如何使用drop-in,以便本地配置与包管理的单元保持分离。
环境变量在Systemd中的作用
环境变量提供了一种直接的方式来配置服务,而无需更改其代码。当systemd启动服务时,它会构建进程环境,并在执行ExecStart=之前应用单元中定义的变量。
Systemd在单元文件的[Service]部分提供了两个主要指令来管理这些变量。
1. 直接定义:Environment指令
此方法允许你直接在Systemd单元文件中定义变量。这适用于不常更改的非敏感配置参数。
用法和语法
Environment指令接受一个以空格分隔的变量赋值列表,格式为"KEY=VALUE"。
# /etc/systemd/system/my-app.service
[Unit]
Description=我的应用程序服务
[Service]
User=myuser
WorkingDirectory=/opt/my-app
# 在单元文件中直接定义变量
Environment="APP_PORT=8080" "NODE_ENV=production"
ExecStart=/usr/local/bin/my-app --start
[Install]
WantedBy=multi-user.target
局限性与安全性
虽然方便,但Environment指令不适合存放密码、令牌或数据库凭据。单元文件通常存储在配置管理系统中、被复制到工单中,或者可以被需要检查服务行为但不应看到秘密的操作员读取。请将其用于端口、功能标志、日志级别和路径等值。
2. 外部配置:EnvironmentFile指令
对于较大的配置,从外部文件加载变量通常更清晰。它允许你独立于主单元文件管理变量文件的权限。它还使包提供的单元保持可读,而本地设置则位于/etc中。
用法和语法
EnvironmentFile指令接受一个配置文件的绝对路径。Systemd逐行读取此文件,将每一行视为一个潜在的KEY=VALUE赋值。
[Service]
# 从外部文件加载变量
EnvironmentFile=/etc/config/my-app-settings.conf
ExecStart=/usr/local/bin/my-app --start
环境文件格式
外部文件必须遵循简单的类shell格式:
- 以
#开头的行被视为注释。 - 以空变量赋值(
VAR=)开头的行将清除之前设置的变量。 - 变量定义为
KEY=VALUE。 - 支持对值进行引号处理(
KEY="VALUE WITH SPACES")。
# /etc/config/my-app-settings.conf
# 非敏感变量
MAX_WORKERS=4
LOG_LEVEL=INFO
# 敏感变量(需要严格的文件权限和仔细的访问控制)
DB_PASSWORD=SecureRandomString12345
避免systemd的环境文件解析器不按你预期方式支持的shell习惯。不要写export KEY=value。不要在等号周围加空格。如果值包含空格,请加引号。如果值包含字面引号、反斜杠或换行符,请在生产环境中依赖它之前进行测试。
处理缺失文件
默认情况下,如果EnvironmentFile指定的文件不存在,Systemd将导致服务启动失败。如果环境文件是可选的,你可以在文件路径前加上连字符(-):
EnvironmentFile=-/etc/config/optional-settings.conf
如果文件前缀为-,Systemd将忽略因文件不存在而导致的错误。
最佳实践:对敏感数据使用Drop-in单元
修改核心单元文件(例如/usr/lib/systemd/system/my-app.service)通常不推荐,尤其是当该文件由包管理器管理时。相反,使用drop-in单元文件来应用配置覆盖或添加。
这种做法很重要,因为它将供应商默认值与本地配置分开。它还使审计更容易:单元说明了配置从何处加载,而该文件的权限说明了谁可以读取它。
逐步Drop-in配置
1. 定位/创建Drop-in目录
对于名为my-app.service的服务,drop-in目录必须命名为my-app.service.d/,并位于/etc/systemd/system/层次结构中。
sudo mkdir -p /etc/systemd/system/my-app.service.d/
2. 创建配置覆盖
在drop-in目录中创建一个文件(例如secrets.conf)。此文件只需要[Service]部分以及你想要覆盖或添加的特定指令。
# /etc/systemd/system/my-app.service.d/secrets.conf
[Service]
# 加载安全凭据文件
EnvironmentFile=/etc/secrets/my-app-credentials.env
3. 保护外部环境文件
这是最关键的安全步骤。确保包含秘密的外部文件具有严格的权限。理想情况下,它应由root:root拥有,并且仅对root用户或服务用户本身可读。
# 创建秘密文件
sudo touch /etc/secrets/my-app-credentials.env
# 向文件填充秘密
sudo sh -c 'echo "DB_PASS=S3cr3tP@ssw0rd" >> /etc/secrets/my-app-credentials.env'
# 设置严格权限
sudo chmod 600 /etc/secrets/my-app-credentials.env
如果EnvironmentFile引用的文件包含凭据,请保持它仅对需要管理服务的帐户可读。当systemd在通过User=降低权限之前读取文件时,0600 root:root很常见,但某些操作模型使用专用的root拥有的组和0640。重要的是普通用户无法读取该文件。
同时要诚实地对待剩余风险。环境变量比硬编码的命令行参数更容易处理,但它们仍然不是一个完整的秘密管理系统。对于更高风险的凭据,请考虑专用的秘密存储、短期凭据、较新发行版上的systemd凭据,或直接读取受保护文件的应用程序特定机制。
故障排除与验证
在对单元文件或drop-in进行任何更改后,必须重新加载Systemd管理器配置。
sudo systemctl daemon-reload
sudo systemctl restart my-app.service
要验证Systemd为运行中的服务成功加载了哪些环境变量,请使用systemctl show命令并专门查询Environment属性:
systemctl show my-app.service --property=Environment
示例输出(显示已加载的变量):
Environment=APP_PORT=8080 NODE_ENV=production DB_PASS=S3cr3tP@ssw0rd
该命令对调试很有用,但它也是一个提醒:任何被允许以root身份运行正确检查命令的人都可以看到这些值。不要将此输出粘贴到共享聊天、工单或公共错误报告中,除非你已对其进行编辑。
如果服务启动失败,请使用journalctl -xeu my-app.service检查服务日志。与环境变量相关的常见失败原因包括:
EnvironmentFile中的文件路径不正确。- 文件缺失(且路径未以
-为前缀)。 - 外部环境文件中的变量语法不正确(例如,
=符号周围有空格)。
实用的有效模式
| 场景 | 使用的指令 | 位置最佳实践 | 安全考虑 |
|---|---|---|---|
| 静态、非敏感配置 | Environment |
直接单元文件或drop-in | 低安全风险。 |
| 敏感凭据(秘密) | EnvironmentFile |
外部文件,通过drop-in(*.service.d/)引用 |
关键: 环境文件必须具有0600权限。 |
| 模块化与覆盖 | EnvironmentFile |
Drop-in单元文件 | 将配置与供应商默认值分开。 |
通过在专用的drop-in单元中利用EnvironmentFile指令并确保严格的文件权限,管理员可以安全且灵活地管理服务配置,遵循最小权限和关注点分离的原则。
对于小型内部服务,一个合理的设置通常如下所示:
# /etc/systemd/system/my-app.service.d/env.conf
[Service]
Environment="APP_ENV=production"
EnvironmentFile=/etc/my-app/runtime.env
EnvironmentFile=-/etc/my-app/local.env
runtime.env包含必需的值。local.env是可选的,允许操作员在维护窗口期间覆盖设置,而无需编辑主单元。更改后:
sudo systemctl daemon-reload
sudo systemctl restart my-app.service
sudo journalctl -u my-app.service -n 50 --no-pager
最安全的习惯很简单:将非敏感默认值保留在单元或普通配置文件中,将秘密保留在包拥有的单元之外,锁定任何包含凭据的文件,并验证加载的环境而不将其泄露到不该出现的地方。
值得避免的常见错误
第一个错误是将秘密放在ExecStart=中:
ExecStart=/usr/local/bin/my-app --db-password=s3cret
当你匆忙时,这看起来无害,但命令行参数通常比环境文件更容易暴露。它们可能出现在进程列表、监控工具、shell历史、崩溃报告或复制的服务定义中。如果应用程序支持读取受保护的配置文件,那通常更好。如果它期望环境变量,请使用受保护的EnvironmentFile=并将值保留在命令行之外。
第二个错误是直接编辑供应商单元。软件包升级可能会替换该文件,下次重启可能会静默地丢弃你的环境设置。使用drop-in:
sudo systemctl edit my-app.service
然后仅添加本地覆盖:
[Service]
EnvironmentFile=/etc/my-app/my-app.env
第三个错误是假设服务看到你在终端中看到的相同shell环境。通常不会。你的交互式shell可能具有来自.bashrc、.profile、SSH会话或部署工具的变量。系统服务从systemd管理的环境启动。如果应用程序需要PATH、JAVA_HOME、NODE_ENV、LD_LIBRARY_PATH或类似值,请显式定义它或使用绝对路径。
例如,这是脆弱的:
ExecStart=npm start
这更容易推理:
WorkingDirectory=/opt/my-app
Environment="NODE_ENV=production"
ExecStart=/usr/bin/npm start
第四个错误是在服务用户不需要写入环境文件时使其可写。一个可以覆盖自己环境文件的Web应用程序可以将普通的应用程序错误转变为持久性问题。在许多设置中,服务用户应该读取应用程序数据并写入日志或上传内容,但它不应该能够重写用于启动服务的凭据。
何时环境变量是错误的工具
环境变量很流行,因为它们简单,但它们并不总是最佳接口。如果值很大、结构化、经常轮换或由多个服务共享,那么真正的配置文件或秘密存储通常更容易管理。
数据库URL是一个合理的环境变量:
DATABASE_URL=postgresql://[email protected]:5432/app
完整的JSON服务帐户文档则不那么令人愉快。引号变得尴尬,意外的换行符导致失败,并且人们在调试时更有可能将其粘贴到日志中。在这种情况下,将JSON存储在受保护的文件中并传递文件路径:
GOOGLE_APPLICATION_CREDENTIALS=/etc/my-app/google-service-account.json
然后单独保护JSON文件:
sudo chown root:my-app /etc/my-app/google-service-account.json
sudo chmod 640 /etc/my-app/google-service-account.json
这并不会使秘密变得神奇。应用程序仍然可以读取它。Root仍然可以读取它。但它避免了将复杂的秘密塞入systemd的环境解析器,并使文件级别的审计更加清晰。
更安全的审查清单
在重启使用环境变量的服务之前,检查四件事:
systemctl cat my-app.service
sudo ls -l /etc/my-app/my-app.env
sudo systemd-analyze verify /etc/systemd/system/my-app.service
sudo systemctl daemon-reload
systemctl cat确认哪些drop-in处于活动状态。ls -l确认权限符合你的预期。systemd-analyze verify可以在重启前捕获一些单元语法问题。它不会验证每个特定于应用程序的设置,但它仍然是一个有用的护栏。
重启后,检查日志以查找启动错误:
sudo systemctl restart my-app.service
sudo journalctl -u my-app.service -n 100 --no-pager
如果你需要确认变量已加载,请仔细查询并在共享前编辑输出。对于敏感服务,我更喜欢先检查非秘密变量,例如APP_ENV或LOG_LEVEL。如果该变量从同一文件加载,则文件路径和解析器语法可能是正确的,你可能根本不需要打印包含秘密的值。
最后一个实际要点:在需要之前计划轮换。如果密码或令牌存储在环境文件中,请记下更改值后必须重启哪个服务,以及该重启是否会导致停机。一个容易设置但难以轮换的凭据最终会变成事故。对于小型服务,轮换运行手册可能只有四行:
sudoedit /etc/my-app/my-app.env
sudo systemctl restart my-app.service
sudo systemctl status my-app.service
sudo journalctl -u my-app.service -n 50 --no-pager
如果每个人都知道影响范围,这就足够了。对于较大的系统,更喜欢可以在轮换期间重叠的凭据,这样你就可以部署新值、验证它,并删除旧值,而无需匆忙的停机窗口。