Пошук уроків, статей та іншого контенту
Безпечно передаватимете паролі, токени й ключі контейнерам без зберігання секретів у Dockerfile та репозиторії.
До секретів належать:
паролі до баз даних;
токени доступу до API;
приватні ключі;
сертифікати;
ключі підпису та шифрування.
Небезпечні приклади:
FROM node:22-alpine
ENV DATABASE_PASSWORD="super-secret-password"
ARG API_TOKEN="token-value"
COPY . .Такі значення можуть стати доступними через:
історію Dockerfile;
шари образу;
команду docker history;
журнали CI/CD;
кеш складання;
репозиторій, якщо Dockerfile зберігається в системі контролю версій.
Не слід вважати ARG безпечним способом передавання секретів. Значення ARG призначене для параметрів складання, а не для конфіденційних даних.
Docker Secrets передає секрет контейнеру як файл. Типовий шлях у контейнері:
/run/secrets/<назва-секрету>Наприклад:
/run/secrets/db_passwordЗастосунок читає значення з цього файлу під час запуску, але секрет не записується в Dockerfile та не передається як звичайна змінна середовища.
Переваги такого підходу:
секрет не потрапляє до образу;
секрет не потрібно вказувати в команді запуску;
застосунок може прочитати його лише в контейнері, якому секрет призначено;
назву файлу можна передавати через змінну на кшталт DATABASE_PASSWORD_FILE, не розкриваючи саме значення.
Docker Compose дозволяє оголосити секрет у верхньому рівні конфігурації та надати його окремому сервісу.
Створимо структуру проєкту:
project/
├── compose.yaml
├── .gitignore
└── secrets/
└── db_password.txtФайл secrets/db_password.txt містить лише пароль:
local-password-123Додамо каталог із секретами до .gitignore:
secrets/services:
app:
image: alpine:3.20
secrets:
- db_password
environment:
DB_PASSWORD_FILE: /run/secrets/db_password
command:
- sh
- -c
- |
echo "Секрет доступний за шляхом: $DB_PASSWORD_FILE"
echo "Довжина секрету: $(wc -c < "$DB_PASSWORD_FILE")"
secrets:
db_password:
file: ./secrets/db_password.txtУ цьому прикладі:
secrets.db_password.file вказує на локальний файл із секретом;
services.app.secrets надає секрет контейнеру app;
Docker монтує секрет у /run/secrets/db_password;
змінна DB_PASSWORD_FILE містить лише шлях до файлу;
саме значення секрету не записане у compose.yaml.
Запуск:
mkdir -p secrets
printf '%s' 'local-password-123' > secrets/db_password.txt
docker compose up --abort-on-container-exitПриклад очікуваного результату:
Секрет доступний за шляхом: /run/secrets/db_password
Довжина секрету: 17У реальному застосунку не виводьте секрет і його значення в журнали. У прикладі виводиться лише довжина, щоб перевірити доступність файлу.
Багато бібліотек і фреймворків підтримують домовленість із суфіксом _FILE. Наприклад:
services:
app:
image: my-app:latest
secrets:
- db_password
environment:
DATABASE_PASSWORD_FILE: /run/secrets/db_password
secrets:
db_password:
file: ./secrets/db_password.txtЗастосунок має прочитати шлях із DATABASE_PASSWORD_FILE, а потім отримати значення з файлу.
Приклад на Node.js:
import { readFileSync } from "node:fs";
const passwordFile = process.env.DATABASE_PASSWORD_FILE;
if (!passwordFile) {
throw new Error("DATABASE_PASSWORD_FILE is not set");
}
const databasePassword = readFileSync(passwordFile, "utf8").trim();
if (!databasePassword) {
throw new Error("Database password is empty");
}
console.log("Пароль бази даних завантажено");У коді важливо:
перевірити, що шлях до секрету переданий;
прочитати файл під час запуску;
прибрати символ переходу на новий рядок за допомогою trim();
не записувати значення пароля в логи.
Назва Docker Secrets також використовується для секретів у Docker Swarm, але рівень захисту відрізняється.
У локальному Compose файл із секретом існує на хості:
secrets:
db_password:
file: ./secrets/db_password.txtТому потрібно захистити:
сам файл;
права доступу до нього;
резервні копії;
сховище, де запускається Docker;
журнали та команди CI/CD.
Це зручний спосіб локальної розробки, але локальний файл не є централізованим зашифрованим сховищем секретів.
У Swarm секрет створюють окремо:
printf '%s' 'production-password-123' | docker secret create db_password -Перевірити список секретів можна так:
docker secret lsЗастосунок або сервіс отримує секрет лише після явного призначення:
services:
app:
image: my-app:latest
secrets:
- db_password
environment:
DATABASE_PASSWORD_FILE: /run/secrets/db_password
secrets:
db_password:
external: trueУ цьому випадку external: true означає, що Docker Swarm очікує секрет із назвою db_password, створений заздалегідь командою docker secret create.
Розгортання стека:
docker stack deploy -c compose.yaml productionУ Swarm секрети керуються Docker і надаються лише сервісам, яким вони призначені. Контейнер читає їх у тому самому каталозі:
/run/secrets/db_passwordІм’я секрету в Docker та ім’я файлу всередині контейнера можуть відрізнятися:
services:
app:
image: my-app:latest
secrets:
- source: production_database_password
target: db_password
environment:
DATABASE_PASSWORD_FILE: /run/secrets/db_password
secrets:
production_database_password:
file: ./secrets/production_database_password.txtТут:
секрет на рівні Compose називається production_database_password;
усередині контейнера він доступний як /run/secrets/db_password.
Це дає змогу зберігати внутрішню структуру конфігурації незалежною від назви файлу на хості.
Секрети потрібно змінювати, якщо:
працівник із доступом до секрету залишив команду;
токен був випадково опублікований;
змінився пароль зовнішньої системи;
минув термін дії ключа;
є підозра на компрометацію.
Не варто бездумно перезаписувати секрет, на який уже посилаються запущені сервіси. Зручніше створити нову версію:
printf '%s' 'new-production-password' | \
docker secret create db_password_v2 -Потім у конфігурації змінити джерело:
services:
app:
image: my-app:latest
secrets:
- source: db_password_v2
target: db_password
environment:
DATABASE_PASSWORD_FILE: /run/secrets/db_password
secrets:
db_password_v2:
external: trueПісля оновлення сервісу старий секрет можна видалити, якщо жоден сервіс більше його не використовує:
docker secret rm db_passwordДля локального Compose достатньо замінити вміст файлу секрету та перезапустити сервіс:
docker compose up -d --force-recreateОднак сам процес ротації має враховувати поведінку системи, до якої підключається застосунок. Наприклад, база даних може вимагати спочатку створити новий пароль, а вже потім оновити контейнер.
Іноді секрет потрібен не запущеному контейнеру, а процесу складання образу, наприклад для доступу до приватного реєстру пакетів. У такому випадку не використовуйте ARG:
ARG NPM_TOKEN
RUN npm config set //registry.example.com/:_authToken=$NPM_TOKENНавіть якщо після команди видалити файл конфігурації, секрет може залишитися в одному з шарів образу або в кеші складання.
Для Docker BuildKit можна передати секрет лише на час конкретного кроку:
# syntax=docker/dockerfile:1
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN --mount=type=secret,id=npm_token \
sh -c 'NPM_TOKEN="$(cat /run/secrets/npm_token)" && npm ci'
COPY . .Складання:
DOCKER_BUILDKIT=1 docker build \
--secret id=npm_token,src=./secrets/npm_token \
-t private-app .Секрет доступний лише під час виконання відповідного RUN і не має потрапити до фінального образу.
Файл ./secrets/npm_token також потрібно додати до .gitignore.
Перевірте, що секрет не міститься у файлах проєкту:
git grep -n -i -E 'password|token|secret|private.?key' -- ':!secrets/*'Перевірте образ на наявність очевидних секретів:
docker history --no-trunc my-app:latestНе передавайте секрети такими способами:
docker run -e DATABASE_PASSWORD="production-password" my-appdocker run my-app sh -c "connect --password=production-password"Значення можуть бути видимими в історії команд, конфігурації процесів, журналах CI або системах моніторингу.
Натомість передавайте шлях до секрету:
docker run \
--mount type=bind,src="$PWD/secrets/db_password.txt",dst=/run/secrets/db_password,readonly \
-e DATABASE_PASSWORD_FILE=/run/secrets/db_password \
my-appДля локального ручного запуску це працює, але Docker Compose або Docker Swarm зручніше використовувати для декларативного призначення секретів.
Дотримуйтеся таких правил:
не додавайте каталог secrets/ до репозиторію;
не копіюйте секрети через COPY у Dockerfile;
не передавайте секрети в CMD, ENTRYPOINT або аргументах команд;
не виводьте секрети в логи;
не додавайте секрети до повідомлень про помилки;
надавайте секрет лише тому сервісу, якому він потрібен;
не використовуйте один пароль для всіх середовищ;
обмежуйте доступ до локальних файлів із секретами;
не додавайте секрети до тестових дампів і резервних копій без захисту.
Якщо стороння система приймає значення лише через змінну середовища, використовуйте це як вимушений компроміс і контролюйте, щоб значення не з’являлося в логах. Коли застосунок підтримує читання з файлу, краще використовувати /run/secrets/....
ENV або ARGENV API_TOKEN=token-value
ARG DATABASE_PASSWORD=passwordENV залишає значення в конфігурації образу, а ARG не призначений для безпечного зберігання секретів.
Навіть приватний репозиторій не варто використовувати як сховище секретів. Додавання файлу до .gitignore запобігає майбутньому додаванню, але не видаляє секрет із попередніх комітів.
Якщо секрет уже потрапив до репозиторію:
негайно відкличте або замініть його;
перевірте журнали доступу;
видаліть секрет із файлів проєкту;
за потреби очистьте історію репозиторію.
Оголошення секрету на верхньому рівні ще не надає його всім контейнерам. Сервіс повинен явно вказати секрет у власному блоці secrets.
Команда на кшталт:
cat /run/secrets/db_passwordможе випадково потрапити до журналів CI або термінального запису. Для перевірки використовуйте лише факт доступності або довжину значення.
.gitignoreФайл секрету може бути випадково доданий командою:
git add .Перевірте статус перед комітом:
git statusНе зберігайте паролі, токени та ключі в Dockerfile, ENV, ARG або командах запуску.
Для запущених контейнерів передавайте секрети як файли в /run/secrets/<ім’я>.
У Docker Compose локальний секрет зазвичай походить із файлу, який не можна додавати до репозиторію.
У Docker Swarm секрети створюються окремо та призначаються лише потрібним сервісам.
Передавайте застосунку шлях до секрету через змінну на кшталт DATABASE_PASSWORD_FILE.
Для секретів під час складання використовуйте BuildKit --mount=type=secret.
Не виводьте секрети в логи та регулярно виконуйте їх ротацію.