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

tempus.build

Слой зависимостей в CI: кеш для скорости, изоляция для безопасности

Почти каждый CI-job начинается одинаково: скачать зависимости. npm install, pip install, docker pull — пакеты из npm, PyPI, Docker Hub и десятка других реестров. Этот шаг — одновременно самый большой рычаг скорости сборки и часть её модели безопасности. Разберём обе стороны и то, как устроен слой зависимостей в tempus.build.

Слева обычный CI: job ходит за зависимостями напрямую в открытый интернет, каждая сборка заново; граница — контейнер. Справа tempus.build: job в одноразовой Kata-VM берёт зависимости из pull-through-кеша по приватной сети, в интернет кеш ходит только при промахе; npm, pip и cargo идут напрямую. Пути к инфраструктуре кластера и cloud-metadata закрыты; Hubble записывает сетевые потоки раннеров, Tetragon на хосте фиксирует признаки выхода из VM.
Одна и та же установка зависимостей: напрямую из интернета против кеша по приватной сети, с аппаратной границей VM вокруг job'а.

Сторона первая: скорость

Без кеша каждая сборка загружает зависимости заново — из публичных реестров, нередко расположенных в другом регионе. mvn, go mod download или docker pull делают сотни мелких запросов к удалённым серверам, и к ним добавляются задержки сети и распаковка. На пайплайнах с тяжёлыми зависимостями это заметная доля времени job’а — и она повторяется на каждом прогоне, хотя пакеты те же.

К тому же публичные реестры иногда отвечают медленно или недоступны.

Сторона вторая: безопасность

Тот же шаг — ещё и риск. CI исполняет недоверенный код: шаги из PR, а главное — сами зависимости и их скрипты установки (postinstall и подобные). Если такой код ничем не ограничен, он может:

  • похитить секреты — токены и ключи из окружения — и отправить их на чужой сервер;
  • связаться с сервером атакующего (C2) и скачать вторую стадию;
  • установить вредоносный или typosquat-пакет либо воспользоваться Атака на цепочку поставки: злоумышленник публикует в публичном реестре пакет с именем вашей внутренней зависимости, и сборка устанавливает чужой код вместо своего. — пакетом в публичном реестре с именем вашей внутренней зависимости.

Из-за этих рисков GitHub не рекомендует self-hosted-раннеры на публичных репозиториях (открывается в новой вкладке) (поэтому tempus.build сейчас обслуживает только приватные репозитории). Вопрос не в том, чтобы запретить коду всё, а в том, чтобы ограничить радиус поражения, если он окажется вредоносным.

Кеш: зависимости приходят по приватной сети

Ускорение строится на том, чтобы не загружать одно и то же дважды. В том же дата-центровом регионе, что и раннеры, работает Кеш-прокси реестра: при первом запросе загружает пакет из upstream-реестра и сохраняет у себя; последующие сборки берут его локально, не выходя в интернет.-кеш на быстром NVMe. Через него идут Docker Hub и другие реестры образов, apt, Go-модули, Maven/Gradle, NuGet, RubyGems и conda-forge:

  • Первый запрос пакета проходит через кеш в upstream-реестр и сохраняется в нём.
  • Последующие сборки берут его из кеша по приватной сети, а не через интернет.
  • Кеш дедуплицирует: слои образов хранятся по хешу один раз и отдаются всем сборкам и раннерам.

Кеш хранит только публичное — по дизайну. Он забирает пакеты анонимно, без ваших учётных данных, поэтому приватные пакеты и образы не может ни получить, ни сохранить. Ваши настройки registry всегда в приоритете: приватный index или registry выигрывает у кеша, и приватные зависимости загружаются напрямую оттуда — у Атака на цепочку поставки: злоумышленник публикует в публичном реестре пакет с именем вашей внутренней зависимости, и сборка устанавливает чужой код вместо своего. нет точки подмены через общее зеркало. По той же логике npm, pip, poetry, uv и cargo идут напрямую в свои реестры: эти инструменты записывают адрес источника в lockfile, и общий кеш оказался бы в файлах, которые вы коммитите.

Раннер по умолчанию настроен на кеш — workflow менять не нужно. Оценка для типичного workflow — 1,8–2,5× (не замер): её дают 4 vCPU у 4core вместо 2 у стандартного ubuntu-latest на приватных репозиториях, bare-metal AMD Ryzen и кеш; на реальных пайплайнах с компиляцией и сборкой образов видели до 5×. Точную цифру для вашего случая покажет собственный пайплайн — подробнее о скорости.

Сдерживание: граница проходит по VM

Скорость — половина выгоды. Вторая половина в том, где проходит граница недоверенного кода. В tempus.build это одноразовая Kata-VM — аппаратная изоляция на каждый job, а не сетевой периметр (как устроена VM-граница — в разборе изоляции Kata). Job — root внутри своей VM, как на GitHub-hosted; граница — VM. Вокруг неё:

  • Пути к инфраструктуре закрыты. В открытый интернет job ходить может, но доступ внутрь кластера (control plane, соседние раннеры) и к cloud-metadata (частый вектор SSRF — кража ролей и токенов узла) заблокирован. Скомпрометированный job по внутренней сети кластера не доберётся до соседних раннеров и control plane.
  • Трафик кешируемых экосистем идёт в кеш по приватной сети — повторное скачивание не выходит за периметр.
  • События сети и хоста записываются. CNI для Kubernetes на eBPF: сетевые политики и микросегментация на уровне ядра, плюс видимость сетевых потоков через Hubble. (Технология ядра Linux: безопасно исполняет программы в ядре для сети, наблюдаемости и безопасности без загружаемого модуля. На ней работают Cilium и Tetragon.-CNI) через Hubble записывает сетевые потоки раннеров; Рантайм-детект на eBPF: в реальном времени отслеживает подозрительное поведение на узле — попытки выхода из изоляции, аномалии инфраструктуры. (Технология ядра Linux: безопасно исполняет программы в ядре для сети, наблюдаемости и безопасности без загружаемого модуля. На ней работают Cilium и Tetragon. на хосте) фиксирует признаки выхода из VM.

Что это даёт вместе

Слой зависимостей закрывает сразу две задачи — за счёт разделения:

  • Скорость — кеш убирает повторное скачивание; заодно держит трафик зависимостей в приватной сети (меньше внешнего контакта).
  • Ограниченный радиус поражения — недоверенный код заперт в Kata-VM, пути к инфраструктуре и metadata закрыты, попытки выхода на хост записываются.

От typosquat и Атака на цепочку поставки: злоумышленник публикует в публичном реестре пакет с именем вашей внутренней зависимости, и сборка устанавливает чужой код вместо своего. защищает прежде всего ваш lockfile с хешами. Кеш от этого не защищает — он отдаёт то, что есть в upstream.

Ограничения

  • Промах кеша всё равно идёт в upstream. Первый прогон новой зависимости не мгновенный — кеш наполняется; выигрыш даёт повторное использование.
  • Исходящий сетевой трафик из раннера — загрузка зависимостей, образов. В облаке за него берут плату, и его нужно контролировать ради безопасности. наружу открыт. Граница недоверенного кода — Kata-VM, а не сеть: закрыты пути к инфраструктуре и metadata, но не весь интернет. Это осознанный компромисс: строгий список по умолчанию останавливал бы сборку на каждой не внесённой в него зависимости.
  • Host-eBPF не видит, что внутри guest-VM — у неё своё ядро. Tetragon фиксирует попытки выхода на хост, а не события внутри гостя; это дополняет VM-изоляцию, а не заменяет её.
  • Кеш не резервируется и не гарантирует сохранность. Целостность самих пакетов держится на их хешах и подписях и вашем lockfile, а не на «доверии зеркалу».

Три стороны одной модели

Это тот же подход, что и в supply chain раннер-образов (что внутри образа) и аппаратной границе Kata (что образ может сделать хосту) — здесь речь про третью сторону: откуда берутся зависимости и как ограничен job. Полная картина и ссылки — на странице «Безопасность и изоляция».

← в блог