Безопасное управление секретами в systemd: LoadCredential, шифрование и ротация без .env | AdminWiki

Безопасное управление секретами в systemd: LoadCredential, шифрование и ротация без .env

28 августа 2026 14 мин. чтения
Содержание статьи

Для нового systemd-сервиса передавайте пароль, API-ключ или токен через LoadCredential=: systemd подготовит файл в каталоге, путь к которому приложение получит через переменную CREDENTIALS_DIRECTORY. Значение не попадет в обычное окружение процесса, аргументы команды и unit-файл.

Если зашифрованное хранение нужно уже на диске, используйте LoadCredentialEncrypted= вместе с systemd-creds. При старте systemd расшифрует credential и передаст приложению обычный временный файл. Механизм снижает риск утечки через Git, EnvironmentFile=, вывод конфигурации и список аргументов, но не защищает секрет от root-компрометации или самого процесса, которому этот секрет выдан.

Ниже приведена схема для Debian, Ubuntu, RHEL, Rocky Linux и AlmaLinux. Перед изменением production-unit проверьте локальную версию systemd и справку конкретного хоста: поддержка директив и режимов systemd-creds зависит от сборки.

Как передать секрет сервису systemd без .env

Какая схема подходит для production

Для локального сервиса с контролируемым доступом подойдет отдельный credential-файл, который хранится вне репозитория, принадлежит root и имеет режим 0600. В unit-файле он подключается строкой LoadCredential=имя:путь. Внутри процесса файл будет доступен по пути вроде ${CREDENTIALS_DIRECTORY}/имя.

Для среды с повышенными требованиями к хранению используйте зашифрованный файл и директиву LoadCredentialEncrypted=имя:путь. Шифрование защищает материал при хранении и резервном копировании, однако ключевой материал хоста нужно резервировать и проверять восстановление. Потеря ключа без проверенной копии означает потерю доступа к credential.

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

Модель угроз и границы защиты systemd credentials

Какие данные считать секретами

К секретам относятся пароли баз данных, API-ключи, bearer-токены, ключи SSH, закрытые ключи TLS, сертификатные цепочки с приватным материалом и конфигурационные файлы с учетными данными. Инвентаризация должна фиксировать владельца, потребителя, срок действия, способ ротации и допустимый простой.

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

Что credentials не скрывают от самого сервиса

Процесс, которому systemd выдал credential, может прочитать его. Hardening не меняет это правило. Ошибка приложения, отладочный лог, core dump или диагностический endpoint способны раскрыть содержимое даже при корректных правах файла.

Отдельно учитывайте доступ root, операторов с правами управления сервисом, резервных систем и средств мониторинга. Credentials защищают канал доставки и уменьшают экспозицию при хранении, но не заменяют контроль доступа к хосту, аудит действий и ротацию.

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

Что выбрать: .env, EnvironmentFile, LoadCredential или LoadCredentialEncrypted

Почему credential-файл отличается от переменной окружения

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

СпособХранениеВидимостьЗащитаСценарий применения
.envОбычно отдельный plaintext-файл рядом с проектомДоступен скриптам, резервному копированию и инструментам разработкиЗависит от прав, исключений Git и дисциплины командыЛокальная разработка или старое приложение при строгом контроле доступа
EnvironmentFile=Файл с парами переменных и значенийЗначения становятся окружением процесса и могут раскрыться через диагностикуПрава файла ограничивают хранение, но не убирают риски окруженияПриложение принимает секрет только через environment
LoadCredential=Источник хранится отдельно, systemd создает credential-файл для запускаСекрет не передается как обычная переменная и не указывается в аргументахПомогают ограниченные права источника и изоляция runtime-каталогаНовое или адаптированное приложение, читающее секрет из файла
LoadCredentialEncrypted=На диске хранится зашифрованный credential-материалПроцесс получает расшифрованный файл только при запускеДобавляет защиту plaintext при хранении, но требует управления ключом хостаProduction-сервис с требованиями к защите резервных копий и диска

Для общей политики защиты хоста полезно связать эту настройку с аудитом SSH, sudo, журналов и файловых прав. Практический чек-лист приведен в руководстве по защите Linux-сервера.

Проверка версии systemd перед внедрением

Проверка поддержки директив и утилиты systemd-creds

Версия дистрибутива не гарантирует одинаковую версию systemd. Backport-пакет, поставщик образа и политика сборки могут изменить доступные возможности. Выполните preflight-check от имени администратора:

systemd --version
ps -p 1 -o pid,comm,args
command -v systemd-creds
systemd-creds --help
man systemd.exec
man systemd.service

В локальной справке найдите LoadCredential= и LoadCredentialEncrypted=. Удобный безопасный поиск по установленным man-страницам:

man systemd.exec | grep -E 'LoadCredential|CREDENTIALS_DIRECTORY'
man systemd-creds | sed -n '1,180p'

Проверка относится именно к PID 1, который запускает сервис. Команда systemd --version показывает установленный пакет, а ps -p 1 помогает исключить контейнерный или нестандартный runtime, где PID 1 управляется иначе.

Почему локальная документация важнее примера из статьи

На Debian и Ubuntu проверьте пакет systemd внутри целевого образа и staging-хоста. На RHEL, Rocky Linux и AlmaLinux сверяйте пакет из подключенного репозитория и фактически работающий PID 1. Команды одинаковы, но набор директив и синтаксис отдельных режимов systemd-creds нужно подтверждать локальной справкой.

Сохраните в чек-листе версию systemd, результат поиска директив, наличие systemd-creds, unit сервиса и успешность тестового запуска. Если encrypted credentials не поддерживаются, начните с LoadCredential= и отдельно спланируйте обновление systemd. Не заменяйте production-unit до теста на копии сервиса.

Пошаговая настройка LoadCredential= для локального credential-файла

Подготовка источника и прав доступа

Исходный plaintext храните в каталоге, недоступном обычным пользователям. Пример использует нейтральное имя и не содержит значения секрета:

install -d -o root -g root -m 0700 /etc/example-service/credentials
umask 077
install -m 0600 /dev/stdin /etc/example-service/credentials/api-token

После запуска последней команды вставьте значение через защищенный терминал и завершите ввод комбинацией, принятой вашей оболочкой. Значение не находится в аргументах команды и не записывается в shell history. Не используйте echo SECRET, printf SECRET или подстановку значения в командную строку.

Проверьте права без вывода содержимого:

namei -l /etc/example-service/credentials/api-token
stat -c '%U %G %a %n' /etc/example-service/credentials/api-token

Родительские каталоги тоже должны ограничивать проход. Режим 0600 бесполезен, если обычный пользователь может заменить файл в каталоге или пройти к нему через слишком широкие права.

Подключение LoadCredential= через drop-in

Пакетный unit лучше оставить без изменений. Создайте drop-in:

systemctl edit example-service.service

Добавьте конфигурацию в секцию [Service]:

[Service]
LoadCredential=api-token:/etc/example-service/credentials/api-token

Левая часть задает имя credential внутри сервиса. Правая часть указывает источник на хосте. Выполните загрузку конфигурации и запуск:

systemctl daemon-reload
systemctl restart example-service.service
systemctl status --no-pager example-service.service

daemon-reload перечитывает unit-файлы. Он не перезапускает уже работающий процесс. После замены содержимого credential обычно нужен systemctl restart, чтобы приложение снова прочитало файл.

Чтение файла через CREDENTIALS_DIRECTORY

Приложение не должно угадывать каталог и не должно печатать содержимое. Нейтральный shell-фрагмент может передать путь дочернему процессу:

#!/bin/sh
set -eu
: "${CREDENTIALS_DIRECTORY:?credential directory is missing}"
credential_path="$CREDENTIALS_DIRECTORY/api-token"
[ -r "$credential_path" ]
exec /usr/local/bin/example-client --token-file "$credential_path"

Код приложения должен открыть файл с минимальным временем жизни дескриптора и закрыть его после чтения. Некоторые API требуют значение без завершающего перевода строки, другие принимают newline. Удаляйте его только по контракту конкретного клиента, не меняя байты вслепую.

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

Шифрование секретов через systemd-creds и LoadCredentialEncrypted=

Подготовка зашифрованного credential без раскрытия значения

systemd-creds работает с credential-материалом и поддерживаемыми ключами systemd. Перед использованием изучите локальный синтаксис:

systemd-creds --help
man systemd-creds

Сначала создайте временный plaintext-файл в runtime-каталоге с закрытыми правами. Значение вводится через stdin:

