SBOM отвечает на вопрос «что лежит внутри артефакта». SLSA отвечает на совсем другой — «как этот артефакт был собран». Это разные слои, и путать их дорого: можно иметь идеальный SBOM и при этом понятия не иметь, что бинарник подменили на скомпрометированном раннере. SLSA Level 2 — это минимальный уровень, который ловит атаки класса SolarWinds. Разберёмся, что он реально даёт и как включить его за пару строк.
Содержание
Открыть содержание
SBOM и SLSA — это про разное
SBOM (Software Bill of Materials) — это опись: список пакетов, библиотек и их версий внутри артефакта. Он бесценен, когда выходит очередной Log4Shell и нужно за минуту понять, затронуты вы или нет. Но SBOM ничего не говорит о том, откуда взялся сам артефакт. Его можно сгенерировать для любого бинарника, в том числе для подменённого.
SLSA (Supply-chain Levels for Software Artifacts) заходит с другой стороны. Это не опись содержимого, а provenance — заверенное описание процесса сборки: из какого коммита, каким пайплайном, на каком build runner-е получился этот конкретный digest. Грубо: SBOM — список ингредиентов на упаковке, SLSA — пломба с записью «произведено на этой фабрике, в эту смену, по этому рецепту».
Поэтому вопрос «SBOM или SLSA» поставлен неверно. Нужны оба: SBOM кормит сканеры уязвимостей, SLSA даёт доверие к самому факту, что артефакт пришёл из вашего пайплайна, а не из чужих рук.
Уровни L1–L3 на пальцах
В SLSA v1.0 уровни описывают, насколько провенанс трудно подделать.
- L1 — provenance просто существует. Вы генерируете описание сборки и прикладываете его. Подделать тривиально: его создаёт тот же процесс, что и артефакт. Польза — в документированности, не в защите.
- L2 — provenance создаётся на хостед build-платформе и подписан ключом, к которому у автора сборки нет прямого доступа. Это ключевой скачок: провенанс генерирует доверенный сервис (сам GitLab Runner на стороне платформы), а не ваш собственный шаг сборки. Подделать его, не скомпрометировав саму платформу, уже нельзя.
- L3 — добавляется усиленная изоляция сборки: непроницаемость между шагами, защита от подмены параметров. Дорого и нужно не всем.
L2 — реалистичная цель для подавляющего большинства команд. Он не требует своей инфраструктуры, ловит самый частый сценарий атаки (подмена на этапе сборки) и включается в GitLab CI одной переменной. L3 — это уже про крупные платформы, регулируемые отрасли и изолированные раннеры.
Почему L2 «перехватывает SolarWinds»
Атака на SolarWinds сработала, потому что злоумышленник внедрился в build-окружение и подменил артефакт после того, как код прошёл ревью. Исходник в git был чистым, подпись релиза была валидной — а в бинарнике сидел бэкдор. Ни код-ревью, ни SBOM такое не ловят: они смотрят на вход и на содержимое, но не на сам процесс сборки.
L2-провенанс закрывает именно этот разрыв. Он привязывает digest артефакта к конкретному коммиту и конкретному пайплайну на доверенном раннере. Если кто-то соберёт «свой» артефакт в обход пайплайна, у него не будет валидного провенанса от вашего builder identity — и проверка на стороне потребителя его отклонит.
Важно понимать, чего L2 при этом не обещает. Он не мешает злоумышленнику с доступом к репозиторию влить вредоносный коммит — это зона код-ревью и защиты веток. Он не гарантирует, что внутри артефакта нет уязвимостей — это зона сканеров. Что он действительно делает — обрубает целый класс атак, где код и релиз выглядят чистыми, а подмена происходит «в тёмной комнате» между ними, на самом билд-сервере. Именно эта комната исторически была слепым пятном: на неё не смотрел никто, потому что «ну собралось же и подписалось». L2 включает там свет и делает любую подмену видимой на проверке.
Сравнение в одной таблице
| SBOM | SLSA provenance | |
|---|---|---|
| Вопрос | что внутри | как собрано |
| Формат | SPDX / CycloneDX | in-toto attestation |
| Что ловит | известные CVE в зависимостях | подмену на этапе сборки |
| Кто может создать | кто угодно для чего угодно | доверенный build runner (L2+) |
| Подделываемость | высокая | низкая (подписан, не вашим ключом) |
| Зачем | приоритизация уязвимостей | доверие к происхождению |
Что нужно, чтобы получить L2 в GitLab CI
Главная хорошая новость: вручную формат провенанса писать не надо — его генерирует сам GitLab Runner. Достаточно включить переменную RUNNER_GENERATE_ARTIFACTS_METADATA, и для каждого build-артефакта job-а рядом появится файл с SLSA-провенансом.
stages: [build]
variables:
RUNNER_GENERATE_ARTIFACTS_METADATA: "true" # runner сам сгенерит provenance
build:
stage: build
rules:
- if: $CI_COMMIT_TAG
script:
- make build # ваш обычный билд
artifacts:
paths:
- dist/app # к этому артефакту runner приложит provenance
Что здесь происходит: ваш script только собирает артефакт, а провенанс создаёт сам раннер — отдельный от вашего кода компонент платформы, к которому build-скрипт не имеет доступа. Именно это разделение и даёт L2: провенанс рождается не тем же шагом, что артефакт. Рядом с dist/app появится artifacts-metadata.json (или <имя>-metadata.json) — это in-toto Statement с предикатом в формате SLSA Provenance v1, и GitLab заявляет для него соответствие SLSA Level 2.
Для контейнерных образов логика та же: собираете и пушите образ, а провенанс на его digest прикрепляете keyless через cosign — тем же блоком id_tokens с audience sigstore, что и при подписи образа: cosign attest --type slsaprovenance --predicate artifacts-metadata.json registry.gitlab.com/acme/app@sha256:....
Проверка на стороне потребителя
Провенанс бесполезен, если его никто не проверяет. У GitLab для артефактов есть CLI glab, который сверяет провенанс через хранилище аттестаций (под капотом — Sigstore/cosign, keyless по OIDC):
glab attestation verify acme/app dist/app
Команда подтверждает, что провенанс выпущен ожидаемым builder identity, и показывает payload — внутри buildDefinition с исходным коммитом, проектом и параметрами сборки. Дальше эти поля сверяют с политикой: «принимаем только из ветки refs/tags/, только из проекта acme/app».
Для контейнерных образов то же делается через cosign verify-attestation --type slsaprovenance с issuer https://gitlab.com, а само правило вешается на admission в кластере — Kyverno умеет проверять не только подпись образа, но и attestation-ы через verifyImages с блоком attestations, сверяя условия по содержимому провенанса.
Что лежит внутри provenance
Полезно представлять, что именно вы проверяете. Провенанс — это in-toto attestation в формате SLSA Provenance v1, и в нём есть несколько ключевых блоков:
subject— список артефактов с ихsha256-дайджестами. Это то, к чему привязан весь документ: провенанс описывает не «образ вообще», а конкретный digest.runDetails.builder.id— кто собирал. Для L2 это идентификатор GitLab Runner-а платформы, а не ваш собственный шаг. Именно это поле сверяют при проверке (черезglabили--certificate-identity-regexpв cosign).buildDefinition.buildType— по какому «рецепту» шла сборка (тип сборки).buildDefinition.externalParameters— входные параметры: репозиторий, ref, триггер. Здесь вы увидитеrefs/tags/v1.2.0и сможете требовать, чтобы релизы шли только из тегов.buildDefinition.resolvedDependencies— зафиксированные источники сборки: коммит исходника и его digest.
Смысл проверки — не «провенанс есть», а «builder.id тот, кому я доверяю, и externalParameters указывают на мой репозиторий и нужную ветку». Без этой сверки наличие провенанса само по себе ничего не доказывает.
Как проверить, что всё работает
Три шага. Первый — что провенанс вообще приложен: для артефакта это показывает glab attestation verify acme/app dist/app, для образа — cosign tree registry.gitlab.com/acme/app@sha256:... (attestation рядом с подписью). Второй — что verify-attestation проходит и payload содержит ожидаемый коммит: проверяйте не факт наличия, а конкретные поля. Третий — негативный тест: соберите артефакт «вручную» (локально, без пайплайна), попробуйте провести его через проверку и убедитесь, что без валидного провенанса он отклонён.
Частые грабли
- Генерируют провенанс собственным шагом. Если собрать «провенанс» своим же скриптом — это L1, а не L2, даже если результат подписан. Весь смысл L2 в том, что провенанс создаёт сам раннер платформы (по
RUNNER_GENERATE_ARTIFACTS_METADATA), к которому ваш билд-скрипт не имеет доступа. Самопальному провенансу можно доверять ровно настолько, насколько вы доверяете собственному (потенциально скомпрометированному) билду. - Проверяют наличие, а не содержимое. Проверка, которая прошла, ещё ничего не значит, если вы не сверили
builder.idи параметры. Пустая проверка пропустит провенанс от чужого проекта, собранного тем же раннером. - Подписывают по тегу, а не по digest. Как и с обычной подписью, провенанс должен быть привязан к
@sha256:. Тег можно переставить, и тогда валидный провенанс будет указывать не на тот образ, что реально запущен. - Считают L2 заменой сканированию. Провенанс не смотрит внутрь артефакта. Без SBOM и сканера у вас будет «достоверно наш дырявый образ».
- Забывают про consumer. Провенанс, который генерируется, но нигде не проверяется на приёме, — это бумажка. Защита появляется только в момент, когда неподходящий provenance что-то блокирует: на admission, в деплой-гейте или в политике реестра.
Ловушка: провенанс ≠ «код безопасен»
L2 доказывает происхождение, а не качество. Он гарантирует, что артефакт собран вашим пайплайном из конкретного коммита — но если в этом коммите уязвимость или закладка, провенанс будет идеально валидным. SLSA и SBOM/сканеры решают разные задачи и не заменяют друг друга: первый отвечает за «это наш артефакт», вторые — за «в нём нет известных дыр». Полная картина — это связка: подпись образа (кто), provenance (как собрано) и SBOM/скан (из чего и насколько дырявое).
Итог
SLSA Level 2 — не «ещё одна SBOM-обёртка», а доказательство того, что артефакт произведён вашим пайплайном, а не подменён на пути от коммита до реестра. Это граница между «доверяем коду» и «доверяем сборке» — и ровно её обходили в громких supply-chain атаках. Цена входа смешная: включить одну переменную в CI и добавить одну проверку у потребителя. С учётом того, что регуляторика (US EO 14028, EU CRA) уже сделала provenance обязательным де-факто, это та гигиена, которую дешевле внедрить сейчас, чем объяснять её отсутствие потом.