Skip to main content

Як правильно управляти секретами (паролями, ключами) у Docker?

Управління secret у Docker це шарова проблема: build-time secret, runtime secret і «як не вписувати password у YAML-файл». Кожен шар має правильний tool; використання неправильного це як паролі опиняються на Pastebin.

Теорія

TL;DR

Три окремі проблеми, три відповіді:

  1. Build-time secret (npm-токени, SSH-ключі приватних repo, registry-credentials): BuildKit secret-mount (--mount=type=secret).
  2. Runtime-secret (DB-паролі, API-ключі): mount як файли через Swarm secret, K8s secret або docker run --secret.
  3. Єдине джерело правди на масштабі: 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 неправильно

bash
docker run -e DB_PASSWORD=hunter2 myapp

Витікає через:

  • docker inspect <container> — повний env у plain text.
  • ps auxe на host — будь-хто з shell-доступом.
  • Власний /proc/1/environ container — читається зсередини.
  • App startup-логи — багато фреймворків логують повний env.
  • Process-listings всередині container.
  • Crash-dumps — env може опинитися у core-файлах.

Пройди через будь-що з цього, і пароль сидить там.

Build-time secret через BuildKit

dockerfile
# syntax=docker/dockerfile:1.7 FROM node:22-alpine WORKDIR /app COPY package*.json ./ RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \ npm ci COPY . .
bash
docker buildx build --secret id=npmrc,src=$HOME/.npmrc -t myapp .

.npmrc змонтовано у RUN-крок, але ніколи не приземляється у жоден шар. docker history не показує сліду.

Порівняй з поганим патерном:

dockerfile
# НЕПРАВИЛЬНО: токен у image-history назавжди ARG NPM_TOKEN RUN echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/.npmrc && npm ci

docker history --no-trunc показує літеральний RUN-рядок, включно з value токена.

Runtime-secret через Swarm

bash
# Створи 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 читає файл при старті:

python
with open('/run/secrets/db_password') as f: password = f.read().strip()

Без env-змінної, без inspectable-string. Всередині container лише running-процес може читати tmpfs-файл.

Compose з secret

yaml
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

bash
# App fetch'ить при старті через Vault sidecar або вбудований SDK vault read secret/data/db/password # АБО бери agent-injector патерн у K8s

AWS Secrets Manager / Parameter Store

python
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 патерн

yaml
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_healthy

Vault Agent fetch'ить secret, пише у tmpfs-volume. App читає з /secrets/. App ніколи не автентифікується до Vault напряму.

Типові помилки

Secret в env

Згадано. Найпоширеніше, найбільш leaked.

Build-args використано як secret

dockerfile
ARG DB_PASSWORD RUN echo "$DB_PASSWORD" > /etc/myapp/db_password

docker history показує build-arg value назавжди. Layer-content включає файл. Два leak за ціною одного.

Secret закомічено у git як secrets.yaml

Класична помилка. Навіть якщо видалив файл пізніше, git-history його має. Бери git-secrets, gitleaks або pre-commit hooks, щоб запобігти commit, не просто прибирати після.

Розшифровка при старті з hardcoded-ключем

python
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 при старті

python
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

dockerfile
# syntax=docker/dockerfile:1.7 FROM node:22-alpine WORKDIR /app COPY package*.json ./ RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \ --mount=type=cache,target=/root/.npm \ npm ci COPY . .
yaml
# GitHub Actions - uses: docker/build-push-action@v5 with: secrets: | npmrc=${{ secrets.NPMRC }}

Жодного .npmrc у image-history; cache-mount тримає install швидкими.

Compose з _FILE патерном

yaml
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.txt

App читає файл при старті; postgres image нативно розуміє _FILE. Той самий secret розподілено між двома сервісами.

Vault Agent sidecar

yaml
# 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-sidecar

App читає /vault/secrets/db. Без Vault SDK у app-коді. Vault ротує credentials; agent оновлює файл in-place.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Коментарі

Ще немає коментарів