Як правильно управляти секретами (паролями, ключами) у Docker?
Управління secret у Docker це шарова проблема: build-time secret, runtime secret і «як не вписувати password у YAML-файл». Кожен шар має правильний tool; використання неправильного це як паролі опиняються на Pastebin.
Теорія
TL;DR
Три окремі проблеми, три відповіді:
- Build-time secret (npm-токени, SSH-ключі приватних repo, registry-credentials): BuildKit secret-mount (
--mount=type=secret). - Runtime-secret (DB-паролі, API-ключі): mount як файли через Swarm secret, K8s secret або
docker run --secret. - Єдине джерело правди на масштабі: external secret-manager (HashiCorp Vault, AWS Secrets Manager, GCP Secret Manager, Azure Key Vault). Container fetch'ять при старті або через sidecar.
Заборонені патерни: secret в ENV, secret у --build-arg, secret запечені у image-шари.
Чому ENV неправильно
docker run -e DB_PASSWORD=hunter2 myappВитікає через:
docker inspect <container>— повний env у plain text.ps auxeна host — будь-хто з shell-доступом.- Власний
/proc/1/environcontainer — читається зсередини. - App startup-логи — багато фреймворків логують повний env.
- Process-listings всередині container.
- Crash-dumps — env може опинитися у core-файлах.
Пройди через будь-що з цього, і пароль сидить там.
Build-time secret через BuildKit
# syntax=docker/dockerfile:1.7
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN \
npm ci
COPY . .docker buildx build --secret id=npmrc,src=$HOME/.npmrc -t myapp ..npmrc змонтовано у RUN-крок, але ніколи не приземляється у жоден шар. docker history не показує сліду.
Порівняй з поганим патерном:
# НЕПРАВИЛЬНО: токен у image-history назавжди
ARG NPM_TOKEN
RUN echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/.npmrc && npm cidocker history --no-trunc показує літеральний RUN-рядок, включно з value токена.
Runtime-secret через Swarm
# Створи secret (один раз)
echo "hunter2" | docker secret create db_password -
# Reference у сервісі
docker service create \
--name api \
--secret db_password \
myappВсередині container:
/run/secrets/db_password # вміст: hunter2
Secret mounted як файл у /run/secrets/<name> на tmpfs (RAM-only, ніколи на disk). App читає файл при старті:
with open('/run/secrets/db_password') as f:
password = f.read().strip()Без env-змінної, без inspectable-string. Всередині container лише running-процес може читати tmpfs-файл.
Compose з secret
version: '3.9'
services:
api:
image: myapp
secrets:
- db_password
- api_key
db:
image: postgres:16
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/db_password # postgres image підтримує _FILE-патерн
secrets:
- db_password
secrets:
db_password:
file: ./secrets/db_password.txt
api_key:
external: trueБагато офіційних image (postgres, mysql, mariadb) підтримують конвенцію _FILE env-var: POSTGRES_PASSWORD_FILE=/run/secrets/db_password замість POSTGRES_PASSWORD=.... Бери цей патерн.
Production: external secret-manager
На масштабі ні «файл у repo», ні «docker secret create з CLI» не виживають. Відповідь: справжній secret-manager:
HashiCorp Vault
# App fetch'ить при старті через Vault sidecar або вбудований SDK
vault read secret/data/db/password
# АБО бери agent-injector патерн у K8sAWS Secrets Manager / Parameter Store
import boto3
client = boto3.client('secretsmanager')
response = client.get_secret_value(SecretId='prod/db/password')
password = response['SecretString']GCP Secret Manager / Azure Key Vault — схожі API.
App автентифікується через workload-identity (IAM-роль, service-account), без статичних credentials ніде, крім cloud IAM.
Sidecar / init патерн
services:
vault-init:
image: vault
command: ["vault", "agent", "-config=/etc/vault/agent.hcl"]
volumes:
- vault-secrets:/secrets
api:
image: myapp
volumes:
- vault-secrets:/secrets:ro
depends_on:
vault-init:
condition: service_healthyVault Agent fetch'ить secret, пише у tmpfs-volume. App читає з /secrets/. App ніколи не автентифікується до Vault напряму.
Типові помилки
Secret в env
Згадано. Найпоширеніше, найбільш leaked.
Build-args використано як secret
ARG DB_PASSWORD
RUN echo "$DB_PASSWORD" > /etc/myapp/db_passworddocker history показує build-arg value назавжди. Layer-content включає файл. Два leak за ціною одного.
Secret закомічено у git як secrets.yaml
Класична помилка. Навіть якщо видалив файл пізніше, git-history його має. Бери git-secrets, gitleaks або pre-commit hooks, щоб запобігти commit, не просто прибирати після.
Розшифровка при старті з hardcoded-ключем
encrypted_password = config['DB_PASSWORD']
key = 'this-is-the-key' # hardcoded → насправді не secret
password = decrypt(encrypted_password, key)Якщо ключ у image, шифрування це театр. Правильний патерн: ключ приходить з runtime-середовища (KMS, IAM, sidecar), encrypted blob at rest нормально.
Логування env при старті
log.info(f"Starting with config: {os.environ}")
# Secret в env тепер також у логахWhitelist log-поля, ніколи blanket-log env. Бери sanitizer, що фільтрує ключі, що збігаються з *PASSWORD*, *TOKEN*, *KEY*.
Реальні архітектури
Маленька команда / single host
- Build-secret: BuildKit secret-mount.
- Runtime: Compose
secrets:з файлами на disk, gitignored. - Прийнятно для staging; ризиково для проду (залежить від threat-моделі).
Середня команда / Swarm
- Build-secret: BuildKit, годовано з CI.
- Runtime: Swarm secret (
docker secret create). - Audit: Docker secret CLI показує, що існує; ротація через перестворення.
Production / Kubernetes / multi-cluster
- Vault або cloud-native secret-manager.
- App fetch'ить при старті через SDK з workload-identity auth.
- Або Vault Agent sidecar, що пише у tmpfs.
- Auto-rotation: short-lived dynamic-credentials (Vault DB-engine).
Питання для поглиблення
Q: Чи Docker Swarm secret шифровані at rest?
A: Так, у raft-store на manager. Розшифровуються при use-time і монтуються як tmpfs (у RAM, без disk-write всередині container).
Q: Чи можу ротувати Swarm-secret?
A: Не in-place. Створи db_password_v2, update сервіс на новий, видали старий. Більшість app треба рестартити для підхоплення нових secret, дизайнь під це.
Q: Що таке _FILE патерн?
A: Конвенція, популяризована офіційними Docker-image: замість MY_VAR=value приймай MY_VAR_FILE=/path/to/file і читай value з файлу. Postgres, MySQL, MariaDB, RabbitMQ всі підтримують. Файл може бути Swarm secret-mount.
Q: Як обробляти secret у CI?
A: Secret-store CI (GitHub Actions secrets, GitLab masked-variables, AWS Secrets Manager, pulled через OIDC-role). Передавай у BuildKit через --secret. Ніколи не комітимо, ніколи не логуємо.
Q: (Senior) Як спроектувати secret-management для multi-team, multi-cluster прод-середовища?
A: Vault як source-of-truth. CI-pipeline кожного сервісу пише свої secret через Vault API при deploy-time. Кожен кластер має Vault Agent sidecar-патерн (або external-secrets-operator на K8s). App автентифікуються через workload-identity (K8s service-account, IAM-role, AppRole). Secret-ротація: dynamic-engine (DB-credentials regenerated кожен request, lifespan хвилини). Audit: кожне secret-read залогований у Vault audit-log. Recovery: Vault unseal-процедура задокументована, key-shares розподілено через Shamir Secret Sharing. Поінт: розробники ніколи не бачать static-password; CI/CD ніколи не бачить його у plaintext поза Vault; нічого у image або env будь-якого container не є чутливим.
Приклади
BuildKit secret для npmrc
# syntax=docker/dockerfile:1.7
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN \
npm ci
COPY . .# GitHub Actions
- uses: docker/build-push-action@v5
with:
secrets: |
npmrc=${{ secrets.NPMRC }}Жодного .npmrc у image-history; cache-mount тримає install швидкими.
Compose з _FILE патерном
version: '3.9'
services:
postgres:
image: postgres:16
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
secrets: [db_password]
api:
image: myapp
environment:
DB_PASSWORD_FILE: /run/secrets/db_password
secrets: [db_password]
secrets:
db_password:
file: ./secrets/db_password.txtApp читає файл при старті; postgres image нативно розуміє _FILE. Той самий secret розподілено між двома сервісами.
Vault Agent sidecar
# K8s pod з vault-agent sidecar
apiVersion: v1
kind: Pod
metadata:
annotations:
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/role: "myapp"
vault.hashicorp.com/agent-inject-secret-db: "secret/data/myapp/db"
spec:
containers:
- name: app
image: myapp
# /vault/secrets/db створено injected agent-sidecarApp читає /vault/secrets/db. Без Vault SDK у app-коді. Vault ротує credentials; agent оновлює файл in-place.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.
Коментарі
Ще немає коментарів