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

Незащищённый Docker Registry: уязвимость CI/CD, через которую утекают секреты за 10 минут

Незащищённый Docker Registry: уязвимость CI/CD, через которую утекают секреты за 10 минут
Время чтения: 12 мин.

На аудите CI/CD-инфраструктуры для финтех-компании я нашёл Docker Registry на порту 5000 без какой-либо аутентификации. Десять минут с curl и docker save — и на экране AWS-ключи, токен GitLab CI и пароль от продакшен-базы, зашитые прямо в слоях контейнерных образов. Пайплайн работал, тесты зелёные, деплой шёл автоматически. При этом любой, кто знал IP-адрес сервера, мог скачать все образы и вытащить из них критические секреты.

Ниже — полный разбор цепочки: от обнаружения открытого реестра до компрометации облачной инфраструктуры. И конкретный чек-лист защиты, который я оставляю клиентам после каждого такого аудита.

Docker Registry без аутентификации — как возникает уязвимость пайплайна

Docker Registry — сервис, который хранит и раздаёт контейнерные образы. Образ — это готовый пакет с приложением и всеми его зависимостями, упакованный для запуска в контейнере. Когда CI/CD-пайплайн (автоматизированная цепочка сборки, тестирования и деплоя кода) собирает приложение, результат в виде Docker-образа отправляется в реестр. Оттуда его забирают серверы для запуска в продакшене.

Проблема — в том, как этот реестр разворачивается. Стандартный Docker Registry v2 по умолчанию стартует без аутентификации. Одна команда docker run -d -p 5000:5000 registry:2 — и реестр слушает на порту 5000, открытый любому в сети. В документации Docker аутентификация вынесена в отдельный раздел конфигурации: htpasswd-файл, TLS-сертификат, интеграция с внешним auth-сервером. Ничего из этого не включено «из коробки».

Сценарий повторяется из проекта в проект. DevOps-инженер поднимает реестр для внутреннего пайплайна и планирует «потом» настроить TLS и авторизацию. «Потом» не наступает — реестр работает, билды идут, бизнес доволен. Через полгода кто-то пробрасывает порт наружу. Или порт был открыт с самого начала, но никто не проверял.

Масштаб подтверждается цифрами: по данным Trend Micro, при одном сканировании обнаружено 197 уникальных открытых реестров и выгружено 20 503 образа общим объёмом 9,31 ТБ. Исследование Flare за один месяц нашло более 10 000 образов на Docker Hub с утечками production-credentials — включая секреты компаний из Fortune 500.

Это не разовая ошибка одного джуниора — это системная проблема конфигурации. В классификации OWASP Top 10 (список десяти самых критичных веб-уязвимостей, обновляется раз в несколько лет) она попадает сразу под две категории: A05:2021 Security Misconfiguration — небезопасные настройки по умолчанию, и A07:2021 Identification and Authentication Failures — отсутствие аутентификации на критическом сервисе.

Русскоязычные источники подробно разбирают сканирование образов на уязвимости (Trivy, Clair, Grype) и безопасность Dockerfile, но почти не описывают атаку на сам реестр — как он обнаруживается, как эксплуатируется и что из него можно вытащить. Этот пробел я здесь и закрываю.

Поиск открытых Docker Registry — разведка перед атакой

Прежде чем эксплуатировать реестр, его нужно найти. В терминах MITRE ATT&CK (открытая база тактик и техник атак — каждая техника обозначается T-кодом, тактика группирует техники по цели атакующего) обнаружение реестра сочетает Active Scanning (T1595, тактика Reconnaissance) и Network Service Discovery (T1046, тактика Discovery).

Действия атакующего просты. Docker Registry v2 слушает на порту 5000 (реже 443 или другом) и отвечает на стандартные HTTP-запросы. Проверить доступность реестра можно одним запросом: curl -s http://TARGET:5000/v2/. Если в ответ приходит {} — реестр открыт без аутентификации. Код 401 Unauthorized — авторизация настроена, идём дальше.

Для массового поиска по интернету есть специализированные поисковики:

Shodan — запрос "Docker-Distribution-Api-Version" port:5000 находит серверы, которые в HTTP-заголовках объявляют себя Docker Registry. Censys работает аналогично — поиск по баннерам и сертификатам TLS. Оба индексируют миллионы IP-адресов и возвращают результаты за секунды.

