Docker-образ с сюрпризом: как проверить чужой образ до запуска
Вы находите на Docker Hub образ, который решает вашу задачу за пять минут — вместо того чтобы час собирать всё руками. docker pull и docker run — и всё уже работает. Проблема в том, что вы запустили на своём сервере чужой код с правами, которые сами же ему выдали, ни разу не заглянув внутрь. Иногда это ничем не заканчивается. Иногда — сервер начинает майнить криптовалюту на чужой кошелёк, или в контейнере тихо сидит процесс, который раз в час стучится на незнакомый IP. Ниже — практический маршрут проверки образа до того, как он попадёт на боевой сервер.
Содержание
Почему нельзя доверять образу «как есть»
Docker-образ — это не просто приложение, это уже собранная файловая система плюс метаданные о том, что запустится при старте (ENTRYPOINT, CMD), какие переменные окружения подставятся и какие порты откроются. Вы получаете это в готовом виде и обычно не видите, из чего это было собрано.
Реальные векторы, с которыми сталкиваются на практике:
- Майнер в точке входа. Образ выглядит как обычный сервис, но
ENTRYPOINTзапускает основной процесс и параллельно — фоновый, который грузит CPU и стучится на пул для майнинга. Заметно по постоянной нагрузке без видимой причины. - Тайпсквоттинг. Образ с именем, похожим на популярный (
nginx-alpineвместоnginx:alpine, лишняя буква в имени пользователя), от произвольного аккаунта, а не от официального паблишера. - Скомпрометированный базовый слой. Автор образа сам не злонамерен, но собрал его поверх старой или заражённой базы, и вредоносный код прилетел транзитивно.
- Бэкдор через переменные окружения или встроенные ключи. В образе может быть зашит SSH-ключ, дефолтный пароль или скрипт, который открывает обратное соединение при определённых условиях.
- Скрытая эксфильтрация данных. Процесс читает переменные окружения (в том числе секреты, которые вы передали при запуске) и отправляет их наружу.
Ни один из этих сценариев не виден в описании образа на Docker Hub. Единственный способ снизить риск — посмотреть, что внутри, до того как образ получит доступ к сети, диску и ресурсам сервера.
Смотрим что внутри: history, inspect, Dockerfile
Если у автора образа есть публичный репозиторий с исходным Dockerfile — начните с него на GitHub/GitLab, это самый прямой способ понять логику сборки. Но Dockerfile под рукой не всегда, а слоям самого образа он не обязан соответствовать 1-в-1 (образ на реестре мог собираться в другой момент времени из другого коммита). Поэтому смотрите и на сам образ.
Скачайте образ без запуска и посмотрите слои и команды, которыми он собирался:
docker pull someuser/some-image:latest
docker history --no-trunc someuser/some-image:latest
--no-trunc обязателен: без него длинные команды обрезаются, и именно в обрезанном хвосте часто прячется что-то интересное — например, curl | bash с загрузкой скрипта со стороннего хоста. Обращайте внимание на:
- команды
RUN, которые тянут что-то из интернета (curl,wget,git clone) без проверки контрольной суммы; - добавление cron-задач или systemd-юнитов;
- изменение прав на бинарники (
chmod +s, установка SUID) — потенциальная эскалация привилегий; - неожиданно большие слои — лишние 200-300 МБ на пустом месте иногда означают, что туда что-то положили сверх заявленного функционала.
Дальше — метаданные самого образа, без запуска контейнера:
docker image inspect someuser/some-image:latest
В выводе смотрите на Config.Entrypoint, Config.Cmd, Config.Env и Config.ExposedPorts. Если у простого веб-сервиса в ExposedPorts внезапно значится порт, не имеющий отношения к заявленной функции (например, 4444 или 6666, типичные для reverse-shell), это повод насторожиться. Подробнее о том, что вообще происходит на каждом слое при запуске образа, — в статье что происходит при docker run по слоям.
Если хочется посмотреть на файловую систему образа без запуска процесса внутри, можно экспортировать слои и пройтись по ним локально:
docker create --name tmp_inspect someuser/some-image:latest
docker export tmp_inspect -o /tmp/image.tar
docker rm tmp_inspect
mkdir /tmp/image_fs && tar -xf /tmp/image.tar -C /tmp/image_fs
docker create не запускает ENTRYPOINT — контейнер только создаётся, процесс внутри не стартует. Это позволяет безопасно распаковать файловую систему и поискать в ней подозрительные бинарники, скрипты автозапуска или прописанные cron-задания — grep по /etc/cron.d, /etc/rc.local, домашним директориям на .bash_profile и подобное.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверПроверяем репутацию источника
Прежде чем разбирать образ побайтово, стоит потратить минуту на источник — это часто отсеивает большинство рисков без всякой технической проверки.
- Официальный образ vs произвольный пользователь. На Docker Hub у официальных образов (
nginx,postgres,redisбез префикса пользователя) есть бейдж Docker Official Image, они поддерживаются организацией Docker и проходят собственный процесс ревью. Образ видаsomeuser/nginx— это чей-то личный форк или сборка, и гарантий там нет никаких, даже если он называется похоже. - Verified Publisher. У части коммерческих образов (например, от известных вендоров ПО) есть значок Verified Publisher — подтверждённая организация. Это не гарантия отсутствия уязвимостей, но подтверждает, что образ действительно от заявленной компании, а не от того, кто зарегистрировал похожий аккаунт.
- Количество pull'ов и звёзд, дата последнего обновления. Образ с парой десятков загрузок и последним обновлением три года назад — не обязательно вредонос, но и доверия к нему меньше, чем к активно поддерживаемому проекту с тысячами загрузок в неделю.
- Ссылка на исходники. Если в описании образа есть ссылка на GitHub-репозиторий с Dockerfile — переходите и смотрите, совпадает ли то, что собирается, с тем, что вы видите в
docker history. Отсутствие исходников при этом не всегда красный флаг (частные образы для внутреннего использования тоже так делают), но для образа из публичного реестра, которым вы не управляете, — повод быть внимательнее. - Профиль автора. Один образ, зарегистрированный вчера, от аккаунта без истории — сильно отличается от автора с десятком поддерживаемых проектов и активностью за годы.
Ни один из этих признаков не доказывает безопасность сам по себе — это фильтр, который отсекает самые дешёвые атаки (тайпсквоттинг, разовые вредоносные заливки), но не заменяет технической проверки.
Сканируем на известные уязвимости
Отдельно от вопроса «злонамерен ли автор» стоит вопрос «сколько в образе известных уязвимостей в пакетах». Даже честный, легитимный образ может тащить старую версию OpenSSL или libc с давно закрытой, но не пропатченной в образе дырой — и это тоже риск, просто другого рода: не бэкдор от автора, а лазейка для того, кто найдёт её снаружи.
Для этого существуют образ-сканеры — утилиты, которые разбирают слои образа, сверяют версии установленных пакетов с базами известных уязвимостей и выдают отчёт с уровнем критичности (critical/high/medium/low) по каждой найденной проблеме. Схема работы в общем виде:
# генерический пример вызова — конкретный сканер и его синтаксис могут отличаться
image-scanner someuser/some-image:latest --severity HIGH,CRITICAL
Что важно понимать про такие сканеры:
- Они находят уязвимости в известных пакетах с известными версиями — если в образ руками зашит кастомный скрипт с бэкдором, сканер его не увидит, это не его задача. Сканер закрывает вопрос «устаревшие/дырявые библиотеки», а не «злонамеренный код».
- Актуальность отчёта зависит от свежести базы уязвимостей у сканера — обновляйте инструмент перед проверкой, иначе он пропустит недавно раскрытые проблемы.
- Отчёт с сотней low/medium находок по системным библиотекам — это норма для любого образа на реальной ОС, а не повод для паники. Смотрите в первую очередь на critical/high и на то, эксплуатируется ли уязвимость удалённо без аутентификации.
- Многие CI-системы (GitLab CI, GitHub Actions) умеют встраивать такое сканирование в пайплайн автоматически — если вы сами собираете образы для деплоя, разумно гонять проверку на каждый билд, а не только руками разово.
Сканер — это фильтр по известным CVE, а не гарантия чистоты образа. Дальше нужен ещё один уровень проверки — поведенческий.
Первый запуск в изоляции: права, сеть, ресурсы
Даже после history, inspect и сканирования лучший источник истины — сам запуск, но не на боевом сервере и не с правами по умолчанию. Идея: дать контейнеру ровно столько, сколько нужно для наблюдения, и ни байтом больше.
Без сети — первый запуск. Если сервис не должен ничего запрашивать наружу на старте, стартуйте вообще без сетевого доступа и смотрите, не падает ли и не жалуется ли он на недоступность внешних адресов (это красный флаг — легитимному сервису для старта обычно не нужен произвольный внешний хост):
docker run --rm --network none --name test_isolated someuser/some-image:latest
Ограниченная сеть для второго прохода. Если сервису нужна сеть по легитимной причине, поднимите отдельный docker-network без выхода в интернет или с egress только на конкретные адреса через firewall-правила на хосте, и смотрите, куда контейнер пытается достучаться в первую очередь.
Урезанные права и ресурсы:
docker run --rm \
--network none \
--read-only \
--cap-drop=ALL \
--security-opt=no-new-privileges \
--memory=256m \
--cpus=0.5 \
--pids-limit=100 \
--user 1000:1000 \
someuser/some-image:latest
--read-only— файловая система контейнера доступна только на чтение, процесс не сможет незаметно что-то дописать на диск (для сервисов, которым нужна запись во временную папку, добавьте--tmpfs /tmp).--cap-drop=ALL— убирает все Linux capabilities, включая те, что нужны для сетевых манипуляций и работы с устройствами; добавляйте обратно только то, что реально требуется (--cap-add=NET_BIND_SERVICEи подобное). О том, почему root внутри контейнера — не то же самое, что root на хосте, и почему это не отменяет нужности capabilities, написано в статье capabilities: почему root в контейнере не root.--security-opt=no-new-privileges— блокирует эскалацию привилегий через SUID-бинарники внутри контейнера, даже если они там есть.--memoryи--cpus— жёсткий потолок ресурсов: если внутри всё-таки майнер, он упрётся в лимит вместо того, чтобы съесть весь сервер.--pids-limit— защита от fork-бомбы и от того, что процесс наплодит десятки дочерних воркеров.--user 1000:1000— запуск не от root даже внутри контейнера, если образ это позволяет (не все образы написаны так, чтобы работать не от root, — тогда увидите ошибки прав на запись, и это отдельный сигнал присмотреться к тому, зачем автору образа понадобился root).
Это не полная замена seccomp-профилям и AppArMor/SELinux, но для первого знакомства с чужим образом — достаточный минимум. Про то, какие системные вызовы режет seccomp-фильтр и что от этого может сломаться, — в статье seccomp: фильтр системных вызовов, что ломается.
Наблюдаем за поведением после запуска
Изоляция снижает ущерб, но сама по себе ничего не говорит о том, что образ пытался сделать. Пока контейнер работает — смотрите на него активно, не оставляйте без присмотра.
Нагрузка на ресурсы:
docker stats test_isolated
Ровный, стабильно высокий CPU без входящей нагрузки — типичный признак майнера. У обычного простаивающего сервиса CPU в состоянии покоя обычно близок к нулю.
Список процессов внутри контейнера:
docker top test_isolated
Если в списке есть процесс с именем, не имеющим отношения к заявленному функционалу образа, или с подозрительным путём запуска (/tmp/.hidden/xmrig-подобные пути в скрытых директориях) — это прямой сигнал остановиться и разбираться.
Сетевая активность. Если контейнер всё же запущен с сетью, посмотрите на исходящие соединения с хоста, зная namespace контейнера, либо снаружи — через tcpdump на интерфейсе моста docker, либо через логи firewall, если вы заранее ограничили egress конкретными адресами и логируете отклонённые пакеты. Внезапные попытки достучаться на IP, никак не связанные с заявленной функцией сервиса (особенно на нестандартные порты вроде 3333/4444, типичные для майнинг-пулов и command-and-control), — веский повод не тащить образ дальше.
Файловая система после остановки. Даже с --read-only часть сервисов пишет в смонтированные тома — проверьте их содержимое на предмет неожиданных файлов после теста.
docker diff test_isolated
Эта команда покажет, какие файлы контейнер добавил, изменил или удалил относительно исходного образа за время работы — полезно даже без --read-only, чтобы увидеть полную картину активности на диске.
Если по итогам такого прогона всё чисто — сеть только туда, куда ожидалось, ресурсы в разумных пределах, процессы соответствуют заявленному функционалу, — можно переносить образ на рабочий контур уже с осознанным набором прав, а не с дефолтными настройками docker run.
Практический момент: такой тестовый прогон разумнее делать не на продовом сервере, где уже крутятся другие сервисы и общая сеть, а на отдельной изолированной машине, которую не жалко перезагрузить или снести целиком, если что-то пошло не так. Небольшой отдельный VPS под карантин образов — дешевле, чем разбираться с последствиями майнера или бэкдора на боевом сервере задним числом.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Docker Hub помечает вредоносные образы сам?
Официальные образы (Docker Official Image) и Verified Publisher проходят дополнительный процесс проверки, но это не тотальное сканирование всего реестра. Образы от произвольных пользователей публикуются без содержательной модерации — ответственность за проверку лежит на том, кто их запускает.
Если образ прошёл сканер без критичных уязвимостей — этого достаточно?
Нет. Сканер находит уязвимости в известных пакетах известных версий, но не видит намеренно встроенный вредоносный код или скрипты автозапуска. Сканирование и поведенческая проверка через изолированный запуск закрывают разные риски и нужны вместе.
Что делать, если образ действительно оказался с майнером или бэкдором?
Остановите и удалите контейнер и сам образ (docker rm, docker rmi), проверьте, не успел ли он что-то записать на смонтированные тома или достучаться наружу за время теста, и не запускайте его версии от того же автора без повторной полной проверки — тайпсквоттинг-аккаунты часто заливают несколько похожих образов сразу.
Обязательно проверять так каждый образ, даже официальный nginx или postgres?
Для широко используемых официальных образов риск на порядки ниже, но полностью нулевым не бывает — историю Dockerfile и docker history на них тоже стоит хотя бы бегло просмотреть, особенно перед обновлением на новую мажорную версию. Полноценный карантинный прогон в первую очередь имеет смысл для образов от малоизвестных авторов и образов, которые вы не обновляли давно.
Можно ли автоматизировать эту проверку, чтобы не делать руками каждый раз?
Да — сканирование уязвимостей и проверку на изменение содержимого образа (например, через хэши слоёв) можно встроить в CI-пайплайн, который гоняет проверку при каждом обновлении версии образа в вашем окружении, до того как новая версия попадёт на прод.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →