Для нового 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_DIRECTORY | systemctl show example-service.service -p LoadCredential, проверка конфигурации приложения | Использовать точное имя слева от двоеточия и формировать путь из переменной systemd |
| Permission denied при чтении | Неподходящий пользователь, права каталога или ограничение sandbox | systemctl show example-service.service -p User, namei -l /etc/example-service/credentials/api-token | Оставить источник закрытым, проверить пользователя сервиса и добавить только нужное разрешение |
| После ротации используется старый токен | Процесс не перечитал credential | systemctl 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-файлу можно передать как безопасную конфигурацию, а сам пароль, токен или ключ должен оставаться в файле.
Параллельное тестирование новой схемы
- Сохраните копию unit и старого источника в защищенном месте с ограниченным сроком хранения.
- Проверьте версию systemd и доступность обеих директив на staging-хосте.
- Подготовьте credential с нейтральным именем и адаптируйте приложение к чтению файла.
- Запустите копию сервиса с отдельным именем или отдельным staging-экземпляром.
- Проверьте healthcheck, журнал, ротацию и запуск после перезагрузки хоста.
- Назначьте окно переключения и заранее подготовьте 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.