install -d -o root -g root -m 0700 /run/example-service-secret
umask 077
install -m 0600 /dev/stdin /run/example-service-secret/plaintext

Затем зашифруйте файл командой из локальной справки. В типичной версии systemd используется форма с входным и выходным путем; режим ключа нужно явно сверить на целевом хосте:

systemd-creds encrypt --with-key=host /run/example-service-secret/plaintext /etc/example-service/credentials/api-token.cred
rm -f /run/example-service-secret/plaintext

Если локальная версия требует другой режим, не подставляйте этот пример без проверки. Используйте systemd-creds --help и выберите режим, который привязывает расшифровку к ключевому материалу хоста. Не помещайте секрет в аргумент, переменную оболочки, журнал CI или команду, доступную через аудит процессов.

Проверьте ciphertext и удалите временный каталог после успешной операции:

stat -c '%U %G %a %n' /etc/example-service/credentials/api-token.cred
rm -rf /run/example-service-secret

Подключение LoadCredentialEncrypted= в unit

Добавьте в drop-in:

[Service]
LoadCredentialEncrypted=api-token:/etc/example-service/credentials/api-token.cred

Приложение при этом читает обычный файл ${CREDENTIALS_DIRECTORY}/api-token. Расшифровка выполняется до старта процесса, поэтому код приложения не должен самостоятельно вызывать systemd-creds и хранить ключ.

systemctl daemon-reload
systemctl restart example-service.service
systemctl status --no-pager example-service.service
journalctl -u example-service.service -b --no-pager

Журнал должен содержать только факт успешного запуска или диагностическую ошибку без значения credential. Запрещайте приложению выводить содержимое файла даже на уровне debug-логов.

Восстановление на другом хосте и резервное копирование

Зашифрованный файл может зависеть от ключевого материала конкретного хоста и настроек systemd. Копирование ciphertext на новый сервер не гарантирует расшифровку. Резервная копия должна включать зашифрованный файл, описание режима шифрования, процедуру доступа к ключу и проверенный сценарий восстановления.

Тестируйте восстановление на отдельном хосте до аварии. Проверка считается завершенной, когда новый экземпляр systemd принимает файл, создает credential-каталог и приложение проходит healthcheck без вывода секрета. Резервную копию храните в системе с отдельным контролем доступа и журналом операций.

Ротация секрета и контролируемый перезапуск сервиса

Ротация обычного credential-файла

Получите новое значение через защищенный канал и создайте временный файл в том же каталоге или файловой системе. Это позволяет заменить файл атомарно:

install -d -o root -g root -m 0700 /etc/example-service/credentials/.pending
umask 077
install -m 0600 /dev/stdin /etc/example-service/credentials/.pending/api-token
chown root:root /etc/example-service/credentials/.pending/api-token
mv -f /etc/example-service/credentials/.pending/api-token /etc/example-service/credentials/api-token
rmdir /etc/example-service/credentials/.pending

Команда mv в пределах одной файловой системы меняет имя готового файла целиком. Сервис видит старую или новую версию, но не частично записанное содержимое. Не выполняйте cat, less или диагностический вывод над credential.

systemctl restart example-service.service
systemctl is-active example-service.service
journalctl -u example-service.service -n 50 --no-pager

Ротация зашифрованного credential

Сформируйте новый plaintext только во временном защищенном каталоге, зашифруйте его в новый ciphertext и удалите plaintext сразу после успешной проверки операции:

install -d -o root -g root -m 0700 /run/example-service-secret
umask 077
install -m 0600 /dev/stdin /run/example-service-secret/plaintext
systemd-creds encrypt --with-key=host /run/example-service-secret/plaintext /run/example-service-secret/api-token.cred
rm -f /run/example-service-secret/plaintext
chown root:root /run/example-service-secret/api-token.cred
chmod 0600 /run/example-service-secret/api-token.cred
mv -f /run/example-service-secret/api-token.cred /etc/example-service/credentials/api-token.cred
rm -rf /run/example-service-secret
systemctl restart example-service.service

До переключения проверьте, что локальный systemd-creds способен обработать новый файл в выбранном режиме. Если версия поддерживает отдельную команду проверки или расшифровки без вывода результата, используйте ее согласно systemd-creds --help. Сам ciphertext не является доказательством успешной расшифровки.

