Перейти к содержимому

Защита секретов в dockerfile при сборке docker-образа: куда они прячутся и 8 проверок

Куда действительно "прячется" секрет при сборке Docker-образа: 8 практических проверок

Ситуация знакомая: секретный токен или ключ случайно попал в Dockerfile, разработчик спохватился, добавил `RUN rm`, в запущенном контейнере файла нет - значит, можно выдыхать? Увы, нет. Во время сборки Docker-образа данные раскладываются по слоям и метаданным так, что "удалённое" нередко остаётся доступным - причём без запуска контейнера. Ниже - разбор восьми типичных способов передачи секрета и то, где именно он в итоге оседает.

Эксперименты проводились в окружении Ubuntu 24.04.1 LTS, Docker Engine 29.1.3, BuildKit v0.26.2. В качестве тестового секрета использовалась строка `SUPER_SECRET_KOTY_HULIGANY_2026`.

1) COPY: секретный файл попадает в слой целиком

Самый распространённый путь - положить файл в образ через `COPY secret.txt /secret.txt`. Команда `docker history --no-trunc` покажет факт инструкции и путь, но не содержимое. Это часто вводит в заблуждение: кажется, раз в истории нет текста, значит, его и в образе нет.

На практике всё проще и хуже: после `docker save` и распаковки архива образа нужный файл находится внутри одного из tar-слоёв и извлекается обычными средствами. То есть секрет остаётся в образе в явном виде - это прямой антипаттерн для защиты секретов в Dockerfile.

2) COPY + отдельный RUN rm: контейнер чистый, образ - нет

Логичный "фикс" - удалить файл после использования:
`COPY secret.txt /secret.txt` → `RUN rm /secret.txt`.

Проверка запуском контейнера покажет, что файла нет. Но слои устроены так, что `rm` создаёт новый слой, в котором файл помечен как удалённый, а данные из предыдущего слоя никуда не деваются. При разборе архива образа секрет обнаруживается в более раннем слое. Поэтому как "рецепт" того, как убрать секреты из Docker образа, отдельная команда `rm` не работает.

3) ENV: секрет уезжает в конфигурацию образа

Когда секрет кладут в `ENV`, файла в контейнере действительно нет - потому что значение хранится не в файловой системе, а в конфиге образа. После `docker save` в JSON-конфигурации легко найти секцию `config.Env`, где будет строка вида:
`SECRET=SUPER_SECRET_KOTY_HULIGANY_2026`.

Кроме того, `docker history --no-trunc` зачастую отображает инструкцию `ENV` вместе со значением. Итог: `ENV` для секретов - одна из самых "громких" утечек.

4) ARG: в окружении контейнера не видно, но след в истории остаётся

`ARG` выбирают, потому что это "только на время сборки". Действительно, в `config.Env` переменной не будет. Однако значение часто сохраняется в истории сборки, и его можно вытащить через `docker history --no-trunc`, если оно фигурировало в команде, породившей слой (или иным образом попало в метаданные сборки). На практике `ARG` - слабая маскировка: контейнер чистый, а артефакты сборки могут сохранить всё необходимое для восстановления секрета.

5) BuildKit Secrets: секрет не записывается в слой

Самый "правильный" современный подход - секреты BuildKit, когда значение монтируется на время выполнения шага и не коммитится в слой. Речь о механике Docker build secrets BuildKit: секрет доступен внутри `RUN` как временный файл в mount'е, но после шага не попадает в слои и не должен появляться в истории команд.

Этот вариант и есть базовая безопасная сборка Docker образов с секретами: секрет можно использовать (например, для `pip`, `npm`, `git clone`), при этом не оставляя его в файловой системе образа.

Практические детали и нюансы такого подхода удобно разобраны в материале про Docker build secrets BuildKit и безопасную сборку - особенно полезно тем, кто внедряет это в CI.

6) Multi-stage build: финальный образ чище, но риски остаются

Мультистейдж-сборка помогает вынести "грязные" операции в промежуточный stage и перенести в финальный только результат (бинарник, собранные артефакты). Если секрет использовался исключительно в builder-стадии и не копировался в финальную, то итоговый образ может не содержать его в слоях.

Но есть важная оговорка: промежуточные стадии и кэш сборки живут локально (а иногда и в удалённом кэше CI), и там секрет может сохраниться. Поэтому multi-stage - это не магическое удаление, а снижение вероятности попадания секрета в то, что вы публикуете. Для строгих требований его лучше комбинировать с BuildKit secret mounts.

7) Удаление секрета внутри одного RUN: работает не всегда

Есть приём: создать/получить секрет, использовать его и удалить в рамках одного `RUN`, чтобы в финальном слое файла не было. Это может сработать, если секрет нигде не "засветился" в строках команд и не был добавлен отдельной инструкцией `COPY/ADD`. Но стоит поместить значение прямо в команду (например, `echo SUPER_SECRET... > file`) - и оно может оказаться в истории.

То есть схема "в одном RUN создал - применил - стёр" полезна как техника минимизации следов, но только при аккуратной реализации и понимании, что именно попадает в метаданные.

8) Перезапись секрета: косметика вместо удаления

Некоторые пытаются "затереть" секрет, перезаписав файл или переменную другим значением. В мире слоёв Docker это почти всегда самообман: старые данные остаются в предыдущих слоях и их можно восстановить. Перезапись меняет текущее состояние файловой системы контейнера, но не переписывает историю слоёв.

---

Что в итоге показали эти проверки

1. `COPY` и `COPY + rm` оставляют секрет в слоях - его можно достать из сохранённого образа.
2. `ENV` хранит секрет прямо в конфигурации образа и часто светит его в истории.
3. `ARG` может не попасть в окружение контейнера, но нередко остаётся в истории/метаданных сборки.
4. Наиболее надёжный путь - секреты BuildKit: они не должны коммититься ни в слои, ни в конфиг.
5. Multi-stage снижает риск утечки в финальный образ, но не гарантирует исчезновение секрета из промежуточных артефактов.

---

Как снизить риск утечек: дополнительные практики

Ещё до сборки полезно включать сканирование Docker образов на секреты в CI: такие проверки ловят и случайно закоммиченные файлы, и токены, попавшие в ENV/историю, и "забытые" конфиги. Это особенно важно в командах, где образы регулярно публикуются в registry и расходятся между окружениями.

Вторая линия обороны - дисциплина сборочного контекста: строгий `.dockerignore`, запрет на хранение ключей рядом с кодом, отдельные директории для локальных конфигов разработчика. Чем меньше попадает в build context, тем меньше шанс, что секрет "прилетит" в образ через невинный `COPY . .`.

Третья практика - разделять "секрет для сборки" и "секрет для рантайма". Ключи для доступа к приватным репозиториям, токены скачивания зависимостей и лицензии SDK - это одно; секреты приложения (доступ к БД, JWT signing key) должны попадать уже на этапе запуска через менеджер секретов/переменные окружения платформы, а не запекаться в образ.

И наконец, полезно договориться о проверяемых правилах: что считается секретом, что запрещено писать в Dockerfile, какие команды блокируются в ревью. Такой набор правил быстро превращается в понятный стандарт защиты секретов в Dockerfile и экономит часы расследований.

Если вам нужно внедрить это системно, начните с перехода на безопасную сборку Docker образов с секретами через BuildKit, а затем добавьте сканирование и политику хранения файлов в репозитории - в связке это даёт максимальный эффект.

Scroll to Top