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

Как извлечь секреты из docker образа и dockerfile даже после Run rm: 8 проверок на ubuntu 24.04

Сценарий знаком многим: секрет случайно оказался в Dockerfile или попал в контекст сборки, разработчик быстро добавил `RUN rm`, файл в контейнере больше не виден - и кажется, что проблема закрыта. На практике это самообман: даже не запуская контейнер, секрет нередко можно извлечь прямо из образа, потому что он "оседает" в слоях или метаданных. Разобраться, где именно прячутся такие следы, помогает серия из восьми проверок, выполненных на Ubuntu 24.04.1 LTS (Docker Engine 29.1.3, BuildKit v0.26.2).

Ниже - логика этих экспериментов и главное, что они показывают: разные инструкции Dockerfile "светят" секреты по‑разному, а привычное "удалил - значит удалил" для слоёв почти никогда не работает. Если вам близка тема "куда девается секрет при сборке", стоит держать в голове принципы, на которых строится безопасная сборка docker образов: слой неизменяем, а история сборки и конфиг образа могут сохранять больше, чем ожидается.

Эксперимент 1: COPY - секрет попадает в слой

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

Эксперимент 2: COPY + RUN rm - "удалили", но не уничтожили

Популярная "заплатка" - скопировать секрет, использовать и затем удалить отдельной командой `RUN rm /secret.txt`. При запуске контейнера файл действительно пропадает: `cat /secret.txt` вернёт ошибку. Но в терминах слоёв это означает лишь то, что новый слой пометил файл как удалённый. Данные, записанные в предыдущем слое, остаются нетронутыми - и при разборе слоёв через `docker save` секрет всё так же извлекается. Это важный вывод для всех, кого волнует удаление секретов из docker образа: "rm" меняет финальное состояние файловой системы, но не стирает содержимое уже созданного слоя.

Эксперимент 3: ENV - секрет уезжает в конфиг образа

С переменными окружения всё ещё прямолинейнее. Если положить секрет через `ENV SECRET=...`, файла в контейнере может не быть вовсе, но значение будет храниться в конфигурации образа. После `docker save` и распаковки в JSON-конфиге обнаруживается `config.Env`, где лежит строка `SECRET=SUPER_SECRET_KOTY_HULIGANY_2026`. Более того, `docker history --no-trunc` обычно печатает полную инструкцию ENV вместе со значением. Вывод однозначный: ENV для секретов - худший выбор, потому что "след" становится максимально удобным для извлечения.

Эксперимент 4: ARG - не в Env, но в истории

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

Эксперименты 5-8: секреты BuildKit, multi-stage и "фокусы" со слоями

Дальше начинаются приёмы, которые действительно способны уменьшить утечки:

BuildKit Secrets (то, что многие называют docker build secrets) позволяют передавать чувствительные данные во время сборки так, чтобы они не попадали ни в итоговые слои, ни в конфиг - при условии, что секрет не "протекает" в команду, логи или артефакты. Этот подход обычно и относят к наиболее надёжным вариантам для сборки.

Multi-stage build помогает "отрезать" всё лишнее: секрет можно использовать на промежуточной стадии (например, для скачивания приватных зависимостей), а затем собрать финальный образ без копирования файлов и кэшей, которые могли содержать секрет. Суть здесь в дисциплине: финальный stage должен получать только чистые артефакты.

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

В сумме эксперименты показывают важную мысль: "секрет в образе" - это не только про файлы внутри контейнера. Это ещё и про историю сборки, конфиг образа и неизменяемость слоёв. Именно поэтому корректнее выбирать механики, которые не создают секрет в слое изначально - в этом смысле docker build secrets для безопасной сборки docker образов чаще выигрывают у любых попыток "прибраться после себя".

Что добавить к практике: как не допускать утечек

Во многих командах проблема начинается не с Dockerfile, а с контекста сборки: секреты случайно попадают в директорию проекта, затем - в `docker build .`. Поэтому стоит жёстко пересмотреть `.dockerignore`, запретить попадание `.env`, ключей и дампов в build context и договориться, что секреты не хранятся рядом с исходниками даже "на минутку".

Отдельно полезно внедрить сканер секретов для docker образов в CI: проверка уже собранного артефакта выявляет утечки, которые не видны при ревью Dockerfile. Такой сканер стоит запускать не только на финальном образе, но и на промежуточных стадиях (если CI их сохраняет), а также на истории сборки, потому что `ARG` и "говорящие" команды в `RUN` умеют выдавать секреты текстом.

Наконец, важно помнить про кэш сборки: даже если финальный образ "чистый", чувствительные данные могут осесть в кэше BuildKit на сборочном агенте. Для защищённых пайплайнов это отдельная зона контроля: изоляция раннеров, регулярная очистка кэша, минимизация логов, запрет вывода переменных и токенов в stdout.

Если резюмировать: попытки "спрятать" секреты постфактум почти всегда слабее, чем подход "не заносить секрет в слой". А вопрос "как скрыть секреты в dockerfile" на практике чаще превращается в архитектурное решение: использовать BuildKit, правильно строить multi-stage и проверять артефакты автоматикой - тогда и удаление секретов из docker образа не приходится имитировать командой `rm`, которая лишь создаёт иллюзию безопасности.

Scroll to Top