Когда нужен daemon-reload, а когда restart

Если изменился unit или drop-in, выполните systemctl daemon-reload. Если изменился только источник credential, systemd может не перечитать его для уже запущенного процесса. Полный systemctl restart создает новый экземпляр процесса и новый каталог credentials.

systemctl reload подходит только приложению, которое документированно перечитывает credential при reload. Сам systemd не заставляет произвольный процесс заново открыть файл по команде reload. Для токенов и паролей используйте restart, если поведение приложения не доказано тестом.

Hardening unit-файла после подключения credentials

Минимальный профиль ограничений

Начните с ограничений, которые соответствуют реальным потребностям сервиса. Пример требует проверки на staging:

[Service]
NoNewPrivileges=yes
ProtectSystem=full
ProtectHome=yes
PrivateTmp=yes
PrivateDevices=yes
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
ReadWritePaths=/var/lib/example-service /run/example-service
CapabilityBoundingSet=

NoNewPrivileges= запрещает процессу получать новые привилегии. ProtectSystem=full закрывает системные каталоги от записи, но приложение может требовать более мягкий режим. ProtectHome= скрывает домашние каталоги, PrivateTmp= дает отдельный временный каталог, а PrivateDevices= ограничивает доступ к устройствам.

RestrictAddressFamilies= разрешает только заявленные семейства сетевых адресов. Если сервис использует DNS, UNIX-сокеты или другой стек, состав списка нужно проверить. ReadWritePaths= оставляет запись только в необходимых каталогах. Пустой CapabilityBoundingSet= убирает Linux capabilities, но некоторые приложения после этого перестают запускаться.

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

Проверка совместимости hardening-параметров

systemd-analyze verify /etc/systemd/system/example-service.service
systemctl daemon-reload
systemctl restart example-service.service
systemctl status --no-pager example-service.service
journalctl -u example-service.service -b --no-pager

Добавляйте ограничения по одному. После каждого изменения проверяйте сетевое соединение, запись в рабочий каталог, доступ к сокету, временным файлам и credential-файлу. При отказе сервиса откатите последний параметр, зафиксируйте причину и подберите более узкое исключение.

Проверка результата после внедрения

Проверка unit и процесса

systemctl cat example-service.service
systemctl show example-service.service -p LoadCredential -p LoadCredentialEncrypted
systemctl status --no-pager example-service.service
systemd-analyze verify /etc/systemd/system/example-service.service
ps -C example-client -o pid,user,args

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

Права источника проверяются отдельно:

namei -l /etc/example-service/credentials/api-token
stat -c '%U %G %a %n' /etc/example-service/credentials/api-token
stat -c '%U %G %a %n' /etc/example-service/credentials/api-token.cred

Проверка приложения без вывода секрета

Используйте healthcheck, тестовое соединение с базой или API, предусмотренную приложением команду проверки. Результат должен сообщать только код успеха и безопасное описание ошибки. Не передавайте credential как аргумент тестовой команды и не включайте трассировку, которая печатает переменные.

systemctl is-active --quiet example-service.service
curl --fail --silent --show-error http://127.0.0.1:8080/health >/dev/null

Отсутствие строки в журнале не доказывает полную защиту. Проверка должна охватывать Git, резервные копии, shell history, environment, аргументы, core dump, права операторов и поведение приложения при ошибке авторизации.

Диагностика типовых проблем LoadCredential

Ищите причину последовательно: версия и синтаксис, права исходного файла, имя credential, значение CREDENTIALS_DIRECTORY, sandboxing, запуск приложения и журнал. Диагностические команды ниже не требуют печати содержимого секрета.

