MAATRIX / Блог / Docker secrets: управление паролями

Docker secrets: управление паролями

Docker secrets: управление паролями

MAATRIX

Если пароль от базы данных лежит в environment: вашего docker-compose.yml, он не спрятан — он просто не виден невооружённым глазом. Любой, у кого есть доступ к докеру на сервере, достанет его одной командой docker inspect. Ниже — почему так происходит, и как хранить секреты так, чтобы их действительно нужно было украсть, а не просто прочитать.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →

Почему пароль в environment variable — это не защита

Переменные окружения контейнера — это не секрет, а метаданные, которые Docker хранит открытым текстом и отдаёт по первому запросу. Проверяется это за 10 секунд на любом рабочем сервере:

docker inspect my_postgres --format='{{json .Config.Env}}'

Команда выведет весь список Env, включая POSTGRES_PASSWORD=..., если он был передан через environment: или -e. Доступ к сокету Docker (а он есть у всех, кто может запускать docker без sudo через группу docker) равен доступу ко всем паролям на хосте.

Дальше по списку — ещё три места утечки:

  • История процессов. Пароль в docker run -e PASSWORD=hunter2 на секунду-другую виден в ps aux любому локальному пользователю.
  • Логи и мониторинг. Агенты мониторинга часто собирают docker inspect со всех контейнеров для дашбордов — и переменные окружения оседают в системах логирования с более широким доступом на чтение, чем сам сервер.
  • История шелла. Пароль в командной строке остаётся в ~/.bash_history или ~/.zsh_history.
  • Coredump. Переменные окружения процесса попадают в core dump и в /proc/<pid>/environ, доступный при определённых правах любому, кто дотянулся до хоста.

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

Docker secrets: что это и как работает в Swarm

Docker secrets — встроенный механизм, спроектированный специально для этой задачи, но по умолчанию он доступен только в режиме Docker Swarm. Секрет создаётся один раз, шифруется в хранилище Swarm (Raft-лог менеджеров) и монтируется в контейнер как файл в tmpfs — оперативную память, не диск, — по пути /run/secrets/<имя>.

Создание секрета из строки или файла:

echo -n "SuperSecretPass123" | docker secret create db_password -
docker secret create ssl_cert ./cert.pem

Список и удаление:

docker secret ls
docker secret rm db_password

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

docker service create \
  --name myapp \
  --secret db_password \
  --secret source=ssl_cert,target=tls.pem \
  myimage:latest

Внутри контейнера появится файл /run/secrets/db_password с содержимым пароля и правами 0444, читаемыми только процессом внутри контейнера. Он никогда не попадает в docker inspect, не логируется и не остаётся на диске хоста — при остановке сервиса tmpfs очищается.

Важное ограничение: docker secret create и монтирование /run/secrets работают только для сервисов, запущенных через docker service create или docker stack deploy в инициализированном Swarm-кластере (docker swarm init). Обычный docker-compose up без Swarm — а это большинство небольших VPS-проектов — секреты Swarm напрямую не получит. Если вы уже используете Swarm, полезно свериться с частыми ошибками Docker Swarm на сервере — там разбираются похожие грабли с сетями и правами.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать VPS

Secrets в обычном docker-compose без Swarm

Хорошая новость: секция secrets: в docker-compose.yml работает и без Swarm, начиная с формата файла версии 3.1+. Механизм отличается от «настоящих» Swarm-секретов — Compose просто монтирует указанный файл в /run/secrets/<имя> как volume, без шифрования в отдельном хранилище. Но это уже кардинально лучше, чем environment:, потому что:

  • содержимое файла не попадает в docker inspect;
  • файл не виден в ps aux и истории шелла;
  • права на файл на хосте вы контролируете сами.

Пример для PostgreSQL, который умеет читать пароль из файла через суффикс _FILE (этот паттерн поддерживают официальные образы postgres, mysql, mongo, redis с современными entrypoint-скриптами):

services:
  db:
    image: postgres:16
    environment:
      POSTGRES_USER: appuser
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
      POSTGRES_DB: appdb
    secrets:
      - db_password
    volumes:
      - pgdata:/var/lib/postgresql/data

