Безопасное управление переменными окружения в сервисных юнитах Systemd

Настройка переменных окружения systemd с помощью Environment, EnvironmentFile, drop-in и более безопасной обработки секретов.

Безопасное управление переменными окружения в сервисных юнитах 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

Избегайте привычек shell, которые парсер файлов окружения systemd не поддерживает так, как вы ожидаете. Не пишите export KEY=value. Не ставьте пробелы вокруг знака равенства. Если значение содержит пробелы, заключайте его в кавычки. Если значение содержит буквенные кавычки, обратную косую черту или символы новой строки, протестируйте его, прежде чем полагаться на него в production.

Обработка отсутствующих файлов

По умолчанию, если файл, указанный в 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, содержит учетные данные, сделайте его доступным для чтения только той учетной записи, которой нужно управлять сервисом. 0600 root:root является обычным, когда systemd читает файл перед понижением привилегий с помощью User=, но некоторые операционные модели используют выделенную группу, принадлежащую 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 файл юнита Отделяет конфигурацию от настроек вендора по умолчанию.

Используя директиву EnvironmentFile в выделенном drop-in юните и обеспечивая строгие права доступа к файлам, администраторы могут безопасно и гибко управлять конфигурациями сервисов, придерживаясь принципов наименьших привилегий и разделения ответственности.

Для небольшого внутреннего сервиса разумная настройка часто выглядит так:

# /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

Четвертая ошибка — делать файл окружения доступным для записи пользователем сервиса, когда это не нужно. Веб-приложение, которое может перезаписать свой собственный файл окружения, может превратить обычную ошибку приложения в проблему сохранения. Во многих конфигурациях пользователь сервиса должен читать данные приложения и записывать журналы или загрузки, но он не должен иметь возможность перезаписывать учетные данные, используемые для запуска сервиса.

Когда переменные окружения — неподходящий инструмент

Переменные окружения популярны, потому что они просты, но они не всегда являются лучшим интерфейсом. Если значение большое, структурированное, часто меняется или используется несколькими сервисами, настоящий файл конфигурации или хранилище секретов обычно проще в управлении.

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. Если она загрузилась из того же файла, путь к файлу и синтаксис парсера, вероятно, верны, и вам, возможно, не нужно выводить значения, содержащие секреты.

Один последний практический момент: планируйте ротацию до того, как она понадобится. Если пароль или токен хранятся в файле окружения, запишите, какой сервис должен быть перезапущен после изменения значения и вызывает ли этот перезапуск простой. Учетные данные, которые легко установить, но трудно ротировать, в конечном итоге приведут к инциденту. Для небольших сервисов runbook ротации может состоять всего из четырех строк:

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

Этого достаточно, если все знают радиус поражения. Для более крупных систем предпочитайте учетные данные, которые могут перекрываться во время ротации, чтобы вы могли развернуть новое значение, проверить его и удалить старое значение без спешки во время окна простоя.