在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检查服务日志。与环境变量相关的常见失败原因包括:

  1. EnvironmentFile中的文件路径不正确。
  2. 文件缺失(且路径未以-为前缀)。
  3. 外部环境文件中的变量语法不正确(例如,=符号周围有空格)。

实用的有效模式

场景 使用的指令 位置最佳实践 安全考虑
静态、非敏感配置 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管理的环境启动。如果应用程序需要PATHJAVA_HOMENODE_ENVLD_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_ENVLOG_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

如果每个人都知道影响范围,这就足够了。对于较大的系统,更喜欢可以在轮换期间重叠的凭据,这样你就可以部署新值、验证它,并删除旧值,而无需匆忙的停机窗口。