Сколько раз ты видел подобный паттерн в чужих репозиториях или ловил себя на желании сделать так же: во время сборки контейнера нужно скачать приватный пакет из закрытого репозитория npm, стянуть бинарник по API или авторизоваться в защищенной базе. Разработчик копирует SSH-ключ или файл с токеном внутрь образа, запускает сборку, а в следующей строке заботливо пишет RUN rm -f /root/.ssh/id_rsa или объединяет команды через амперсанды.
Логика кажется кристально ясной: файл был, файл удалили. Запускаем контейнер командой docker run, проверяем каталог через ls -la — чистота, секрета нет. Можно смело пушить готовый образ в публичный реестр Docker Hub или корпоративный GitHub Packages.
Но в реальности ты только что подарил злоумышленникам свои боевые ключи доступа на блюдечке.
Анатомия whiteout-файлов в OverlayFS
Чтобы понять масштаб катастрофы, нужно заглянуть под капот слоистой файловой системы OCI-образов (Union File System, в подавляющем большинстве случаев это OverlayFS).
Образ Docker — это не монолитный жесткий диск, а стопка неизменяемых (immutable) слоев, каждый из которых физически представляет собой обычный tar-архив с дельтой изменений файловой системы. Инструкции COPY или ADD создают слой, куда упаковывается твой приватный ключ.
Когда в следующей строке ты вызываешь RUN rm, движок OverlayFS физически не может стереть байты из уже зафиксированного нижнего слоя. Вместо этого в новый верхний слой записывается крошечный системный маркер — так называемый whiteout-файл с префиксом .wh.<имя_файла>. При сборке слоев в единое дерево виртуальной файловой системы драйвер видит этот маркер и просто скрывает файл из вывода пользовательских команд.
В своем исследовании инженер Лука Кларич наглядно показал, как элементарно извлекаются подобные «удаленные» секреты. Любой пользователь, скачавший образ, делает следующее:
# Сохраняем образ в локальный архив и распаковываем слои
docker save my-company-app:latest -o image.tar
tar -xf image.tar
# Ищем наш файл в tar-архивах каждого исторического слоя
find . -name "layer.tar" -exec tar -tf {} \; | grep -E "id_rsa|token|secret"
И секрет всплывает в открытом виде за доли секунды. Популярные утилиты анализа образов вроде dive или сканеры Trivy подсвечивают такие скрытые файлы мгновенно.
Безопасная сборка с Docker BuildKit Secrets
Единственный надежный способ использовать конфиденциальные данные на этапе сборки — не допускать их попадания в слои образов вообще. Современный бэкенд сборки Docker BuildKit предоставляет для этого нативный механизм: директиву монтирования секретов --mount=type=secret.
Секрет монтируется во временную файловую систему в оперативной памяти (tmpfs) по пути /run/secrets/<id> исключительно на время выполнения конкретной команды RUN:
# syntax=docker/dockerfile:1
FROM alpine:3.20
RUN apk add --no-cache curl ca-certificates
# Монтируем токен доступа без сохранения в историю слоев образа
RUN --mount=type=secret,id=api_token,required=true \
TOKEN=$(cat /run/secrets/api_token) && \
curl -f -H "Authorization: Bearer $TOKEN" \
-o /usr/local/bin/private-tool \
https://internal.example.com/bin/private-tool && \
chmod +x /usr/local/bin/private-tool
CMD ["/usr/local/bin/private-tool"]
При вызове команды docker build значение секрета передается напрямую из переменной окружения хоста или из локального файла, не оставляя следов в кэше сборки:
# Передача секрета из переменной окружения рабочей станции или CI-раннера
export API_TOKEN="ghp_my_super_secret_deployment_token"
docker build --secret id=api_token,env=API_TOKEN -t my-company-app:latest .
В результате в итоговом образе нет ни самого файла, ни белых маркеров OverlayFS, ни упоминания токена в команде docker history. Память tmpfs мгновенно очищается ядром сразу после завершения команды RUN. Это простое правило сборки экономит компаниям сотни часов расследования инцидентов утечки инфраструктурных доступов.