secrets:
  db_password:
    file: ./secrets/db_password.txt

volumes:
  pgdata:

Если ваш образ не поддерживает _FILE-конвенцию (у самописных приложений это обычная ситуация), придётся читать файл вручную внутри приложения при старте — это буквально open('/run/secrets/db_password').read().strip(), а не что-то экзотическое. Такой подход стоит закладывать сразу, когда вы проектируете конфигурацию сервиса для продакшен docker-compose на VPS.

Практическая реализация: файлы с ограниченными правами

Собираем рабочую схему для одиночного сервера без Swarm — это самый частый случай на VPS.

1. Структура каталогов и права.

mkdir -p /opt/myapp/secrets
chmod 700 /opt/myapp/secrets

2. Секреты — по одному файлу на значение, без переносов строк в конце.

echo -n "SuperSecretPass123" > /opt/myapp/secrets/db_password.txt
echo -n "sk-live-xxxxxxxxxxxx" > /opt/myapp/secrets/api_token.txt
chmod 600 /opt/myapp/secrets/*.txt
chown root:root /opt/myapp/secrets/*.txt

Флаг -n в echo важен: без него в файл попадёт лишний символ переноса строки, который потом окажется частью пароля и сломает аутентификацию — классическая причина «пароль правильный, а подключиться не могу».

3. Каталог секретов — обязательно в .gitignore и вне бэкапов кода.

# .gitignore
secrets/
.env
*.pem

Это не опция, а обязательное условие: секрет, один раз попавший в git-историю, считается скомпрометированным навсегда, даже если коммит потом удалить — история восстанавливается.

4. docker-compose.yml ссылается на файлы, не на значения.

services:
  app:
    build: .
    secrets:
      - db_password
      - api_token
    environment:
      DB_PASSWORD_FILE: /run/secrets/db_password
      API_TOKEN_FILE: /run/secrets/api_token

secrets:
  db_password:
    file: /opt/myapp/secrets/db_password.txt
  api_token:
    file: /opt/myapp/secrets/api_token.txt

5. Если приложение обязательно хочет переменную окружения, а не файл, читайте файл в entrypoint-скрипте и экспортируйте значение только внутри контейнера, не наружу:

#!/bin/sh
export DB_PASSWORD="$(cat /run/secrets/db_password)"
exec "$@"

Пароль всё равно попадёт в environ процесса внутри контейнера — от этого не уйти, если библиотека требует именно переменную, — но он не окажется в docker inspect хоста и не утечёт через логирование конфигурации compose-файла.

Если на сервере несколько человек с SSH-доступом, дополнительно закрутите гайки на самом SSH — двухфакторка снижает риск, что до этих файлов вообще кто-то доберётся: двухфакторная аутентификация SSH на VPS.

Ротация паролей без простоя

Секрет, который никогда не меняется, рано или поздно становится общедоступным — через бывшего сотрудника, скомпрометированный ноутбук разработчика или забытый staging-сервер. Ротация должна быть рутиной, а не разовым проектом после инцидента.

Порядок действий для файлового подхода:

# 1. Генерируем новое значение
NEW_PASS=$(openssl rand -base64 24)

# 2. Меняем пароль в самой СУБД, не только в файле
docker exec -it myapp_db psql -U appuser -c \
  "ALTER USER appuser WITH PASSWORD '${NEW_PASS}';"

# 3. Обновляем файл секрета
echo -n "$NEW_PASS" > /opt/myapp/secrets/db_password.txt
chmod 600 /opt/myapp/secrets/db_password.txt

# 4. Перезапускаем только зависимые сервисы
docker compose up -d --force-recreate app

В Swarm ротация чуть аккуратнее: секрет нельзя обновить in-place, нужно создать новый под другим именем и переключить на него сервис, после чего удалить старый:

echo -n "$NEW_PASS" | docker secret create db_password_v2 -
docker service update \
  --secret-rm db_password \
  --secret-add source=db_password_v2,target=db_password \
  myapp
docker secret rm db_password

Такое разделение по версиям (_v2, _v3) — это официально рекомендуемый Docker паттерн, потому что секрет, подключённый к работающему сервису, удалить нельзя, а мгновенно подменить его содержимое механизм не позволяет намеренно — это защита от гонки состояний при обновлении.

Заведите себе напоминание на ротацию раз в 90 дней для боевых паролей и сразу — при увольнении любого, у кого был доступ к серверу или репозиторию с конфигами.

Когда пора переходить на HashiCorp Vault

Файловый подход из предыдущих разделов закрывает 90% реальных задач на одиночном VPS или небольшом кластере: секреты не в git, не в docker inspect, не в истории команд. Но у него есть потолок — секреты статичны, лежат на диске (пусть и с правами 600), и ротация — это скрипт, который вы должны не забыть запустить.

HashiCorp Vault решает следующий уровень задач:

  • Динамические секреты — Vault сам создаёт временного пользователя БД с TTL на час и отзывает его автоматически, вместо одного статичного пароля на все времена.
  • Централизованный аудит — кто, когда и какой секрет запросил, фиксируется в лог Vault, а не разбросано по серверам.
  • Автоматическая ротация и short-lived токены вместо ручных скриптов из раздела выше.
  • Интеграция с контейнерами через Vault Agent Sidecar (Kubernetes) или через vault agent с шаблоном, который сам пишет актуальный секрет в файл на хосте, откуда его подхватывает docker-compose тем же способом, что описан выше.

Минимальный пример получения секрета из Vault для локального теста:

vault kv put secret/myapp/db password="SuperSecretPass123"
vault kv get -field=password secret/myapp/db

Разворачивание Vault — отдельная инфраструктурная задача: нужен сам сервер Vault, unseal-ключи, политики доступа (ACL), TLS между Vault и клиентами и продуманная схема хранения ключей разблокировки самого Vault (секретное хранилище тоже нужно защищать). Для одного-двух проектов на VPS это обычно избыточно; переходить на Vault стоит, когда секретов и сервисов становится десятки, а не единицы. Если задача попроще — хранить пароли для личного использования, — присмотритесь к self-hosted менеджеру паролей вроде Vaultwarden на VPS: это не про секреты контейнеров, но закрывает смежную боль с паролями людей.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать VPS

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Можно ли использовать docker secrets без Docker Swarm вообще?

Да, через секцию secrets: в docker-compose с источником file: — она монтирует файл в /run/secrets/ без Swarm. Полноценное шифрованное хранилище секретов Swarm (Raft-лог) в этом режиме недоступно, но утечка через docker inspect и логи всё равно закрывается.

Приложение не поддерживает чтение пароля из файла, только переменную окружения. Что делать?

Читайте файл в entrypoint-скрипте контейнера и экспортируйте переменную только внутри него: export VAR="$(cat /run/secrets/имя)". Это не так надёжно, как нативная поддержка файлов, но полностью убирает секрет из docker inspect хоста.

Секрет утёк в git-историю. Ротации файла достаточно?

Нет. Считайте пароль скомпрометированным навсегда, даже после удаления коммита — историю можно восстановить. Меняйте сам пароль в СУБД/сервисе, а не только файл, и проверьте, не остались ли форки или клоны репозитория со старой историей.

Чем secrets отличаются от простого volume с файлом пароля?

Технически в non-Swarm режиме — почти ничем, это тот же bind-mount под другим синтаксисом. Разница появляется в Swarm: секреты хранятся в зашифрованном Raft-логе, монтируются в tmpfs (не на диск) и не реплицируются на узлы, где не запущен использующий их сервис.

Нужно ли шифровать сам файл секрета на диске?

Если сервер не полностью под вашим контролем или вы храните бэкапы диска у третьей стороны — да, имеет смысл держать /opt/myapp/secrets на зашифрованном разделе (LUKS) или использовать Vault. Для одиночного VPS с правами 600 и root-only доступом это чаще избыточно, но зависит от вашей модели угроз.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →