SBOM для LLM-шлюза нужен каждой релизной сборке
SBOM для LLM-шлюза: как фиксировать версии, сканировать пакеты и контейнеры, подписывать сборки и отзывать уязвимый релиз.

LLM-шлюз часто считают тонким HTTP-сервисом: принял запрос, выбрал модель, передал ответ. В production это один из самых насыщенных зависимостями компонентов. Он принимает внешний трафик, хранит ключи провайдеров, разбирает JSON, пишет аудит, применяет маскирование PII, работает с очередями, базами данных, SDK и контейнерной платформой. Ошибка в любой части этой цепочки может затронуть каждый вызов модели.
Поэтому SBOM для LLM-шлюза нужен не ради папки для аудита. Он должен отвечать на неприятный вопрос за несколько минут: «Какая именно сборка сейчас обрабатывает запросы, из чего она состоит, где ещё запущена и как её остановить?» Если вместо ответа команда открывает несколько репозиториев, ищет тег в реестре и спорит, какой образ попал в кластер, SBOM у неё есть только на бумаге.
SBOM фиксирует состав выпуска, а не намерения команды
SBOM должен описывать то, что вы поставили, а не то, что разработчики собирались поставить. В нём нужны идентификатор компонента, версия, источник, хеш или другой способ проверки целостности, лицензия, связи с другими компонентами и сведения о самом выпуске.
SPDX определяет SBOM как набор элементов, описывающих один пакет, и позволяет передавать состав, происхождение, лицензии и сведения о проблемах качества или безопасности. В спецификации связи между элементами являются частью модели, а не декоративным полем. Это важно для шлюза: библиотека могла попасть в образ напрямую, через SDK провайдера или через системный пакет базового слоя.
Многие команды делают проще: генерируют JSON из исходного каталога, прикладывают его к релизу и считают задачу закрытой. Такой файл не знает, какие зависимости исключил multi-stage build, какие системные пакеты принёс базовый образ, какой бинарник добавил скрипт установки и что реально оказалось в финальном слое. Он полезен как ранний сигнал для разработчика, но не годится для решения об отзыве работающего образа.
Разделяйте четыре вещи, которые часто называют одним словом «состав»:
- lock-файл фиксирует разрешение зависимостей в экосистеме языка;
- SBOM исходников описывает дерево зависимостей до сборки;
- SBOM контейнера описывает содержимое поставляемого образа;
- provenance связывает образ с исходной ревизией, окружением и процессом сборки.
Если перепутать эти артефакты, команда получает ложную уверенность. Например, requirements.txt может быть аккуратным, а финальный образ содержит устаревший openssl из базового дистрибутива. Или наоборот: сканер репозитория видит библиотеку в тестовой зависимости и блокирует выпуск, хотя в production-образ она не попадает.
У LLM-шлюза состав шире lock-файла
Для LLM-шлюза единицей контроля должен быть исполняемый путь запроса, а не один репозиторий. В карту состава входят компоненты, которые участвуют в приёме, обработке, маршрутизации и журналировании запросов.
Начните с конечного образа сервиса. В нём обычно есть runtime языка, HTTP-сервер, TLS-библиотеки, парсер JSON, клиентские SDK, библиотеки токенизации, драйверы хранилищ, агент телеметрии и системные пакеты. Если шлюз поддерживает локальные модели, список расширяют сервер инференса, CUDA- или иные ускоряющие библиотеки, загрузчики весов и сами артефакты моделей.
Затем добавьте соседние артефакты, но не сливайте их в один бесформенный документ. Отдельно ведите:
- образ самого шлюза;
- образ воркера, если он обрабатывает асинхронные задания;
- образ прокси или адаптера, если он выпускается вашей командой;
- пакет расширения, который загружается во время запуска;
- файл весов и конфигурацию локальной модели.
Файл весов модели не является библиотекой Python, а токенизатор не равен контейнеру. Для них нужны похожие дисциплины: неизменяемый идентификатор, криптографический хеш, источник, лицензия, процедура допуска и владелец. Но отдельная инвентаризация модели полезнее, чем попытка спрятать гигабайтный артефакт в обычный SBOM пакетов. Иначе при вопросе «какая версия весов работала в понедельник?» вы увидите длинный список apk и pip, а не получите ответа.
Не забудьте о конфигурации, которая меняет поверхность атаки. В SBOM обычно не записывают секреты, полные URL и содержимое правил. Но релиз должен ссылаться на версию схемы конфигурации и на digest безопасного конфигурационного пакета. Это позволяет отличить один и тот же образ, запущенный с отключённой проверкой TLS, от того же образа с корректной настройкой. Секреты и персональные данные в SBOM не попадают никогда.
Версии надо закреплять так, чтобы сборку можно было повторить
Тег образа, плавающая версия библиотеки и загрузка скрипта из сети делают выпуск неповторяемым. Сборка, которая вчера использовала одни байты, завтра под тем же именем может собрать другие. Для шлюза с ключами провайдеров и внешним трафиком это плохая сделка.
Фиксируйте зависимости на трёх уровнях. На уровне языка используйте lock-файл с хешами артефактов, где это поддерживает экосистема. На уровне контейнера указывайте базовый образ по digest. На уровне CI фиксируйте версии генератора SBOM, сканера, сборочного образа и действий, которые запускает пайплайн.
Плохой Dockerfile выглядит привычно:
FROM python:3.12-slim
RUN pip install openai fastapi uvicorn
COPY . /app
Он не даёт ответа, какой именно Python и какие колёса попали в образ. Теги меняются, а pip install разрешает зависимости во время сборки. Даже если приложение ведёт себя одинаково, расследование инцидента превращается в реконструкцию прошлого по логам внешних репозиториев.
Минимальный принцип для production-сборки выглядит иначе:
FROM registry.example/base/python@sha256:<зафиксированный-digest>
WORKDIR /app
COPY requirements.lock ./
RUN pip install --require-hashes -r requirements.lock
COPY . ./
Здесь placeholder не является готовой конфигурацией. Команда должна подставлять фактический digest из проверенного внутреннего реестра и обновлять его через контролируемый pull request. --require-hashes полезен тем, что прекращает установку, когда скачанный пакет не совпал с разрешённым содержимым. Он не заменяет проверку уязвимостей, но мешает незаметно подменить артефакт под знакомой версией.
Популярный совет «обновляйте всё каждую ночь» звучит безопасно, но ломает управляемость. Автоматический pull request на обновление допустим. Автоматический выпуск нового набора зависимостей в production без оценки изменений создаёт другой риск: вы не знаете, какая правка вызвала ошибку маршрутизации, рост задержки или несовместимость SDK. Обновление должно оставлять след: исходная ревизия, старый и новый digest, различие SBOM, результаты тестов и решение о выпуске.
Один SBOM на репозиторий не отвечает на вопрос об образе
Генерируйте SBOM после сборки финального образа и привязывайте его к неизменяемому digest. Именно этот файл нужен дежурному инженеру при CVE, аудитору при проверке и контроллеру допуска перед запуском в кластере.
Syft умеет строить SBOM для образов и файловых систем, а также выводить SPDX и CycloneDX. Для практики я бы начал с одного формата, который умеют обрабатывать ваши сканеры и хранилище, и добавил второй только по требованию потребителя. Сам инструмент прямо показывает такой сценарий: он создаёт SPDX JSON и CycloneDX JSON из одного образа.
Пример команды для локальной проверки:
IMAGE=registry.example/llm-router@sha256:<digest>
syft "$IMAGE" -o spdx-json=sbom.spdx.json
sha256sum sbom.spdx.json
Ожидаемая форма вывода второй команды такая:
8f1c...c92a sbom.spdx.json
Сохраните этот хеш вместе с digest образа. Сам JSON нельзя считать неизменяемым только потому, что он лежит в CI-артефактах: многие системы очищают их по сроку хранения, а некоторые разрешают заменить файл с тем же именем. Хранилище должно удерживать релизные артефакты столько, сколько вы обязаны поддерживать или расследовать выпуск.
В CI добавьте проверку, что SBOM получен именно от digest, а не от тега. Допустим, пайплайн собрал registry.example/llm-router:build-482. Сначала он публикует образ, затем получает его content digest из реестра, и только после этого генерирует SBOM для адреса @sha256:.... Если генератор читает тег, параллельный push может сменить содержимое до сканирования. Такая гонка редка до первого инцидента и очень неприятна после него.
Для исходников можно выпускать второй, вспомогательный SBOM. Он помогает находить риск до контейнерной сборки и сравнивать изменения в pull request. Просто назовите его честно, например source-sbom, и не подменяйте им SBOM релизного образа.
Сканер находит известное, а решение принимает политика
Сканер сопоставляет компоненты с базой известных уязвимостей. Он не понимает ваш поток данных, не знает, доступен ли код по сети, и не может сам решить, нужно ли отключить релиз. Поэтому правило «любая CVE блокирует всё» быстро превращается в очередь ложных тревог, а правило «разберёмся позже» оставляет опасные сборки работать месяцами.
NIST в SSDF рекомендует встроить безопасные практики в жизненный цикл разработки, чтобы уменьшать число уязвимостей в выпущенном ПО, снижать последствия неустранённых проблем и устранять причины повторения. Это не инструкция поставить один сканер. Это требование к процессу, в котором результат сканирования получает владельца и действие.
Политика для LLM-шлюза должна учитывать как минимум четыре свойства находки:
- есть ли исправленная версия и можно ли её обновить без изменения API;
- присутствует ли компонент в финальном образе;
- достижим ли уязвимый код через внешние запросы, админский интерфейс или обработку файлов;
- затрагивает ли проблема секреты, изоляцию арендаторов, аудит или выполнение кода.
Возьмём типичный сбой. Сканер нашёл уязвимость в библиотеке архивов внутри базового образа. Команда ставит исключение, потому что «шлюз архивы не принимает». Через две недели команда добавляет endpoint для загрузки пакетных задач, библиотека начинает обрабатывать входной ZIP, а исключение остаётся. Нужна не вечная пометка ignore, а запись с причиной, владельцем, сроком пересмотра и условием отмены: например, «исключение допустимо только пока production endpoint не принимает архивы».
Другая крайность тоже опасна. Если CI падает на уязвимости в неиспользуемом пакете и никто не может выпускать исправления, инженеры начнут отключать сканер целиком. Разделите пороги: блокируйте выпуск при критичной или высокой проблеме в достижимом production-компоненте, требуйте явного решения для остальных, а исключения автоматически возвращайте на пересмотр. У политики должен быть владелец из инженерной команды, а не безымянная таблица из службы безопасности.
Контейнер надо проверять как готовый исполняемый артефакт
Сканирование репозитория не заменяет сканирование контейнера. Финальный образ может унаследовать старые системные пакеты, содержать случайно скопированный файл конфигурации, включать shell-утилиты, отладочный сервер или бинарник, которого нет в исходниках приложения.
Проверка должна идти после того, как сборщик сформировал финальный образ, и до того, как этот digest станет кандидатом на развёртывание. Отчёт сканирования привязывайте к тому же digest, что SBOM. Если сканер умеет читать SBOM, это удобно: повторное сканирование после обновления базы уязвимостей можно запустить без реконструкции старого образа. Но перед внедрением проверьте совместимость версий генератора, формата и сканера в тестовом пайплайне. Даже инструменты из одной экосистемы иногда меняют поддержку формата несинхронно.
Одно из разумных правил допуска выглядит так:
- CI собирает образ из закреплённых входных артефактов и публикует его по digest.
- Генератор создаёт SBOM именно для этого digest, затем хранилище принимает оба файла как один релизный набор.
- Сканер проверяет образ или SBOM по актуальной базе, а политика решает, можно ли продолжать.
- Пайплайн подписывает образ и его сведения о сборке только после успешных проверок.
- Среда запуска принимает только digest с доверенной подписью и утверждённым статусом.
Не пытайтесь вписать в эту проверку содержимое промптов, ответы моделей или ключи API. Это не компоненты поставки и не должны попадать ни в SBOM, ни в отчёт сканера. Для таких данных существуют отдельные меры: маскирование, политика журналирования, ограничение доступа и сроки хранения.
Подпись связывает SBOM с конкретной сборкой
SBOM без связи с образом легко подменить или перепутать. Два файла с именем sbom.json не доказывают, что один относится к другому. Нужна проверяемая цепочка: исходная ревизия привела к сборке, сборка выпустила образ с digest, а этот образ получил конкретный SBOM и отчёт сканирования.
SLSA использует provenance как сведения о том, где, когда и как создан артефакт. Практический смысл проще терминологии: после инцидента вы должны отличать образ, собранный вашим доверенным пайплайном из ожидаемой ревизии, от образа, который кто-то собрал локально и загрузил под похожим тегом.
Подпись решает вопрос авторства и целостности только тогда, когда среда запуска её проверяет. Подписать образ в CI и разрешать кластеру скачивать любой тег из реестра бессмысленно. Контроллер допуска или другой механизм деплоя должен сверять как минимум:
- образ указан по digest, а не плавающему тегу;
- digest имеет подпись от доверенного издателя;
- аттестация относится к этому же digest;
- provenance указывает на разрешённый репозиторий и ревизию;
- статус уязвимостей не запрещает запуск.
Не доверяйте полю version в манифесте больше, чем digest. Версия нужна человеку, digest нужен автоматике. Хорошая релизная запись содержит оба: понятное имя вроде 2026.07.23.4 и неизменяемый идентификатор содержимого. Если команда вручную меняет тег после подписи, она должна создавать новый релизный набор, а не пытаться «переназначить» существующую подпись.
Отзыв сборки должен работать в ночном инциденте
Отзыв нельзя сводить к удалению тега из реестра. Работающий pod уже скачал образ. Автоскейлер может держать старую версию. У инженера может остаться локальная копия. Другой кластер может использовать digest напрямую. В момент инцидента вам нужны данные о распространении выпуска и заранее утверждённая последовательность действий.
Процедура начинается с идентификации. Дежурный получает CVE, ошибку поставщика или сообщение о компрометации. Он находит затронутый компонент по SBOM, получает список образов с совпадающим компонентом, затем список развёртываний по digest. Если SBOM нельзя запросить по названию компонента и версии, вы храните архив, а не рабочий инструмент.
Дальше действует заранее назначенный владелец релиза. Он принимает одно из трёх решений: временно ограничить путь трафика, заменить образ на исправленный или остановить сервис. Для шлюза часто опасно просто выключить всё: бизнес-процессы могут зависеть от авторизации, обработки обращений или внутренних ассистентов. Но это не повод оставлять известный риск без срока. Временное снижение риска должно иметь конкретное техническое действие, например отключение уязвимого endpoint, запрет загрузки вложений или ограничение доступа только внутренней сетью.
После замены образа проверьте факт отзыва, а не только статус CI:
- нет ли затронутого digest среди запущенных workload в каждом кластере;
- не указывает ли deployment, job или cron-задача на старую версию;
- блокирует ли политика допуска повторный запуск отозванного digest;
- перестал ли реестр выдавать право на публикацию и развёртывание этого выпуска;
- сохранились ли SBOM, отчёты и журнал решений для разбора инцидента.
Самая частая дыра здесь не техническая, а организационная. Без списка владельцев, канала связи и права остановить релиз дежурный собирает согласования, пока уязвимый шлюз принимает трафик. Проведите учебный отзыв на безопасном тестовом digest. Засекать людей не нужно. Нужно обнаружить, что у вас нет доступа к реестру, не видно второго кластера или политика допуска проверяет только теги.
Чеклист должен стать условием выпуска
SBOM работает, когда его выпуск и проверка входят в обычный релизный маршрут. Отдельная ежеквартальная задача всегда проигрывает срочному исправлению, миграции модели и дедлайну продукта.
Проверьте свой LLM-шлюз по этому списку:
- Каждая production-сборка имеет immutable digest, SBOM конечного образа, отчёт сканирования и запись об исходной ревизии.
- Lock-файлы и базовые образы закреплены, а CI не скачивает непроверенные зависимости под плавающими версиями.
- Политика сканирования различает наличие пакета, достижимость, серьёзность, исправление и временное исключение.
- Реестр и кластер не допускают ручной запуск неподписанного или отозванного digest.
- Команда может по компоненту найти все затронутые релизы и по релизу найти все активные развёртывания.
SPDX полезен именно потому, что даёт общий язык для состава, происхождения и лицензий, а NIST SSDF полезен тем, что требует встроить безопасность в инженерный процесс. Но ни стандарт, ни сканер не построят цепочку принятия решений вместо вас.
В AI Router такой контроль особенно уместен для шлюзов, которые маршрутизируют запросы между внешними провайдерами и собственными моделями: состав выпуска, границы обработки данных и история изменений нельзя восстанавливать задним числом. Начните не с закупки нового сканера, а с одного образа, который сегодня можно связать с digest, SBOM, отчётом и рабочей процедурой отзыва. Если этого нельзя сделать за один рабочий день, следующий инцидент покажет причину слишком наглядно.
Часто задаваемые вопросы
Чем SBOM отличается от package-lock.json или poetry.lock?
Нет. Lock-файл фиксирует зависимости одного языка или менеджера пакетов, а работающий шлюз включает ещё базовый образ, системные пакеты, бинарники, плагины, конфигурацию и собственный код. SBOM должен описывать именно поставляемый артефакт, а не только исходный репозиторий.
Какой формат SBOM выбрать для LLM-шлюза: SPDX или CycloneDX?
Для начала выберите один формат, который умеют читать ваш сканер, хранилище и аудит. SPDX хорошо подходит, когда нужны связи, происхождение и лицензии; CycloneDX часто удобен для процессов управления уязвимостями. Хуже всего выпускать два формата без владельца и проверять ни один.
Нужно ли фиксировать digest базового контейнера?
Да, если образ может дойти до production. Тег вроде python:3.12-slim меняется, поэтому он не доказывает, из какого базового слоя собран конкретный релиз. Фиксируйте образ по digest и сохраняйте этот digest рядом с SBOM и подписью.
Что делать, если сканер нашёл CVE без доступного патча?
Сначала подтвердите, что уязвимый пакет действительно есть в опубликованном образе, затем оцените достижимость и наличие исправления. Критичная уязвимость в пути обработки внешнего HTTP-трафика требует немедленного решения, а запись без исправления или в неиспользуемой утилите требует обоснованного исключения с датой пересмотра.
Достаточно ли удалить тег контейнера после инцидента?
Нет. Удаление тега не останавливает уже запущенные экземпляры и не мешает кому-то развернуть образ по сохранённому digest. Отзыв включает блокировку допуска в кластере, остановку или замену workloads, удаление доступа к реестру и проверку, что трафик больше не попадает в уязвимую версию.
Нужен ли отдельный SBOM для self-hosted модели?
Да, но их нужно разделять. SBOM образа описывает код и пакеты, а отдельная инвентаризация моделей фиксирует файл весов, источник, лицензию, хеш, параметры запуска и дату допуска. Смешанный документ быстро становится непроверяемым и не отвечает ни на один вопрос хорошо.
Какие артефакты хранить вместе с SBOM?
Минимум храните SBOM, digest образа, идентификатор исходной ревизии, данные о сборке, результат сканирования и подпись или аттестацию. Эти записи должны иметь общий идентификатор релиза, иначе во время расследования команда будет вручную сопоставлять несвязанные файлы.
Нужно ли проверять подпись образа в Kubernetes?
Да, если инфраструктура позволяет выполнить эту проверку до запуска workload. Контроллер допуска должен сверять digest с разрешённым списком, наличие подписи и связанной аттестации. Проверка только в CI полезна, но не защищает от ручного деплоя или подмены тега после сборки.
Как быстро нужно отзывать уязвимую сборку?
Это зависит от уязвимости и типа релиза, но порядок действий надо утвердить заранее. Для уязвимости, которая доступна по сети или затрагивает секреты, счёт идёт на часы; для плановой замены библиотеки допустим обычный релизный цикл. Процедура без конкретных владельцев и канала связи не сработает в ночном инциденте.
Как проверить, что SBOM относится к нужному контейнеру?
Не принимайте документ на веру. Сверьте, что его subject совпадает с digest образа, подпись принадлежит доверенному издателю, а метаданные сборки указывают на ожидаемый репозиторий и ревизию. Если эти связи нельзя проверить автоматически, SBOM остаётся справкой, а не контролем.