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:
- Compose читает файл
./secrets/db_password.txt. - Монтирует его как файл
/run/secrets/db_passwordвнутрь контейнеровpostgresиapp. - Postgres-образ распознаёт
POSTGRES_PASSWORD_FILE(специально поддержанный паттерн) — читает значение оттуда. - Приложение
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 vars | secrets |
|---|---|---|
Видны в docker inspect | да | только имя secret’а, не значение |
Видны в /proc/<pid>/environ | да | нет (читаются только через файл) |
| Попадают в логи приложений | часто (через os.environ) | редко (нужно явно читать файл) |
| Попадают в Git через compose.yml | если хардкод — да | нет, потому что в YAML только имя |
| Поддерживаются Swarm / k8s | да | да (через secret-resources) |
Образы, которые умеют _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? Несколько причин:
- Можно использовать в swarm-кластере, где bind path не существует на всех node’ах.
- Помечается в
docker inspectкак «config», а не «random bind» — лучше читается. - Совместимая семантика с
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-файлах.
Для 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
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.