Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Як правильно управляти секретами (паролями, ключами) у Docker?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Три шари «не клади secret у звичайні місця»:** BuildKit `--mount=type=secret` для build-time, Docker Swarm secrets (file-mounted) для runtime, external-manager (Vault, AWS Secrets Manager) для прода на масштабі. **Ніколи не використовуй ENV-змінні або `--build-arg` для secret.** ```dockerfile RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm ci ``` ```yaml services: api: secrets: [db_password] secrets: db_password: external: true ``` **Головне:** secret в env витікають через `inspect`, image-history, ps, логи. Secret, mounted як файли, існують лише у tmpfs при runtime. Для multi-service проду external secret-manager (Vault, cloud) це єдина прийнятна відповідь.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Управління 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.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.