Сканирование уязвимостей образов

Как находить известные уязвимости в собранных образах и снижать риск ещё до деплоя.

CVE (Common Vulnerabilities and Exposures) — публичный идентификатор известной уязвимости, например CVE-2023-1234. Сканер образа сверяет пакеты внутри с базой CVE и сообщает о совпадениях.

Образ кажется «вашим кодом», но на 90% состоит из чужого: базовый дистрибутив (Debian, Alpine), системные библиотеки (openssl, zlib), интерпретатор или рантайм, зависимости приложения. В любом из этих компонентов со временем находят уязвимости. Образ, собранный полгода назад, мог быть чистым, а сегодня в его версии openssl уже есть критический CVE. Сканирование — это автоматическая проверка «что из известного плохого приехало вместе с образом». Этот урок — про оборону: как находить и чинить, а не эксплуатировать.

Зачем это на практике

Уязвимый образ в проде — это потенциальная точка входа для атакующего и провал любого аудита безопасности. Сканер встраивают в CI, чтобы ловить проблему до релиза: собрали образ → просканировали → если есть критичные CVE, сборка падает и образ не уезжает в прод. Дешевле обновить базовый образ на этапе сборки, чем разбирать инцидент.

Откуда в образе берутся CVE

Источников несколько, и полезно различать их, потому что чинятся они по-разному:

  • Базовый образ. FROM python:3.11 тянет целый Debian с десятками системных пакетов. Их уязвимости становятся вашими.
  • Системные библиотеки. libssl, libc, curl — низкоуровневые компоненты, в которых регулярно находят дыры.
  • Зависимости приложения. Пакеты из requirements.txt / package.json — у них свои CVE.
  • Старость образа. CVE «появляется» не в образе, а в базе данных уязвимостей. Образ не менялся, но мир узнал о проблеме — и теперь сканер её видит.

Инструменты: trivy и docker scout

Trivy (Aqua Security) — популярный open-source сканер. Принимает имя образа, скачивает актуальную базу CVE и выдаёт отчёт.

# Просканировать образ
trivy image ghcr.io/acme/api:1.4.2

# Только критичные и высокие, остальное скрыть
trivy image --severity CRITICAL,HIGH ghcr.io/acme/api:1.4.2

# Падать с ненулевым кодом, если есть CRITICAL — удобно для CI
trivy image --exit-code 1 --severity CRITICAL ghcr.io/acme/api:1.4.2

docker scout — встроенный в Docker CLI инструмент. Удобен «из коробки» и хорошо показывает, какие CVE уйдут, если обновить базовый образ.

# Сводка по уязвимостям образа
docker scout cves ghcr.io/acme/api:1.4.2

# Рекомендации: на какой базовый образ перейти
docker scout recommendations ghcr.io/acme/api:1.4.2

Как читать отчёт

Отчёт сканера — это таблица: для каждой уязвимости указаны пакет, установленная версия, версия с фиксом, идентификатор CVE и уровень опасности (severity). Уровни обычно: CRITICAL, HIGH, MEDIUM, LOW, UNKNOWN.

Package    Installed   Fixed In   Vulnerability    Severity
openssl    3.0.11      3.0.13     CVE-2024-XXXX    CRITICAL
libcurl    8.4.0       8.5.0      CVE-2023-YYYY    HIGH
zlib       1.2.13      (none)     CVE-2023-ZZZZ    MEDIUM

Ключевая колонка — Fixed In. Если там есть версия — уязвимость исправляется обновлением пакета или базового образа. Если стоит (none) — фикса пока нет, и решать придётся иначе (принять риск с обоснованием, временно подавить конкретный CVE, поискать другой базовый образ). Не пытайтесь закрыть «все 200 LOW» сразу — приоритет всегда у CRITICAL и HIGH с доступным фиксом.

Главное лекарство — обновить базовый образ

Большая часть системных CVE лечится одним движением: взять свежий базовый образ. Сопровождающие официальных образов регулярно пересобирают их с пропатченными пакетами.

# Было — старый и, возможно, дырявый
FROM python:3.11.4-slim

# Стало — свежий патч той же ветки
FROM python:3.11-slim

Пересоберите образ — и десятки системных CVE исчезнут, потому что внутри уже обновлённые openssl, libc и прочее. Дополнительно помогает выбор минимального базового образа: -slim или alpine содержат меньше пакетов, а значит — меньше поверхности для уязвимостей. Регулярная пересборка (хотя бы по расписанию в CI) удерживает образ «свежим» сама по себе.

SBOM: опись содержимого

SBOM (Software Bill of Materials) — машиночитаемый список всего, что входит в образ: каждый пакет и его версия. Это как состав на упаковке продукта. Зачем он нужен оборонительно: когда завтра объявят новый громкий CVE в какой-нибудь библиотеке, вам не придётся пересканировать всё — достаточно поискать пакет в сохранённых SBOM и сразу понять, какие образы затронуты.

# Сгенерировать SBOM образа в стандартном формате
docker scout sbom ghcr.io/acme/api:1.4.2

# trivy тоже умеет SBOM (например, формат CycloneDX)
trivy image --format cyclonedx --output sbom.json ghcr.io/acme/api:1.4.2

Как это работает под капотом

Сканер не «угадывает» уязвимости и не запускает код. Он инвентаризует образ: разбирает слои, читает метаданные пакетных менеджеров (база dpkg/apk, файлы requirements.txt, lock-файлы) и составляет список «пакет → версия». Затем сверяет этот список с локально скачанной базой уязвимостей (агрегат из NVD, дистрибутивных трекеров и др.): для каждого пакета известно, в каких версиях есть какие CVE и где их починили. Совпадение версии с уязвимым диапазоном попадает в отчёт. Поэтому критично держать базу свежей — сканер видит ровно то, что в ней есть. Тот же образ, просканированный сегодня и через неделю, может дать разный результат: добавились новые CVE.

Частые ошибки

  • Не сканировать вообще. «У нас же свой код» — но 90% образа чужое. Без сканера вы про дыры просто не знаете.
  • Гнаться за нулём. Попытка закрыть все LOW/MEDIUM выматывает команду. Сфокусируйтесь на CRITICAL/HIGH с фиксом.
  • Сканировать раз и забыть. CVE появляются постоянно. Нужна регулярная пересборка и повторное сканирование, а не разовая проверка.
  • Игнорировать колонку Fixed In. Половина проблем лечится сменой базового образа — а команда вручную правит зависимости.
  • Тяжёлый базовый образ. Полный ubuntu вместо -slim/alpine = лишние пакеты = лишние CVE.

Итоги

  • Образ почти весь состоит из чужого кода; уязвимости приезжают с базовым образом, библиотеками и зависимостями.
  • CVE «появляется» при пополнении базы данных — старый неизменный образ со временем становится «уязвимым».
  • Сканируйте через trivy или docker scout, встраивайте проверку в CI с падением на CRITICAL.
  • Главное лекарство — обновить базовый образ и пересобрать; смотрите колонку Fixed In.
  • SBOM — опись содержимого; помогает быстро находить затронутые образы при новом CVE.
Проверьте себя
1. Образ не менялся полгода, но сканер вдруг показывает новые CRITICAL. Почему?
AОбраз повредился при хранении в реестре
BВ базу данных уязвимостей добавились CVE, которых раньше там не было
CСканер ошибается, образ на самом деле чист
DDocker автоматически подменил слои образа
2. В отчёте сканера у уязвимости в колонке Fixed In указана версия. Что это обычно означает?
AУязвимость не опасна и её можно игнорировать
BУязвимость можно устранить, обновив пакет или базовый образ до указанной версии
CФикса не существует, нужно удалить пакет вручную
DЭто версия самого сканера
3. Зачем оборонительно хранить SBOM (опись пакетов) образов?
AЧтобы образы запускались быстрее
BЧтобы при объявлении нового CVE быстро найти, какие образы содержат уязвимый пакет
CЧтобы уменьшить размер образа
DЧтобы заменить аутентификацию в реестре