Nmap — сетевой сканер для целевой разведки конкретной подсети на внутреннем пентесте. Команда nmap -p 5000 --open -sV 10.0.0.0/24 покажет все хосты с открытым портом 5000 и попытается определить версию сервиса. Если в результатах Docker Registry (API: 2.0) — цель найдена.

На внутренних пентестах открытые реестры чаще всего обнаруживаются в dev- и staging-сегментах. Их создавали «для себя», и они никогда не проходили security review. Managed-решения вроде Harbor, GitLab Container Registry или облачных реестров (AWS ECR, GCR, Azure ACR) по умолчанию требуют аутентификацию. Но голый registry:2 разворачивается именно потому, что он проще и не требует ничего, кроме Docker.

Утечка секретов сборки: пошаговая эксплуатация незащищённого реестра

Реестр найден и отвечает без авторизации — начинается эксплуатация. В MITRE ATT&CK это Exploit Public-Facing Application (T1190, тактика Initial Access): доступ через публично доступный сервис без подбора паролей и без эксплойтов. Сервис просто открыт.

Ниже — пошаговая последовательность с объяснением каждого шага.

Предусловия: компьютер с curl, docker и tar. Сетевой доступ к целевому хосту. На Kali Linux всё перечисленное стоит из коробки.

Перечисление репозиториев через Registry API v2

Первый шаг — узнать, какие образы хранятся в реестре. Docker Registry HTTP API v2 (REST-интерфейс для управления реестром) предоставляет эндпоинт _catalog, который возвращает полный список репозиториев.

# Запрос списка всех репозиториев
curl -s http://TARGET:5000/v2/_catalog
# Ожидаемый ответ: {"repositories":["app-backend","payment-service"]}

# Для конкретного репозитория — список тегов (версий образа)
curl -s http://TARGET:5000/v2/payment-service/tags/list
# Ожидаемый ответ: {"name":"payment-service","tags":["latest","v2.1"]}

Если _catalog вернул JSON со списком — реестр полностью открыт на чтение. Имена репозиториев часто говорят за себя: payment-service, auth-api, internal-dashboard. Такие названия подсказывают, где искать секреты в первую очередь.

Для подробностей об образе запрашиваем манифест: curl -s http://TARGET:5000/v2/payment-service/manifests/latest. Он покажет список слоёв, из которых состоит образ, и метаданные сборки. Иногда секреты видны уже здесь — прямо в переменных окружения из конфигурации образа.

Извлечение CI/CD секретов из слоёв образа

Следующий шаг — скачать образ и найти в нём чувствительные данные. В MITRE ATT&CK это Credentials In Files (T1552.001, тактика Credential Access) — поиск учётных данных, сохранённых в файлах.

Образ скачивается командой docker pull TARGET:5000/payment-service:latest. Дальше два метода анализа.

Метод А — runtime-анализ. Запустить контейнер через docker run -it --rm TARGET:5000/payment-service:latest /bin/sh и осмотреться внутри: ls -la, cat .env, env. Работает быстро, но образ может не запуститься из-за несовпадения архитектуры (ARM vs x86) или недостающих зависимостей.

Метод Б — статический анализ. Работает всегда, потому что не требует запуска контейнера:

# Сохранить образ как tar-архив
docker save TARGET:5000/payment-service:latest -o service.tar
# Распаковать (каждый слой образа — отдельный каталог)
mkdir layers && tar -xf service.tar -C layers
# Искать секреты через все слои по шаблонам
grep -rl "AWS_SECRET\|PRIVATE KEY\|password=" layers/

Если grep вернул пути к файлам — в них лежат секреты. Типичное содержимое найденного .env-файла (конфигурационного файла с переменными окружения):

AWS_ACCESS_KEY_ID=AKIAYO5AK3xxxxxxxxxx
AWS_SECRET_ACCESS_KEY=DjD71wF1xxxxxxxxxxxx
DB_PASSWORD=pr0d_s3cret_2024
GITLAB_TOKEN=glpat-xxxxxxxxxxxxxxxxxxxx
STRIPE_SECRET_KEY=sk_live_xxxxxxxxxxxx

По данным Trend Micro, среди файлов с чувствительными данными в открытых реестрах чаще всего встречаются: .env с ключами API и паролями баз данных, Dockerfile с секретами в директивах ARG/ENV, конфигурации CI/CD-систем (.gitlab-ci.yml, Jenkinsfile, azure-pipelines.yml) с токенами доступа, SSH-ключи id_rsa и конфигурации пакетных менеджеров (.npmrc с токенами для приватных npm-пакетов).

Исследование Intrinsec показало: из случайной выборки публичных Docker-образов примерно 14% содержат данные, которые не должны быть доступны публично — учётные данные, приватные ключи, API-токены и персональную информацию пользователей. Каждый седьмой образ. Вдумайтесь.

От утечки CI/CD секретов к компрометации облака

Самое опасное начинается после извлечения секретов. Допустим, в .env лежат AWS_ACCESS_KEY_ID и AWS_SECRET_ACCESS_KEY. Атакующий настраивает AWS CLI через aws configure --profile stolen, указывает найденные ключи и проверяет их командой aws sts get-caller-identity --profile stolen. Если команда вернула ARN (Amazon Resource Name — уникальный идентификатор ресурса в AWS) — ключи действительны. Атакующий аутентифицирован в облаке компании.

В MITRE ATT&CK это Valid Accounts (T1078) — использование легитимных учётных данных вместо взлома. По данным IBM X-Force, за 2024 год рост атак с использованием действительных учётных данных составил 71%. Атакующие не взламывают — они входят по ключу, который компания сама оставила в открытом доступе.

Дальше — разведка привилегий: aws s3 ls покажет доступные S3-бакеты (облачные хранилища файлов), aws iam list-attached-user-policies покажет права скомпрометированного пользователя. По данным Flare, в 42% случаев один скомпрометированный образ содержал пять и более различных секретов — один образ способен открыть доступ сразу к нескольким облачным сервисам, репозиториям кода и внутренним системам.

И ещё хуже: по данным того же исследования, 75% разработчиков, удалявших секреты из контейнеров после обнаружения утечки, не отзывали и не ротировали сами ключи. Удалённый из образа секрет навсегда остаётся в истории слоёв. И в руках атакующего.

Supply chain атака DevOps — бизнес-логика угрозы

Зачем атакующему открытый Docker Registry? Мотивация выходит далеко за пределы кражи одного пароля.

Открытый реестр — точка входа для supply chain атаки (атаки на цепочку поставки ПО). Если реестр доступен не только на чтение, но и на запись — а при отсутствии аутентификации это частый случай — атакующий может подменить образ.

Сценарий: злоумышленник скачивает payment-service:latest, внедряет бэкдор, заливает образ обратно через docker push под тем же тегом. Следующий деплой из CI/CD-пайплайна заберёт модифицированный образ и развернёт его в продакшене. Ни один функциональный тест не сработает — приложение работает как прежде, просто теперь в нём есть код атакующего.

По данным CrowdStrike, количество cloud intrusion-инцидентов выросло на 26% год к году за 2024 год. Docker Registry без аутентификации — одновременно источник действительных учётных данных и точка для модификации кода, который попадёт в продакшен.

Если после получения доступа к контейнеру атакующий обнаружит уязвимое ядро, возможен container escape — выход из контейнера на хост-систему (Escape to Host, T1611, тактика Privilege Escalation). Пример — CVE-2022-0847, она же DirtyPipe: уязвимость ядра Linux версий от 5.8 до 5.16.11 (CWE-908 — Use of Uninitialized Resource).

CVSS-оценка (стандартизированная шкала критичности уязвимости от 0 до 10) — 7.8 по вектору CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H. Расшифровка: AV:L — атака локальная (нужен доступ к системе), AC:L — низкая сложность, PR:L — достаточно обычного пользователя, UI:N — без участия жертвы. EPSS-оценка (Exploit Prediction Scoring System — вероятность эксплуатации уязвимости в ближайшие 30 дней) — 0.90, это топ-1% среди всех CVE. Уязвимость включена в каталог CISA KEV — список уязвимостей, которые уже эксплуатируются в реальных атаках — с решением SSVC «Act» (патчить немедленно). Публичные PoC для container escape через DirtyPipe есть на GitHub, например DataDog/dirtypipe-container-breakout-poc (77 звёзд).

