Learning Platform
Глоссарий Troubleshooting
Урок 13.04 · 22 мин
Средний
dockercomposesecretsconfigssecurity

Secrets и configs: безопасная передача паролей

В compose-стенде секреты по умолчанию приходят через env vars. Это удобно, но небезопасно: переменные среды видны в docker inspect любому, кто имеет доступ к docker socket, плюс попадают в логи при docker compose config. Compose даёт лучший механизм — secrets:. Файл с секретом монтируется в /run/secrets/<name> и читается приложением оттуда. В этом уроке разберём, как это работает, чем отличается от env, и зачем существуют configs:.


Secrets: типы, base64 vs encryption, etcd encryption at rest

Почему env vars — плохо для секретов

services:
  app:
    image: myapp
    environment:
      DB_PASSWORD: super-secret-pass

Любой человек с доступом к Docker socket может видеть:

docker inspect myproj-app-1 | grep -A 5 Env
# "Env": [
#   "DB_PASSWORD=super-secret-pass",
#   ...
# ]

Дочерние процессы получают переменную через environ(7) — её видно в /proc/<pid>/environ (на Linux, через cat). Логи, которые случайно включают env (например, os.environ в stack trace) — раскрывают значение.

Это не значит «env vars нельзя использовать». Для multiconfig-параметров (LOG_LEVEL, FEATURE_FLAGS) — нормально. Для секретов — нет.


Compose secrets — основа

services:
  postgres:
    image: postgres:17
    environment:
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
    secrets:
      - db_password

  app:
    image: myapp
    secrets:
      - db_password
      - api_key

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

Что произойдёт при docker compose up:

  1. Compose читает файл ./secrets/db_password.txt.
  2. Монтирует его как файл /run/secrets/db_password внутрь контейнеров postgres и app.
  3. Postgres-образ распознаёт POSTGRES_PASSWORD_FILE (специально поддержанный паттерн) — читает значение оттуда.
  4. Приложение app само читает /run/secrets/db_password.

Что лежит в /run/secrets

mkdir -p ./secrets
echo "supersecret123" > ./secrets/db_password.txt

# compose.yml как выше...
docker compose up -d

docker compose exec app sh -c 'ls -la /run/secrets/'
# -r--r--r-- 1 root root 15 May 15 10:30 db_password

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

docker compose exec app cat /run/secrets/db_password
# supersecret123

/run/secrets — это tmpfs (in-memory). Файл не попадает в writable-layer контейнера, на диск хоста — тоже не записывается через docker. Это уровень изоляции выше, чем env vars.


Почему это лучше

Аспектenv varssecrets
Видны в docker inspectдатолько имя secret’а, не значение
Видны в /proc/<pid>/environданет (читаются только через файл)
Попадают в логи приложенийчасто (через os.environ)редко (нужно явно читать файл)
Попадают в Git через compose.ymlесли хардкод — данет, потому что в YAML только имя
Поддерживаются Swarm / k8sдада (через secret-resources)
Жизненный цикл secret'а в compose
./secrets/db_password.txtФайл на хосте. Содержит секрет, в .gitignore. На prod может быть выводом vault-cli или скопирован через ansible-vault
compose читает файлПри docker compose up compose читает содержимое и подсовывает Docker daemon
Docker mount tmpfsDocker создаёт tmpfs-mount /run/secrets/ в контейнере и кладёт файл с правильным именем. RAM-only, не пишется на диск контейнера
App reads fileПриложение само открывает файл и читает. Не через env, не через CLI-флаг. Это safe path
Container stopsПри остановке tmpfs испаряется. Значение секрета никогда не было persisted
docker inspect: только имяdocker inspect показывает 'secret db_password mounted', но не значение. Это можно безопасно копировать в баг-репорт

Образы, которые умеют _FILE суффикс

Многие официальные образы поддерживают паттерн <VAR>_FILE: если переменная имеет суффикс _FILE и указывает на путь — образ читает значение из файла, а не из переменной.

  • Postgres: POSTGRES_PASSWORD_FILE, POSTGRES_USER_FILE, POSTGRES_DB_FILE.
  • MySQL: MYSQL_ROOT_PASSWORD_FILE, MYSQL_PASSWORD_FILE.
  • Redis: REDIS_PASSWORD_FILE (через redis-server подключение).
  • MinIO: MINIO_ROOT_USER_FILE, MINIO_ROOT_PASSWORD_FILE.
services:
  postgres:
    image: postgres:17
    environment:
      POSTGRES_PASSWORD_FILE: /run/secrets/pg_password
    secrets:
      - pg_password

secrets:
  pg_password:
    file: ./secrets/pg_password.txt

Чистый паттерн. Никаких env-vars с секретом. Образ сам всё подтянет.


Своё приложение: как читать secret

Python:

import os
from pathlib import Path

def read_secret(name: str) -> str:
    path = Path(f"/run/secrets/{name}")
    if not path.is_file():
        raise RuntimeError(f"Secret {name} not mounted")
    return path.read_text().strip()

DB_PASSWORD = read_secret("db_password")

Стрипуем whitespace — типичная ошибка: в txt-файл случайно попадает \n в конце, и password не работает. Привычка делать .strip() спасает.

В Java/Go/Node — то же: открыть файл, прочитать, использовать.


external secrets

secrets:
  api_key:
    external: true

external: true означает «этот secret уже существует в Docker daemon, не создавай». Полезно в production со swarm или когда секреты управляются вне compose:

docker secret create api_key /path/to/file
# Создаёт secret в Docker. Доступен из compose с external: true.

В compose standalone (не swarm) external означает «не пытайся прочитать из file:» — но при этом значение должно быть как-то доступно через Docker secret API. На swarm-стэке это работает естественно.


configs — то же, но для не-секретных файлов

configs — близнец secrets, но для несекретных конфигов. Тот же механизм mount’а файла, но без аспекта «прячь от inspect».

services:
  nginx:
    image: nginx:1.27-alpine
    configs:
      - source: nginx_conf
        target: /etc/nginx/conf.d/default.conf

configs:
  nginx_conf:
    file: ./nginx/default.conf

Файл ./nginx/default.conf mount’нется в /etc/nginx/conf.d/default.conf контейнера nginx.

Зачем не просто bind mount? Несколько причин:

  1. Можно использовать в swarm-кластере, где bind path не существует на всех node’ах.
  2. Помечается в docker inspect как «config», а не «random bind» — лучше читается.
  3. Совместимая семантика с secrets.

Для compose в single-host бои между configs и bind mount нет смысла — bind проще. configs важен в swarm-сценариях.


Реальный пример

services:
  postgres:
    image: postgres:17
    environment:
      POSTGRES_USER: app
      POSTGRES_DB: etl
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
    volumes:
      - pgdata:/var/lib/postgresql/data
    secrets:
      - db_password
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d etl"]
      interval: 5s

  app:
    build: ./app
    depends_on:
      postgres:
        condition: service_healthy
    environment:
      DATABASE_HOST: postgres
      DATABASE_USER: app
      DATABASE_NAME: etl
      DATABASE_PASSWORD_FILE: /run/secrets/db_password
    secrets:
      - db_password
      - api_key

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

volumes:
  pgdata:

./secrets/db_password.txt:

mySupp3rSecur3Pass

(одна строка, без переноса)

# .gitignore:
echo "secrets/*.txt" >> .gitignore

# secrets/.gitkeep — чтобы папка попала в Git как структура
touch secrets/.gitkeep

docker compose up -d
# Никаких паролей в env, никаких паролей в compose.yml. Только в txt-файлах.
TIP

Для production: txt-файлы с секретами в Git, разумеется, не лежат. На prod-сервере их кладут через ansible-vault, sops, или secret manager (Vault, AWS Secrets Manager). Compose тогда просто читает локальный файл, который кто-то заранее подготовил.


Попробуй сам

mkdir -p secrets-demo/secrets && cd secrets-demo

echo "supersecret-pg-password" > secrets/db_password.txt
echo "abc-api-key-12345" > secrets/api_key.txt

cat > compose.yml <<'YAML'
services:
  postgres:
    image: postgres:17
    environment:
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
    secrets:
      - db_password
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 3s

  client:
    image: postgres:17
    depends_on:
      postgres:
        condition: service_healthy
    secrets:
      - db_password
      - api_key
    command: sh -c 'echo "--- secrets ---"; ls -la /run/secrets/; cat /run/secrets/db_password; echo; cat /run/secrets/api_key; echo "--- env ---"; env | grep PASS || echo "no PASS in env"'

secrets:
  db_password:
    file: ./secrets/db_password.txt
  api_key:
    file: ./secrets/api_key.txt
YAML

docker compose up
# client напечатает содержимое secrets, в env пароля НЕТ.

# inspect — увидим, что в env пароля нет, только mount secret.
docker compose ps -q | head -1 | xargs docker inspect | jq '.[0].HostConfig.Mounts'

# Cleanup.
docker compose down
cd .. && rm -rf secrets-demo
WARNING

Compose secrets защищают от случайного leak через docker inspect и env-listing. Они не защищают от человека, у которого есть exec в контейнер — он зайдёт и прочитает /run/secrets/db_password. Для защиты от этого нужен secret manager с rotation и audit (Vault, k8s secrets с RBAC).

В последнем уроке модуля — масштабирование и ресурсы: --scale worker=3, deploy.resources, restart policy.


Проверка знанийKnowledge check
Сравни environment: { DB_PASSWORD: secret } и secrets: [db_password] с файлом в /run/secrets/ . Объясни три различия в безопасности и почему официальные образы Postgres/MinIO предлагают POSTGRES_PASSWORD_FILE как первоклассный путь.
ОтветAnswer
Различия: (1) Видимость в docker inspect: env-vars видны полностью в Config.Env (любой с доступом к docker socket видит секрет). Secrets видны как имя mount'а — значение не показывается. (2) Видимость через /proc/<pid>/environ: env-vars наследуются дочерним процессам и доступны через cat /proc/<pid>/environ (любой пользователь в контейнере видит). Secrets читаются только при явном open() файла — наследования через env нет. (3) Risk of leaking through logs/stack traces: код часто делает logger.debug(f"env: {os.environ}") и логирует пароли. Чтение из файла требует явный path-аргумент — гораздо реже случайно дампится. Постгрес/MinIO предлагают _FILE-суффикс как первоклассный путь, потому что: (а) entrypoint-скрипт стандартизирован: проверяет наличие <VAR>_FILE, читает значение из path, использует; (б) поощряет правильный паттерн "secret = файл, не env"; (в) совместимо с compose secrets, swarm secrets, k8s secrets — везде секрет всё равно mount'ится как файл, _FILE-суффикс универсален.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Почему compose secrets безопаснее, чем env-vars для паролей?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5