Давай честно. Пароль от базы, токен платежного шлюза, приватный ключ для доступа в приватный реестр - все это где-то лежит. Вопрос не в том, есть ли у тебя секреты, а в том, насколько широко они размазаны. Самый частый сценарий утечки выглядит так: разработчик кладет пароль в ENV в Dockerfile, потому что так быстрее, образ уезжает в реестр, а через полгода стажер делает
Код: Выделить всё
docker inspectТема docker секреты на собеседованиях и в реальной эксплуатации это лакмус: по тому, как человек хранит docker пароль, сразу видно, понимает он модель угроз или просто катит код. В этом уроке разберем, почему ENV и слои образа - это публичная доска объявлений, как работают build secret docker и runtime-секреты под капотом, и как выстроить принцип "секреты снаружи, контейнер только читает". Глубоко, с разбором вывода команд и граблями, на которые наступают даже сеньоры.
Ключевая мысль, которую надо усвоить раз и навсегда: образ - это артефакт, который ты публикуешь и кешируешь, а секрет - это то, что нельзя публиковать и кешировать. Совмещать их физически нельзя. Все решения ниже - про то, как развести эти две сущности во времени и в пространстве.

Почему ENV и слои образа не место для секретов в контейнере
Начнем с механики, потому что без нее советы звучат как догма. Образ Docker - это набор слоев (read-only), описанных манифестом, плюс конфиг. Каждая инструкция Dockerfile, которая меняет файловую систему или метаданные, порождает слой или запись в конфиге. И вот ключевое: слои неизменяемы и кешируются, а конфиг доступен любому, кто скачал образ.
Разберем три канала утечки, которые надо понимать пальцами.
1. ENV живет в конфиге образа и виден всем. Если ты пишешь в Dockerfile
Код: Выделить всё
ENV DB_PASSWORD=hunter2Код: Выделить всё
docker inspect myapp:latest --format '{{json .Config.Env}}'Код: Выделить всё
["PATH=/usr/local/bin:/usr/bin:/bin","DB_PASSWORD=hunter2","LANG=C.UTF-8"]Код: Выделить всё
inspectКод: Выделить всё
--build-arg2. История слоев хранит команды и ARG. Смотрим:
Код: Выделить всё
docker history --no-trunc myapp:latestКод: Выделить всё
ARG TOKENКод: Выделить всё
RUN curl -H "Authorization: $TOKEN" ...Код: Выделить всё
history3. Логи и CI. Секрет в ENV рано или поздно попадет в дамп окружения. Любой
Код: Выделить всё
printenvКод: Выделить всё
set -xВывод жесткий: ни ENV, ни ARG, ни COPY секретного файла в образ - не годятся. Образ должен быть стерильным и безопасным для публикации даже в публичный Docker Hub. Все, что секретно, подается отдельно - либо на момент сборки через BuildKit, либо в рантайме через монтирование.
Build secret docker: как BuildKit подает секрет в сборку и не оставляет следа
Иногда секрет нужен именно во время сборки: скачать приватный пакет, авторизоваться в приватном npm/composer-реестре, клонировать приватный git. Для этого есть build secret docker - механизм BuildKit, который монтирует секрет в файловую систему сборки только на время одной инструкции RUN и не пишет его ни в слой, ни в историю.
BuildKit - это движок сборки по умолчанию в современном Docker и в
Код: Выделить всё
docker buildxКод: Выделить всё
# syntax=docker/dockerfile:1
FROM node:22-slim
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \
npm ci --omit=dev
COPY . .Код: Выделить всё
docker buildx build --secret id=npmrc,src=$HOME/.npmrc -t myapp:latest .Код: Выделить всё
--secretКод: Выделить всё
/run/secrets/<id>Код: Выделить всё
targetКод: Выделить всё
docker historyМожно подать секрет и из переменной окружения, не создавая файл:
Код: Выделить всё
export GH_TOKEN=ghp_xxx
docker buildx build --secret id=gh,env=GH_TOKEN -t myapp .Код: Выделить всё
RUN --mount=type=secret,id=gh \
git clone https://oauth2:$(cat /run/secrets/gh)@github.com/org/private.gitКод: Выделить всё
cat /run/secrets/gh > /app/token.txtГрабля номер два - забыть строку
Код: Выделить всё
# syntax=docker/dockerfile:1Код: Выделить всё
--mount=type=secretКод: Выделить всё
DOCKER_BUILDKIT=1После сборки обязательно проверь себя - это часть гигиены supply chain:
Код: Выделить всё
docker history --no-trunc myapp:latest | grep -i secret
docker inspect myapp:latest --format '{{json .Config.Env}}'Секреты в контейнере в рантайме: файл, Docker secrets и внешние хранилища
Сборка стерильна, но приложению в рантайме все равно нужен пароль к базе. Как подать секреты в контейнере, не вшивая их в образ? Принцип один: секрет приходит снаружи, контейнер его только читает, желательно из файла, а не из ENV. Файл лучше ENV, потому что ENV наследуется дочерними процессами и легко утекает в дампы, а файл с правами 0400 читает только нужный процесс.
Вариант для одиночного хоста и Compose - монтирование файла-секрета. Compose v2 умеет работать с секретами как с отдельной сущностью. Файл
Код: Выделить всё
compose.yamlКод: Выделить всё
services:
app:
image: myapp:latest
secrets:
- db_password
environment:
DB_PASSWORD_FILE: /run/secrets/db_password
secrets:
db_password:
file: ./secrets/db_password.txtКод: Выделить всё
/run/secrets/db_passwordКод: Выделить всё
POSTGRES_PASSWORD_FILEКод: Выделить всё
./secrets/db_password.txtКод: Выделить всё
.gitignoreВариант для кластера - Docker secrets в Swarm. Нативные docker secrets (именно как механизм) живут в Swarm: они хранятся в зашифрованном Raft-логе менеджеров, передаются на ноды по mTLS и монтируются в сервис как tmpfs-файл в
Код: Выделить всё
/run/secrets/<name>Код: Выделить всё
printf 'S3cr3tP@ss' | docker secret create db_password -
docker service create --name app --secret db_password myapp:latestКод: Выделить всё
/run/secrets/db_passwordКод: Выделить всё
docker service update --secret-rm old --secret-add newВариант для зрелого продакшена - внешнее хранилище. HashiCorp Vault, Yandex Lockbox, AWS Secrets Manager - это управляемые хранилища с аудитом, версионированием и автоматической ротацией. Подключают их обычно двумя путями:
- entrypoint-подход: скрипт-обертка при старте контейнера ходит в Vault/Lockbox по короткоживущему токену, кладет секреты во временный файл или в окружение текущего процесса и запускает приложение через . Секрет не вшит, живет только в памяти процесса.
Код: Выделить всё
exec - sidecar-подход: рядом с приложением едет агент (например Vault Agent), он аутентифицируется, забирает секреты и кладет их в общий tmpfs-том. Приложение читает файл, агент сам обновляет его при ротации. Приложение про Vault вообще не знает - чистое разделение ответственности.
Код: Выделить всё
yc lockbox payload get.env, .gitignore и сканирование репозитория на секреты через gitleaks
Самая массовая утечка - не хитрый docker inspect, а банальный закоммиченный
Код: Выделить всё
.envКод: Выделить всё
.envКод: Выделить всё
.gitignoreКод: Выделить всё
.env.exampleКод: Выделить всё
# .gitignore
.env
.env.local
secrets/
*.pem
*.keyКод: Выделить всё
.gitignoreЗапуск через образ, без локальной установки:
Код: Выделить всё
docker run --rm -v "$(pwd):/repo" zricethezav/gitleaks:latest \
detect --source=/repo --verboseКод: Выделить всё
Finding: AWS_SECRET=AKIA...
Secret: AKIAIOSFODNN7EXAMPLE
RuleID: aws-access-token
File: config/prod.yml
Commit: 4f2a1c9
Author: dev2
Код: Выделить всё
CommitКод: Выделить всё
git filter-repoПравильное место для gitleaks - два рубежа: pre-commit hook (ловит до коммита, на машине разработчика) и шаг в CI на каждый PR (ловит то, что просочилось мимо хука). Это прямая часть supply chain security: ты защищаешь не только образ, но и исходники, из которых он собирается.
Типичные грабли и антипаттерны
- ARG для секрета. кажется безопасным, но он виден в
Код: Выделить всё
ARG TOKEN. ТолькоКод: Выделить всё
docker history.Код: Выделить всё
--mount=type=secret - COPY .env в образ. Классика. Файл с паролями уезжает в реестр внутри слоя. Добавь в
Код: Выделить всё
.env, не только вКод: Выделить всё
.dockerignore.Код: Выделить всё
.gitignore - echo секрета в логи. для отладки - и токен в логах CI навсегда. Никогда не печатай секреты.
Код: Выделить всё
RUN echo "token: $TOKEN" - Запись секрета в слой внутри RUN. BuildKit-секрет, сохраненный в файл командой - это уже не секрет. Используй в рамках одной команды.
- Один пароль на все среды. dev, stage, prod должны иметь разные секреты. Утечка из dev не должна открывать prod.
- Секрет в ENV вместо файла. ENV наследуется дочерними процессами и течет в дампы. Где можешь - паттерн _FILE.
- Длинноживущие токены. Статический пароль, который никогда не меняется, - вопрос времени. Внешние хранилища дают короткоживущие и ротируемые секреты.
- Создай файл со строкой
Код: Выделить всё
./secret.txt.Код: Выделить всё
super-secret-value - Напиши Dockerfile с и инструкцией
Код: Выделить всё
# syntax=docker/dockerfile:1, выводящей секрет в лог сборки (для лабы, не для прода).Код: Выделить всё
RUN --mount=type=secret,id=mysecret cat /run/secrets/mysecret - Собери: . Убедись, что секрет напечатался в выводе RUN.
Код: Выделить всё
docker buildx build --secret id=mysecret,src=./secret.txt -t lab . - Проверь, что в образе его нет: и
Код: Выделить всё
docker history --no-trunc lab. Содержимого секрета быть не должно.Код: Выделить всё
docker inspect lab --format '{{json .Config.Env}}' - Подними Compose-сервис с секретом-файлом и -переменной, зайди внутрь (
Код: Выделить всё
_FILE) и прочитайКод: Выделить всё
docker compose exec app sh.Код: Выделить всё
/run/secrets/db_password - Прогони gitleaks по любому своему репозиторию и посмотри на формат находок.
- Почему делает
Код: Выделить всё
docker historyнебезопасным даже если ты удалил файл с токеном в следующем слое?Код: Выделить всё
ARG TOKEN - Чем монтирование секрета файлом в лучше передачи через ENV в рантайме?
Код: Выделить всё
/run/secrets/ - Что физически происходит с build secret после завершения RUN-инструкции и почему он не попадает в слой?
- Ты нашел секрет в истории git через gitleaks. Достаточно ли удалить файл и почистить историю?
Образ публикуется и кешируется, секрет - нет, поэтому они не должны жить в одном месте. На сборке секрет подается через
Код: Выделить всё
--mount=type=secretКод: Выделить всё
.envКод: Выделить всё
.gitignoreКод: Выделить всё
.dockerignore