Полная цепочка: открытый реестр (T1190) → извлечение секретов из слоёв (T1552.001) → аутентификация в облаке украденными ключами (T1078) → подмена образа → container escape (T1611) → контроль хоста. Supply chain атака, собранная из стандартных техник без единого zero-day.

Безопасность CI/CD пайплайна: чек-лист защиты от docker registry misconfiguration

Защита начинается с простого — не оставлять реестр открытым. Ниже — чек-лист, который я оставляю каждому клиенту после аудита.

Аутентификация и сетевой доступ:

  • Настроить basic auth или token-based аутентификацию для Registry API. Docker Registry поддерживает htpasswd-файл и внешние authorization-серверы
  • Закрыть порт реестра от публичного доступа. Реестр должен быть доступен только из CI/CD-сети через VPN или приватную подсеть
  • Включить TLS: реестр должен работать через HTTPS. Без TLS даже настроенная аутентификация бесполезна — пароли летят по сети открытым текстом
  • Рассмотреть managed-решения (Harbor, GitLab Container Registry, облачные реестры AWS ECR / GCR / Azure ACR) вместо голого registry:2 — аутентификация и TLS включены по умолчанию

Управление секретами в контейнерах:

  • Не зашивать секреты в Dockerfile через ARG или ENV. Использовать Docker BuildKit secrets (--mount=type=secret) или секрет-менеджеры: HashiCorp Vault, AWS Secrets Manager, Kubernetes Secrets
  • Добавить .env, .git, id_rsa, *.pem, .npmrc в .dockerignore — это предотвращает случайное копирование чувствительных файлов в образ при COPY . .
  • Использовать multi-stage builds (многоступенчатая сборка): секреты и инструменты сборки остаются в промежуточном слое и не попадают в финальный образ, который уходит в реестр

Сканирование и supply chain security контейнеризация:

  • Встроить сканирование образов в пайплайн. Trivy или Grype находят и уязвимые пакеты, и зашитые секреты за секунды
  • Подписывать образы через Docker Content Trust (DCT) или Cosign/Sigstore — это гарантирует, что в продакшен попадёт образ, который собрал CI/CD, а не подменённый атакующим
  • Проверять сети на открытые реестры регулярно: nmap -p 5000,5001 --open по внутренним подсетям раз в неделю — дешёвый и рабочий контроль

Минимизация blast radius (радиус поражения):

  • Ротировать все секреты, которые когда-либо попадали в Docker-образ. Удалить секрет из текущей версии недостаточно — он остаётся в истории слоёв предыдущих сборок
  • Применять принцип наименьших привилегий для облачных ключей: AWS-ключ, используемый в приложении, не должен иметь AdministratorAccess — только те permissions, которые нужны для работы
  • Запускать контейнеры от непривилегированного пользователя (USER nonroot в Dockerfile) и без лишних Linux capabilities — это существенно затрудняет container escape даже при наличии уязвимости ядра

За два года я провёл аудит CI/CD-инфраструктуры в восьми компаниях — от стартапов до организаций с тысячами контейнеров в продакшене. В пяти из восьми Docker Registry был доступен без аутентификации хотя бы из одного сегмента сети. В трёх из пяти оттуда удавалось извлечь действующие облачные ключи.

Реакция команды каждый раз одинаковая: «Мы знали, что надо настроить авторизацию, но руки не дошли».

Это не проблема знаний. Средства DevSecOps-безопасности контейнеров бесплатны и документированы. Это проблема приоритизации, где безопасность реестра проигрывает каждой следующей бизнес-фиче в бэклоге. CI/CD-пайплайн — самая недооценённая поверхность атаки в современной инфраструктуре: по определению у него есть доступ ко всему — к исходному коду, секретам, продакшен-серверам и облачным аккаунтам. Одна docker registry misconfiguration — и этот доступ получает любой желающий.

Пока DevOps-культура в компании не включает security review на этапе «поднял сервис — проверь доступ», открытые реестры будут появляться снова. Базовый трек IB Basics — это то, что я бы прошёл вместо самостоятельного блуждания по YouTube, если бы начинал путь в ИБ сейчас.

Эту тему и смежные навыки разбирают на практике в курсе «Основы практик DevOps» Codeby Academy.