СимптомВероятная причинаКоманда проверкиБезопасное исправление
Unit не загружается после добавления директивыВерсия systemd или синтаксис не поддерживает параметрsystemd --version, systemd-analyze verify /etc/systemd/system/example-service.serviceСверить локальную справку; использовать поддерживаемую директиву или обновить staging-хост
Сервис запущен, но приложение не находит файлНеверное имя credential или путь строится без CREDENTIALS_DIRECTORYsystemctl show example-service.service -p LoadCredential, проверка конфигурации приложенияИспользовать точное имя слева от двоеточия и формировать путь из переменной systemd
Permission denied при чтенииНеподходящий пользователь, права каталога или ограничение sandboxsystemctl show example-service.service -p User, namei -l /etc/example-service/credentials/api-tokenОставить источник закрытым, проверить пользователя сервиса и добавить только нужное разрешение
После ротации используется старый токенПроцесс не перечитал credentialsystemctl status example-service.service, journalctl -u example-service.service -n 50Выполнить systemctl restart; reload применять только при подтвержденной поддержке приложением
Encrypted credential не расшифровываетсяНеверный формат, режим ключа или отсутствует ключевой материал хостаsystemd-creds --help, journalctl -u example-service.service -bСверить режим на исходном и целевом хосте, восстановить проверенную резервную копию
После hardening сервис перестал подключатьсяRestrictAddressFamilies= или capability-ограничения блокируют зависимостьjournalctl -u example-service.service -b, тест healthcheckУбрать последний параметр, определить требование и добавить минимально необходимое разрешение

Миграция с .env или EnvironmentFile без необратимых изменений

Аудит текущего .env и EnvironmentFile

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

systemctl cat example-service.service
systemctl show example-service.service -p EnvironmentFiles -p Environment
rg -n 'EnvironmentFile|\.env|TOKEN|PASSWORD|API_KEY' /etc/systemd/system /etc/example-service

Команда поиска должна выполняться с учетом политики доступа и не должна выводить значения в общий терминал или CI-лог. Разделите значения: путь к credential-файлу можно передать как безопасную конфигурацию, а сам пароль, токен или ключ должен оставаться в файле.

Параллельное тестирование новой схемы

  1. Сохраните копию unit и старого источника в защищенном месте с ограниченным сроком хранения.
  2. Проверьте версию systemd и доступность обеих директив на staging-хосте.
  3. Подготовьте credential с нейтральным именем и адаптируйте приложение к чтению файла.
  4. Запустите копию сервиса с отдельным именем или отдельным staging-экземпляром.
  5. Проверьте healthcheck, журнал, ротацию и запуск после перезагрузки хоста.
  6. Назначьте окно переключения и заранее подготовьте rollback-команды.

Если сервис размещен в облачной инфраструктуре, отдельно проверьте права операционной системы, снимки дисков и резервное копирование. Среда вроде Timeweb Cloud может упростить создание отдельного staging-сервера, но модель доступа к секрету внутри Linux остается ответственностью администратора.

Удаление старого источника

После переключения наблюдайте сервис в течение согласованного окна. Удалите или обнулите старый .env и EnvironmentFile= по политике хранения, проверьте резервные копии и Git-историю, обновите runbook и настройки секретного сканера.

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

Для сервисов, которые используют AI API, тот же принцип применим к ключу агрегатора: храните его как credential-файл, а не в unit или командной строке. Например, при подключении AiTunnel приложение должно получать путь к ключу через конфигурацию, а не печатать сам ключ в логах.

Откат и операционный чек-лист

Условия успешного внедрения

  • Версия PID 1, наличие systemd-creds и поддержка нужных директив подтверждены на целевом хосте.
  • Секрет отсутствует в Git, unit-файле, аргументах процесса и обычном environment.
  • Источник и родительские каталоги имеют минимально необходимые владельца и права.
  • Приложение читает файл из CREDENTIALS_DIRECTORY и проходит функциональный healthcheck.
  • Ротация проверена без значения в shell history, журнале и командной строке.
  • Hardening добавлялся по одному параметру и не ломает сетевые, файловые и runtime-зависимости.
  • Для encrypted credential существует резервная копия и тестовое восстановление.
  • Команды отката проверены на staging и записаны в операционный runbook.

Для отката сохраните старую конфигурацию drop-in в защищенном месте. Удалите или временно переименуйте новую drop-in-конфигурацию, восстановите прежнюю схему, перечитайте unit и перезапустите сервис:

systemctl edit example-service.service
systemctl daemon-reload
systemctl restart example-service.service
systemctl status --no-pager example-service.service
journalctl -u example-service.service -b -n 100 --no-pager

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

Для расширенного аудита Linux-сервера сопоставьте эти проверки с чек-листом аудита. При работе с контейнерами учитывайте отдельную модель доступа к secrets и namespace, описанную в руководстве по безопасности Docker.

Поделиться:
Сохранить гайд? В закладки браузера