Изоляция секретов служб и проверка ротации через systemd
Когда на одном выделенном сервере работают API-сервис, задачи синхронизации и сборщики данных мониторинга, служебные токены нередко оказываются разбросаны по скриптам запуска, файлам окружения и записям о развёртывании. Просмотр аргументов процессов, журналы сбоев или очередное архивирование конфигурации могут расширить круг доступа к этим секретам. В этой статье мы используем LoadCredential в systemd на Debian 12: исходные файлы с секретами остаются в каталоге под контролем администратора, а копии для использования во время работы передаются только указанной службе. Процедуру ротации проверяем с помощью воспроизводимых тестов.
1. Сначала определите границы изоляции учётных данных
Здесь решается вопрос «как служба получает секрет», а не задачи выпуска ключей, их централизованного хранения или выбора протокола удалённой аутентификации. Системой и данными на выделенном сервере CloudDDR управляет пользователь. После оплаты данные для подключения можно посмотреть в панели управления, а затем войти в систему через SSH. Для следующих операций нужны права администратора. Сначала выполните их на тестовой службе, не меняя сразу способ запуска рабочего приложения.
Переменные окружения не стоит использовать как основной способ передачи секретов: их значения могут попасть в отладочный вывод, окружение дочерних процессов или диагностические архивы. Аргументы запуска также могут быть записаны инструментами просмотра процессов. При использовании LoadCredential исходный файл читает системный менеджер служб. При запуске службы он предоставляет каталог с учётными данными, а программа находит нужный файл через CREDENTIALS_DIRECTORY.
Изоляция файлов с учётными данными не защищает от процесса, который уже получил права администратора, и не мешает приложению записывать секреты в журнал. Граница защиты должна учитывать одновременно права на файлы, учётную запись службы и поведение приложения при выводе данных.
В этом примере исходный файл по-прежнему хранится в открытом виде: использование LoadCredential само по себе не обеспечивает шифрование данных при хранении. Резервные копии всего сервера, административный доступ и записи о выпуске ключей также следует обрабатывать как чувствительные данные.
2. Создайте отдельную учётную запись и ограничьте доступ к исходному файлу
Подготовьте отдельную среду для проверки
Проверьте вывод systemctl --version. В примере используется systemd из Debian 12; также необходимо установить Python 3. Убедитесь, что имя учётной записи не занято уже работающей службой. Команду создания учётной записи из следующего блока выполняйте только в том случае, если она ещё не существует.
sudo apt-get update
sudo apt-get install python3
id credential-check
sudo useradd --system --no-create-home --shell /usr/sbin/nologin credential-check
sudo install -d -o root -g root -m 0700 /etc/service-secrets
sudo sh -c 'umask 077; printf "%s" "fixture-token-alpha" > /etc/service-secrets/api-token'
sudo stat -c '%U %G %a %n' /etc/service-secrets /etc/service-secrets/api-token
Выполняйте useradd только после того, как id сообщит об отсутствии учётной записи. Если она уже существует, выясните её назначение, а не используйте неизвестную учётную запись повторно. Ожидаемые права: 700 для каталога и 600 для файла; владелец и группа — root. Строка в примере — тестовые данные, которые не предоставляют никаких прав доступа, поэтому её допустимо оставлять в истории команд. Настоящий токен нельзя указывать непосредственно в команде таким способом.
Настоящее значение следует получить через контролируемую передачу файла, а затем установить в целевой каталог с правами администратора. Не переносите секреты через временные файлы редактора, вывод на терминал или Shell с включённой трассировкой. Учётная запись службы не должна иметь возможности прочитать исходный файл:
sudo -u credential-check test -r /etc/service-secrets/api-token
printf '%s\n' "$?"
Ожидаемый код завершения — 1. Если получено 0, сначала проверьте права каталога, ACL файла и членство учётной записи в группах. Не продолжайте развёртывание до устранения причины.
3. Настройте программу на чтение только runtime-копии учётных данных
Сохраните следующую программу в /usr/local/libexec/credential-check.py, предварительно создав родительский каталог. Назначьте root владельцем каталога и программы и установите права 755. Учётная запись службы не должна иметь права записи ни в каталог, ни в файл программы. Программа сообщает только заранее заданные ошибки и не выводит токен, его длину или хеш, чтобы сама проверка не создавала новый канал утечки.
import os
from pathlib import Path
directory = os.environ.get("CREDENTIALS_DIRECTORY")
if not directory:
raise SystemExit("Credential directory missing")
try:
token = (Path(directory) / "api-token").read_bytes()
except OSError:
raise SystemExit("Credential unavailable") from None
if not token or b"\n" in token or b"\r" in token:
raise SystemExit("Credential format rejected")
В этом примере предполагается, что токен не пуст и не содержит переводов строк. Это не универсальное правило для всех форматов ключей. В рабочей программе формат нужно проверять по требованиям внешней системы: например, если допускается многострочное содержимое PEM, эту проверку использовать нельзя.
Создайте /etc/systemd/system/credential-check.service:
[Unit]
Description=Service credential validation
[Service]
Type=oneshot
User=credential-check
Group=credential-check
LoadCredential=api-token:/etc/service-secrets/api-token
ExecStart=/usr/bin/python3 /usr/local/libexec/credential-check.py
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=yes
UMask=0077
Это юнит для однократной проверки, поэтому состояние inactive после успешного завершения — нормальный результат. Включать его запуск при загрузке системы не требуется; также не следует переносить на него политику перезапуска рабочей службы ради тестирования. Реальное приложение может читать файл тем же способом, но ограничения файловой системы нужно проверить с учётом каталогов, в которые оно записывает данные. Копировать эти настройки без проверки нельзя.
4. Отдельно проверьте чтение файла и результат аутентификации
sudo systemctl daemon-reload
sudo systemctl start credential-check.service
sudo systemctl show credential-check.service -p Result -p ExecMainStatus
sudo journalctl -u credential-check.service -n 20 --no-pager
Ожидаются Result=success и ExecMainStatus=0. Это означает, что учётная запись службы может прочитать учётные данные через runtime-каталог. Ранее уже было проверено, что напрямую прочитать исходный файл она не может. Не подтверждайте это с помощью cat, полного вывода переменных окружения или дампа памяти процесса.
Эта проверка не доказывает, что токен действителен. После подключения к реальной службе нужно дополнительно выполнить запрос с аутентификацией, который не изменяет рабочие данные, и сверить статус ответа и ожидаемые права доступа. При ошибке аутентификации отдельно проверьте формат учётных данных, активирован ли новый токен во внешней системе, было ли приложение действительно перезапущено и не использовал ли запрос старое соединение или кеш.
В записи о проверке сохраняйте только версию юнита, последовательность операций, коды завершения и вывод об успешности аутентификации — без значения токена. Если нужно связать запись с конкретной ротацией, используйте внутренний номер изменения, а не хеш токена. При просмотре журналов также убедитесь, что обработчики ошибок не выводят заголовки запросов или полную конфигурацию.
5. Выполните ротацию и откат при сбое через атомарную замену
Обновление исходного файла не меняет копию учётных данных, которую использует уже запущенная служба. Правильная последовательность ротации: получить новое значение, проверить формат, выполнить атомарную замену, перезапустить службу, проверить аутентификацию и только после этого отозвать старое значение по правилам внешней системы. Один лишь daemon-reload не обновляет секреты работающей службы.
Следующие команды заменяют только тестовые данные. Временный и целевой файлы находятся в одном каталоге, чтобы переименование выполнялось в пределах одной файловой системы.
sudo sh -c 'umask 077; printf "%s" "fixture-token-beta" > /etc/service-secrets/api-token.next'
sudo chown root:root /etc/service-secrets/api-token.next
sudo chmod 0600 /etc/service-secrets/api-token.next
sudo mv -f /etc/service-secrets/api-token.next /etc/service-secrets/api-token
sudo systemctl restart credential-check.service
sudo systemctl show credential-check.service -p Result -p ExecMainStatus
Затем проведите тест отказа: замените исходный тестовый файл пустым и снова запустите проверочный юнит. Ожидаются ненулевой код завершения и заранее заданное сообщение об ошибке формата. После восстановления непустого тестового значения повторный запуск должен завершиться успешно. Так можно убедиться, что приложение не продолжает незаметно работать при отсутствии секрета. Не проводите разрушительные проверки на рабочих токенах.
Перед реальной ротацией выясните, может ли внешняя система одновременно принимать старый и новый токены. Если нет, потребуется согласованное переключение. Если старое значение уже отозвано, восстановление старого файла не восстановит аутентификацию. Храните старое значение только в течение контролируемого срока, отведённого на откат, и защищайте его такими же правами доступа. Не оставляйте его копии надолго в каталогах приложения.
После завершения проверки отключите и удалите тестовый юнит, скрипт и тестовые учётные данные, выполните daemon-reload и убедитесь, что временных файлов не осталось. Итогом должна стать воспроизводимая процедура: обычная учётная запись не может прочитать исходный файл, целевая служба читает runtime-копию учётных данных, некорректное значение приводит к явной ошибке, после ротации реальная аутентификация проходит успешно, а в журналах нет чувствительных значений.
Часто задаваемые вопросы
Скрывает ли LoadCredential секреты от администратора?
Нет. Администратор и процессы с достаточными правами могут читать исходный файл или память службы. Механизм уменьшает раскрытие через аргументы, переменные среды и обычные учётные записи, но не защищает секреты от администратора.
Получит ли работающая служба новый секрет после замены файла?
Нет. LoadCredential загружает данные при запуске службы. Нужно атомарно заменить исходный файл, перезапустить службу и проверить аутентификацию с новым секретом. Один daemon-reload не обновляет действующие данные.
Выделенный сервер AMD EPYC 7543P — от $447 в месяц.