Безопасность Docker-образов: что искать на ревью и как исправлять
Большинство проблем Docker-образов видно прямо в Dockerfile: лишние пакеты, секреты в слоях, root-процесс и неподписанная база. Разбираем, как находить такие места на ревью и рефакторить образ без потери воспроизводимости.

На ревью Dockerfile опасность часто выглядит как обычная оптимизация: заменить базовый образ на меньший, убрать пакет, объединить команды или перенести конфигурацию в переменную окружения. Но одна неверная строка может оставить пароль в истории слоёв, запустить приложение от root или превратить обновление системных пакетов в неповторяемую сборку. Ниже — практический разбор для Docker Engine 26+ с BuildKit: что проверять, как переписывать проблемные места и чем измерять результат.
Что именно проверять на ревью
Безопасность образа складывается из нескольких независимых свойств. Малый размер не означает безопасный образ, а свежий тег не гарантирует воспроизводимую сборку.
- Происхождение базы. Понятно ли, из какого образа начинается сборка и как его обновляют.
- Содержимое. Не попали ли в финальный слой компиляторы, менеджеры пакетов, тестовые данные и отладочные утилиты.
- Секреты. Нет ли ключей, паролей и токенов в аргументах, переменных окружения, файлах и истории слоёв.
- Права. Запускается ли процесс от отдельного пользователя с минимальными полномочиями.
- Воспроизводимость. Можно ли повторить сборку через неделю с тем же набором зависимостей.
- Поведение при запуске. Не подменяет ли приложение команду контейнера и получает ли сигналы завершения.
Хорошее ревью образа отвечает не только на вопрос «соберется ли контейнер», но и на вопрос «что произойдет, если этот контейнер уже скомпрометирован».
Слой образа — неизменяемый снимок файловой системы, создаваемый инструкцией Dockerfile. Удаление файла в следующем слое не стирает его из предыдущего: поэтому секрет, однажды записанный в слой, может остаться доступным при просмотре истории образа.
Четыре подхода и их цена
На практике ревью обычно сводится к выбору между удобством сборки, размером образа и контролем происхождения компонентов. Универсального варианта нет.
| Подход | Что выигрываете | Чем платите | Когда выбирать |
|---|---|---|---|
| Полный дистрибутив | Привычные инструменты и проще диагностика | Больше пакетов и поверхность атаки | Для отладки или приложений, которым действительно нужны системные утилиты |
| Минимальный runtime-образ | Меньше файлов и компонентов в проде | Сложнее понять причину сбоя внутри контейнера | Для финального слоя после отдельной сборки |
| Многоэтапная сборка | Компиляторы и исходники остаются в builder-слое | Нужно явно описать границу между этапами | Для Go, Java, Node.js, Python с бинарными зависимостями |
| Образ по digest | Фиксируется конкретное содержимое базы | Обновление требует контролируемого изменения digest | Для релизов и аудируемых сборок |
Теги вроде latest удобны для эксперимента, но плохо подходят для релизной цепочки: один и тот же Dockerfile может собрать разные исходные слои. Digest фиксирует содержимое, однако не решает проблему устаревших уязвимостей. Обновление базы все равно должно быть частью регулярного процесса.
Рефакторинг проблемного Dockerfile
1. Отделите сборку от запуска
Плохой вариант смешивает компиляцию и запуск в одном образе:
FROM node:22
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build
CMD ["npm", "start"]
В таком образе могут остаться исходники, кэш менеджера пакетов и инструменты, которые нужны только для сборки. Безопаснее использовать многоэтапную сборку. Ниже показана схема для Node.js; названия команд зависят от проекта и должны соответствовать его lock-файлу.
# syntax=docker/dockerfile:1
FROM node:22-bookworm AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:22-bookworm-slim AS runtime
WORKDIR /app
ENV NODE_ENV=production
COPY package.json package-lock.json ./
RUN npm ci --omit=dev && npm cache clean --force
COPY --from=builder /app/dist ./dist
RUN groupadd --system app && useradd --system --gid app app
USER app
CMD ["node", "dist/server.js"]
Смысл разделения простой: финальный слой получает только runtime-зависимости и результат сборки. После изменения этого участка проверьте не только размер образа, но и наличие всех файлов, которые нужны приложению при запуске.
2. Не передавайте секреты через ARG и ENV
Такая запись опасна:
ARG NPM_TOKEN
ENV NPM_TOKEN=$NPM_TOKEN
RUN npm install
Аргументы сборки и переменные окружения могут попасть в метаданные, историю или итоговую конфигурацию образа. Токен не должен становиться частью результата сборки. Для временного доступа к секрету используйте механизм секретов BuildKit, если он поддерживается вашей версией Docker и CI.
# syntax=docker/dockerfile:1
FROM node:22-bookworm AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=secret,id=npm_token \
NPM_TOKEN="$(cat /run/secrets/npm_token)" npm ci
Секрет монтируется только на время конкретного шага и не копируется в слой автоматически. Перед использованием проверьте документацию вашей CI-системы: она должна передавать секрет в BuildKit, а не подставлять его в командную строку.
3. Зафиксируйте пользователя и контекст
Если в Dockerfile нет USER, процесс обычно стартует с правами root внутри контейнера. Это не дает ему автоматически права на хосте, но увеличивает последствия уязвимости приложения и ошибки конфигурации.
FROM node:22-bookworm-slim
WORKDIR /app
COPY --chown=app:app package*.json ./
COPY --chown=app:app dist ./dist
USER app
CMD ["node", "dist/server.js"]
Важно, чтобы пользователь app существовал в базовом образе или был создан до копирования файлов. Проверьте запуск командой с временным контейнером и убедитесь, что процесс не имеет root-полномочий. Если приложение пишет в каталог, заранее создайте только этот каталог и выдайте ему нужные права, а не делайте writable всю файловую систему.
4. Сузьте контекст сборки
Docker отправляет в builder контекст, указанный в команде сборки. Если рядом с Dockerfile лежат .git, локальные дампы, файлы окружения и каталоги зависимостей, они могут попасть в контекст или быть случайно скопированы инструкцией COPY . ..
.git
.env
.env.*
node_modules
npm-debug.log*
dist
coverage
*.pem
*.key
Файл .dockerignore не заменяет проверку Dockerfile: он уменьшает контекст, но не исправляет уже попавшие в образ секреты. После изменения проверьте историю и содержимое собранного образа.
Как проводить ревью по шагам
- Определите финальный процесс. Найдите
CMDилиENTRYPOINT, выясните, какой пользователь его запускает и какие каталоги ему нужны. Проверка: приложение стартует без ручного входа в контейнер. - Разделите build и runtime. Выпишите компиляторы, тестовые зависимости и исходники, затем проверьте, что они не копируются в финальный этап. Проверка: в runtime-образе остается только необходимый результат.
- Проследите путь секретов. Ищите
ARG,ENV,RUN echo, конфигурационные файлы и шаблоны CI. Проверка: секрет приходит при запуске или через временный механизм сборки, но не хранится в образе. - Проверьте источники пакетов. Убедитесь, что lock-файл используется, а установка не зависит от случайного состояния внешнего репозитория. Проверка: повторная сборка использует тот же lock-файл и дает ожидаемый набор зависимостей.
- Зафиксируйте проверку. Просканируйте финальный образ, изучите его историю и запустите минимальный smoke test. Проверка: найденные проблемы разделены на исправляемые обновлением, ложные срабатывания и принятые риски.
Что обычно ломается
- «После перехода на slim приложение не запускается». В runtime не хватает системной библиотеки или сертификатов. Сначала найдите конкретную ошибку загрузки, затем добавьте только требуемый пакет, а не возвращайте полный дистрибутив.
- «USER app получает Permission denied». Каталог был создан root-пользователем и не доступен процессу. Исправьте владельца через
--chownили выдайте права только на каталог с временными файлами. - «Секрет все еще виден после удаления строки». Он мог остаться в старом теге или слое. Пересоберите образ без секрета, удалите публикацию скомпрометированного образа и замените сам ключ.
- «Сканер показывает уязвимость в пакете, которого приложение не использует». Пакет может находиться в базовом слое или транзитивной зависимости. Обновите базу и lock-файл, а решение по исключению риска зафиксируйте с причиной и сроком пересмотра.
- «В проде контейнер завершается не сразу». Приложение может неправильно обрабатывать сигналы или запускаться через оболочку. Используйте exec-форму
CMDи проверьте корректное завершение на тестовом окружении.
Где этот подход ломается
Минимальный образ не спасает от уязвимости в самом приложении, вредоносной зависимости или неверных прав на уровне оркестратора. Запуск не от root тоже не отменяет необходимость ограничить capabilities, файловую систему, сеть и доступ к секретам в Kubernetes или другой среде.
Есть и практическое ограничение: слишком агрессивное уменьшение образа усложняет диагностику. Не добавляйте отладочные утилиты в постоянный production-слой ради удобства расследования. Лучше иметь отдельный диагностический образ или временный контейнер с контролируемым доступом.
Что делать дальше
Сначала зафиксируйте для каждого сервиса базовый образ, digest, пользователя, способ передачи секретов и список runtime-зависимостей. Затем добавьте в CI три независимые проверки: сборку с lock-файлом, сканирование финального образа и smoke test под непривилегированным пользователем. На проде наблюдайте не только уязвимости, но и дату обновления базы, изменения состава образа и попытки записи в неожиданные каталоги. Инвалидация старых образов и ротация секретов — отдельные процессы, их не стоит считать автоматически решенными после рефакторинга Dockerfile.
Редактор изображений: Убрать лишнее, заменить фон, добавить объект, расширить кадр — на вашем изображении.
Спросить за 20 ₽Источники: Docker Build checks и рекомендации по сборке образов, Официальный справочник Dockerfile
Частые вопросы
Достаточно ли использовать alpine-образ, чтобы контейнер стал безопаснее?
Можно ли передавать пароль через ARG, если потом удалить переменную?
Что делать, если секрет уже попал в опубликованный образ?
Обязательно ли запускать приложение не от root?
Почему сканер показывает уязвимость после обновления пакета?
Нужно ли фиксировать digest базового образа?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.