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

Пароль базы данных, API-ключ или токен из переменной окружения может оказаться в логе CI, дампе процесса, резервной копии виртуальной машины или выводе команды для диагностики. Поэтому проблема не в самом формате KEY=value, а в том, кто и когда получает доступ к значению. Ниже — практические приёмы для Linux с systemd 255+ и Docker Engine 26.x: от минимальной защиты файла до передачи секрета через отдельное хранилище.
Сначала определите, откуда секрет может утечь
Переменная окружения доступна процессу и обычно передаётся его дочерним процессам. На Linux сведения о запущенном процессе могут быть видны через /proc/<pid>/environ пользователю с достаточными правами. Это не означает, что любой пользователь сервера сразу прочитает пароль, но расширяет круг мест, которые нужно защищать.
- Командная строка. Не передавайте секрет как аргумент вроде
--password=...: он может попасть в список процессов, историю shell или журнал запуска. - Логи. Опасны отладочный вывод конфигурации, исключения с полным окружением и middleware, который записывает заголовки запросов.
- CI/CD. Секрет может раскрыться при включённом debug-режиме, неудачном masking или публикации артефактов сборки.
- Резервные копии. Файл
.env, каталог деплоя и снимок диска часто копируются вместе с приложением. - Тесты и дампы. Core dump, heap dump и диагностический архив могут содержать уже расшифрованное значение.
Переменная окружения — способ доставки секрета процессу, а не система управления секретами.
Секрет — конфиденциальное значение, раскрытие которого даёт доступ к данным или операции: пароль, приватный ключ, токен, строка подключения. Публичный идентификатор проекта или адрес сервиса секретом не считается, даже если он хранится рядом с секретами.
Три рабочих подхода
| Подход | Как приложение получает значение | Плюсы | Цена и риск |
|---|---|---|---|
| Файл с правами 0600 | Сервис читает локальный файл при запуске | Просто внедрить на одном сервере | Нужно отдельно защищать файл, бэкапы и ротацию |
| systemd credentials | systemd передаёт файл во временный каталог credentials | Секрет не нужно класть в командную строку и общий env | Приложение должно уметь читать значение из файла |
| Docker Swarm secrets | Docker монтирует секрет в контейнер как файл | Секрет не записывается в образ и не обязан быть env-переменной | Нужен Swarm, а доступ контролируется на уровне сервиса |
Если приложение принимает только переменные окружения, файл с правами 0600 — допустимый промежуточный вариант. Если код можно изменить, лучше добавить чтение секрета из файла и использовать systemd credentials или Docker secrets.
Минимальная защита на сервере с systemd
Этот вариант подходит для одного Linux-сервера и приложения, которое пока не умеет работать с менеджером секретов. Пример ниже рассчитан на systemd 255+ и предполагает, что сервис запускается от отдельного пользователя myapp.
Вариант с закрытым EnvironmentFile
- Создайте отдельный каталог. Не храните секрет рядом с исходниками и каталогом, который раздаёт веб-сервер.
- Запишите значения в файл с закрытыми правами. В файле не должно быть кавычек вокруг значения без необходимости, пробелов вокруг знака равенства и комментариев с настоящими ключами.
- Подключите файл в unit. Передавайте имя файла через
EnvironmentFile=, а не через аргументыExecStart=. - Проверьте права и перезапустите сервис. Убедитесь, что приложение стартует, но не выводит конфигурацию в журнал.
# Подготовка каталога и файла
sudo install -d -o root -g myapp -m 0750 /etc/myapp
sudo install -o root -g myapp -m 0640 /dev/null /etc/myapp/app.env
# Содержимое /etc/myapp/app.env:
# DATABASE_URL=YOUR_DATABASE_URL
# PAYMENT_API_KEY=YOUR_API_KEY
# Фрагмент /etc/systemd/system/myapp.service
[Service]
User=myapp
Group=myapp
EnvironmentFile=/etc/myapp/app.env
ExecStart=/usr/local/bin/myapp
Restart=on-failure
После изменения unit выполните sudo systemctl daemon-reload и sudo systemctl restart myapp. Проверка результата: команда sudo systemctl status myapp показывает активный сервис, а sudo journalctl -u myapp не содержит значения ключа.
Группа myapp здесь нужна не для удобства, а для ограничения чтения. Если сервису файл не нужен после старта, можно сделать владельцем файла самого пользователя приложения и убрать доступ группы.
Лучше: systemd LoadCredential
В systemd 255+ секрет можно передать как credential-файл. Приложение читает его из каталога, путь к которому systemd предоставляет через специальный параметр окружения. Это не делает секрет бессмертным: процесс всё равно может прочитать файл, а значение может попасть в память приложения. Но оно не лежит в обычном EnvironmentFile и не появляется в аргументах запуска.
# Файл /etc/myapp/database-password
YOUR_DATABASE_PASSWORD
# В unit-файле:
[Service]
User=myapp
LoadCredential=database-password:/etc/myapp/database-password
ExecStart=/usr/local/bin/myapp
Restart=on-failure
Приложение должно открыть credential-файл по пути из переменной $CREDENTIALS_DIRECTORY/database-password. Если конкретная программа не поддерживает такой режим, не подставляйте путь вслепую: проверьте документацию приложения или оставьте вариант с EnvironmentFile.
Секреты в Docker: не путайте env и secrets
Переменная окружения контейнера удобна для настроек, но она остаётся частью окружения процесса. Не записывайте секрет в Dockerfile: слой образа, история сборки или кэш могут сохранить значение даже после удаления строки из финального файла.
В Docker Swarm secrets монтируется в контейнер как файл, обычно под путём /run/secrets/<имя>. Приложение читает этот файл при старте. Сам секрет не нужно добавлять в образ и не нужно прокидывать через environment:. Для одиночного Docker-хоста проверьте, поддерживает ли выбранный оркестратор тот же механизм: Docker Compose и Swarm имеют разные модели управления секретами.
# Схематичный фрагмент чтения секрета в shell-приложении
SECRET_FILE=/run/secrets/database_password
if [ ! -r "$SECRET_FILE" ]; then
echo "secret file is not readable" >&2
exit 1
fi
DATABASE_PASSWORD=$(cat "$SECRET_FILE")
exec /usr/local/bin/myapp
Этот фрагмент не создаёт секрет и не заменяет настройку Docker-сервиса. Он показывает только контракт приложения: значение читается из файла, а при его отсутствии процесс завершается, вместо запуска с пустым паролем.
Что ломается на практике
Секрет попал в Git
Удаление файла из рабочей ветки не удаляет его из истории. Немедленно отзовите или замените ключ у владельца сервиса, затем очистите историю репозитория по правилам выбранного инструмента. Маскирование в CI не отменяет ротацию: опубликованный секрет нужно считать скомпрометированным.
Секрет виден в логах
Проверьте сериализацию настроек, трассировщики HTTP и обработчики ошибок. Маскируйте значения по именам полей, но не рассчитывайте только на список вроде PASSWORD: строка подключения может называться DATABASE_URL.
Приложение не видит новый ключ
Большинство процессов читают окружение при запуске. После ротации нужно перезапустить сервис или использовать механизм динамического обновления, если его поддерживает конкретное приложение. Проверяйте не значение ключа, а факт загрузки: код ответа, успешное подключение и сообщение без раскрытия секрета.
Права выглядят правильными, но доступ всё равно есть
Проверьте владельца файла, группу, ACL, права на родительские каталоги, резервные копии и пользователя, под которым работает агент мониторинга. Команда ls -l показывает не всю модель доступа, если на сервере используются ACL или контейнерные монтирования.
Когда какой способ выбирать
- Один сервер и простое приложение. Начните с закрытого файла и отдельного системного пользователя. Добавьте ротацию и контроль резервных копий.
- Приложение можно изменить. Перейдите с env на чтение файла через systemd credentials. Так проще не раскрывать секрет инструментам, которые перечисляют окружение.
- Несколько контейнеров в Swarm. Используйте Docker secrets и выдавайте секрет только тому сервису, которому он нужен.
- Несколько окружений и частая ротация. Нужен внешний менеджер секретов с аудитом, разграничением ролей и короткоживущими учётными данными. Его конкретный клиент и политика зависят от облака или инфраструктуры.
Не смешивайте секреты и обычные настройки без причины. Регион, порт и уровень логирования можно хранить как конфигурацию. Пароли, приватные ключи и токены должны иметь владельца, срок действия и процедуру отзыва.
Что сделать дальше
Составьте карту секретов: имя, сервис-получатель, источник, срок действия, процедура ротации и места, где значение может попасть в лог или бэкап. Затем проверьте тестовое окружение командой поиска по артефактам и журналам, не выводя сами значения. В проде следите за неуспешными обращениями к секрет-хранилищу, перезапусками после ротации и неожиданным чтением файлов. Следующий практический шаг — убрать секреты из Dockerfile, Git и аргументов процессов, а затем перевести самые критичные из них на файловую передачу через systemd или Docker secrets.
Редактор изображений: Убрать лишнее, заменить фон, добавить объект, расширить кадр — на вашем изображении.
Спросить за 20 ₽Источники: OWASP Secrets Management Cheat Sheet, Docker Docs: Manage sensitive data with Docker secrets
Частые вопросы
Переменные окружения действительно небезопасны?
Можно ли хранить файл .env на сервере?
Что делать, если секрет уже попал в Git или лог?
Нужно ли перезапускать приложение после смены переменной окружения?
Что делать, если у меня другая версия systemd или Docker?
Какие подводные камни важны в проде?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.