Безопасное управление переменными окружения в сервисных юнитах 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. Распространенные причины сбоя, связанные с переменными окружения, включают:
- Неправильный путь к файлу в
EnvironmentFile. - Отсутствие файла (и путь не был указан с префиксом
-). - Неправильный синтаксис переменной во внешнем файле окружения (например, пробелы вокруг знака
=).
Практические шаблоны, которые работают
| Сценарий | Используемая директива | Лучшая практика размещения | Соображения безопасности |
|---|---|---|---|
| Статическая, нечувствительная конфигурация | 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
Этого достаточно, если все знают радиус поражения. Для более крупных систем предпочитайте учетные данные, которые могут перекрываться во время ротации, чтобы вы могли развернуть новое значение, проверить его и удалить старое значение без спешки во время окна простоя.