Привет! С вами Александр Трифанов, руководитель направления Application Security в Авито. Я почти десять лет занимаюсь пентестами и созданием решений для продуктовой безопасности. В этой статье расскажу про security gates: что это такое, зачем они нужны и как построить проверки, после которых разработчики не будут проклинать команду безопасности (либо я просто не в курсе).
Наш опыт будет полезен тем, кто строит безопасную разработку в большой компании и пытается не превратить все это в боль для разработчиков. Я буду говорить не только про техническую часть, но и про пользовательский опыт: в области гейтов он часто оказывается важнее, чем кажется на первый взгляд.
Что внутри?

Гейты и другие термины: о чем вообще речь
Гейт — это любая проверка, которая принимает решение и может что‑то запретить или разрешить. Неважно, что именно.
В этой статье я буду рассматривать два типа гейтов, которые чаще всего встречаются в разработке.
Quality gates — это гейты качества: они отвечают за соответствие стандартам разработки и общую работоспособность.
Security gates — гейты безопасности, которые фокусируются на отсутствии уязвимостей и соответствии политикам безопасности.
Технически они часто устроены одинаково: есть источник сигнала, политика и enforcement. Но цель разная: quality gates защищают качество и работоспособность, security gates — безопасность и соответствие политикам безопасности.
Дальше я пишу про гейты в целом — с одной важной оговоркой. Все, что касается UX, менее приоритетно для гейтов, которые выполняют жизненно важные проверки (читай: пропустишь — бизнес закроется). Там в любом случае сначала нужно заниматься самим гейтом, а уже потом плавно подкручивать пользовательский опыт.
ASOC — слой агрегации security findings из разных сканеров. SOAR — система оркестрации и автоматизации процессов безопасности. В нашем случае ASOC и SOAR используются как единая точка, где хранится нормализованный вердикт по уязвимости.
Зачем нужны гейты
Вы начали строить в компании безопасную разработку: добавили сканеры, настроили SOAR или ASOC, приняли SLA о сроках исправления уязвимостей. Все чинится, все рады.
И тут прилетает первое нарушение SLA. Команда говорит: «Прости, мы здесь не успеваем в срок, давай отложим на следующий спринт». Вы думаете, что один раз можно закрыть глаза на проблему, и разрешаете подвинуть дедлайн. Разработчики видят, что ничего страшного не произошло, и продолжают нарушать SLA.
Кроме того, SLA могут нарушаться из‑за приоритетов, отсутствия владельца, технического долга, отсутствия фикса, несовместимости обновления, аварий и так далее. Так теряется контроль над рисками, особенно на масштабах крупной компании. Гейт — именно то, что не дает разработчикам совершить проблематичное действие без явной обработки риска безопасностью.

С чего начать внедрение
Любой гейт начинается с источника детектов. Самый простой пример — когда мы вручную тестируем каждый релиз на проникновение. Другое решение — сканер: здесь мы прогоняем статическое тестирование безопасности приложений (SAST). Еще можно использовать монетку и блокировать каждый второй релиз — иногда и это работает:) Суть в том, что источник обязательно должен быть, в каком‑либо виде.

Куда можно вставить гейт? На самом деле куда угодно. Как мы обычно берем жизненный цикл ПО и добавляем к нему безопасность, так и гейт встраивается во все этапы. Хотя, конечно, есть нюансы.
С точки зрения пользовательского опыта я не рекомендую запускать тяжелые сканеры прямо в синхронном пайплайне CI/CD. Сейчас объясню, почему.
Разработчик отправляет пуш на сервер. Сервер отправляет информацию в CI/CD, CI/CD начинает сборку и компиляцию. В том числе туда можно вставить статические сканеры, которые проверяют код на уязвимости. И туда же можно вставить гейт.
Гейт в этом случае может выглядеть очень просто: если статический сканер что‑то нашел — сборка сломалась, пайплайн упал, дальше ничего не пошло.
Почему не надо так делать?
Во‑первых, если сканеры запускаются в CI/CD, вам нужно как‑то управлять ложноположительными срабатываниями (false positive). Скорее всего, вы будете делать это через комментарии в коде или через конфигурационный файл, который лежит рядом с кодом. Значит, разработчик может прийти туда, написать такие же комментарии и вместо вас выключить проверки. Конечно, от этого можно защититься проверками, ревью и пр., но в итоге получается достаточно сложная система. Надежда управлять злоупотреблениями на этом этапе рушится.
Во‑вторых, такой подход дорого поддерживать, и он плохо масштабируется. Допустим, разработчик получил десять уведомлений о проблемах и пошел их исправлять. Исправил девять, а одну проглядел. Потом перезапустил сборку, подождал еще 10–15 минут, снова получил ошибку, расстроился и потратил дополнительное время, пока пайплайн снова отработает. Это боль, которая на масштабе случается постоянно. Если у вас тысячи разработчиков, посчитайте, сколько человекочасов вы потратите зря.
Пайплайн со сканерами (пока без гейта)
Мы в Авито пошли другим путем и наладили примерно такой пайплайн:

Разработчик заводит git push. Сервер с кодом сообщает: вот вся информация о пуше, отправляйся в оркестратор для сканеров.
Дальше оркестратор по своим правилам запускает все необходимое: SAST, который сканирует код, SCA, который сканирует библиотеки, поиск секретов и обязательно какой‑нибудь YAML Security, потому что большинство конфигов живет в YAML, TOML и похожих форматах. Еще вы, наверное, напишете какой‑нибудь сканер, чтобы соответствовать бейзлайну.
Все это агрегируется в ASOC или SOAR, а дальше отправляется разработчику. Желательно автоматически, иначе вы утонете в репортах.
Дальше подробнее о жизненном цикле детекта.
У детекта есть три параметра.
Первый — status. Бывает active и inactive: сканер продолжает видеть сработку в коде и этот код считается активным (например, выкачен в прод) или нет.
Второй параметр — verification_status. Бывает valid, false candidate и false positive. Valid и false positive — обработанные состояния; на первый взгляд может показаться, что детект либо валиден, либо не валиден, но часто возникают условия, когда детект валиден, но чинить его не будем (тестовый код, эксплуатация невозможна, риски отсутствуют и так далее). Для этого мы храним третье поле verification_reason.
Таким образом, детект проходит через следующие стадии:
Детект от сканера попадает в SOAR, status = active, verification_status = false candidate.
↓
В SOAR его ждут разные автоматики: эвристики, LLM, валидаторы (для секретов), в конце цепочки — AppSec (только там, где автоматика дает высокий процент фолзов).
↓
Далее начинает работать автоматика постановки задачи: находится владелец сервиса, формируется задача на исправление с приоритетом и SLA.
↓
Владелец уведомляется о новой задаче. Если задач больше одной — они группируются в одно сообщение.
Гейта в пайплайне пока нет. Это еще не блокировка, а инфраструктура детектов и уведомлений.
Что мы видим с точки зрения девелоперского UX? Здесь встречается два типа реакций.
Первый тип — когда разработчик пришел из сурового энтерпрайза с такими же суровыми безопасниками. Там все SAST гонялись в CI/CD по 30–40 минут на каждую сборку. Он спокойно ждал вердикта, понимал ценность и привык к этому. Он не видит всего этого у нас, потому что наши проверки работают асинхронно, и думает: «Сканов‑то нет, ребята, у нас проблемы». На самом деле он просто пишет хороший код и не получает никаких нотификаций. Он молодец, но считает, что мы не молодцы.
Второй тип — когда люди жалуются, что получают очень много алертов. Они спрашивают у нейросети: «Ты тоже считаешь, что это false positive?» А она им: «Да, я с вами согласна». Нейросети, не обладая полным контекстом, сейчас все еще легко соглашаются с пользователем. То есть те, кто получает больше сработок от сканеров, думают, что получают слишком много фолзов.
У асинхронного сканирования есть еще один нюанс. Всегда остается маленький процент ошибок: где‑то вылезает таймаут, где‑то сканер падает по нехватке памяти — это у SAST вообще любимое. Один слой проверок может что‑то пропустить, но вероятность пропуска снижается, когда несколько гейтов стоят подряд в разных точках жизненного цикла разработки.
Минимальный набор для гейта
Первое, о чем следует задуматься, — качественное управление false positive. Для этого можно использовать как интерфейс, так и настройки. Главное, чтобы разработчик мог сообщить: «Это false positive», — а вы могли завалидировать, что он не ошибся и учел все возможные риски и факторы.
Мы распределили срабатывания фактически по статистике. Отсмотрели их и сказали: правила этих SAST почти никогда не фолзят. У них стоит автоподтверждение, они сразу падают на разработчика. При этом разработчик может прийти и сказать: «Ребята, здесь все‑таки false positive, давайте валидировать». Так мы не заваливаем AppSec задачами.
Второе — дедупликация одинаковых срабатываний. Разработчики часто меняют код. Если один и тот же код чуть‑чуть меняется, а SAST не умеет в дедупликацию, у вас появляется очень много одинаковых срабатываний, которые просто устаревают во времени. SOAR или ASOC обычно помогают с этой проблемой.
Третье — рубильник, он же bypass на случай аварий. Очень важная штука для бизнеса. Если что‑то действительно пошло не так и ваш пайплайн умер, вы должны иметь возможность выключить его, все починить и включить обратно. Тогда бизнес будет счастлив, потому что вы учли его требования и ничего не прилегло из‑за безопасности.

Наш первый гейт: проверка на этапе деплоя
Мы в первую очередь пошли по классике: встаем в CI/CD на шаг деплоя, но не запускаем там SAST. К этому моменту все SAST уже отработали, поэтому мы просто идем в SOAR и спрашиваем, есть ли там активные уязвимости, за которые мы хотим заблокировать разработчика.
Критерий, за что именно блокировать, очень важен. Здесь нужно учитывать SLA. Как только срок по SLA нарушается — вы блокируете. В случае высококритичных уязвимостей блокировку нужно запускать практически сразу при нарушении. На детекты средней критичности допустимо выделять два спринта. Пока эти спринты не закончились, вы не имеете права блокировать. Разработчик спокойно работает и тащит фичи, потому что взял задачу на исправление проблемы в следующий спринт.
Проблема сервисов в режиме поддержки и большая красная кнопка
Часто SLA нарушается у сервисов, которые находятся в режиме поддержки. Разработчики коммитят и деплоят крайне неактивно, сервис спокойно живет годами без изменений. Иногда туда приходит какой‑нибудь багфикс, но это не мешает идиллии. В этом случае гейт на конкретный сервис бесполезен — он просто не отработает.
Мы решили, что так нельзя, и сделали большую красную кнопку, которая может все испортить разработчику, если он игнорирует проблему.
В Авито есть так называемые юниты — группы из нескольких команд разработки. У команд разработки есть микросервисы, у микросервисов есть уязвимости, дальше смерть Кощея по рекурсии. Мы сделали простую админку, где все сервисы сгруппированы по юнитам, и можем вручную заблокировать целый юнит. При блокировке все сервисы юнита не смогут деплоиться. Значит, не получится байпаснуть, сославшись на то, что сервис находится в режиме поддержки.

Одно лишь упоминание большой красной кнопки всегда производит эффект волшебного пенделя: люди начинают суетиться, что‑то делать, срочно планировать исправление.
Заморочка в том, что за шесть лет мы не нажали эту большую красную кнопку ни разу. Кому хочется мешать работе целого юнита по незначительному поводу? И хотя само ‑существование такой кнопки заставляет аккуратнее соблюдать SLA, люди не перестают косячить — они просто делают это по чуть‑чуть.
Позже мы сделали еще одну красную кнопку, только слабее. Но обо всем по порядку.
Pre‑receive: самый левый гейт
Вы помните, что гейт можно вставить куда угодно. Если справа не понравилось, пойдем налево и попробуем сделать pre-receive.

Разработчик делает git push. У всех систем хранения кода есть веб‑хуки, по которым они могут выполнять действия на события. Есть событие pre-receive (название может отличаться): перед пушем со стороны сервера идет синхронный запрос в оркестратор SAST.
Локально это похоже на pre-commit/pre-push, где мы не можем нормально влиять на процесс. Поэтому мы пошли на серверную сторону и обратились к событиям в хранилище кода. Такие события есть практически везде — в GitHub, GitLab, Bitbucket. Но возможны нюансы в зависимости от версий: где‑то проверка асинхронна, где‑то есть жестко заданный таймаут — важно учесть это еще на стадии проектирования.
Запускаются только быстрые сканеры, только часть проверок, потому что мы не можем заставить разработчика долго ждать. Здесь счет идет на секунды — мы ограничились десятью. Важно учитывать сетевые задержки: на само сканирование остается еще меньше времени. После этого мы получаем ответ и решаем, пропускать пуш или отклонять.
Если мы отклоняем, разработчик получает сообщение: есть срабатывание, и мы решили, что это большая проблема, у тебя может быть malware, секрет или что‑то еще. Вот номер уязвимости, с которым ты можешь обработать false positive в нашем канале. Обработка фолзов — это обязательно.

Что мы получили в качестве фидбэка на самый левый гейт?
Во‑первых, оказалось, что есть разработчики, которые вообще не хотят фиксировать конкретные версии библиотек. У них есть requests, и они не хотят думать о каких‑то версиях. «Лучше я потом перепишу весь код, когда выйдет новая версия requests и все мне сломает». Да, такие ребята реально существуют.
Во‑вторых, появилась проблема с кодом, который запускают локально. Особенно это актуально для аналитиков: они никуда не деплоят код, а просто складывают в Bitbucket, чтобы не потерялся. У нас не всегда есть механика, позволяющая понять, что деплоится, а что нет, поэтому люди получают алерты на локальный код. С этим приходится мириться.
Из плюсов: когда к нам приходят с новостью вроде «В библиотеке нашли вредонос, срочно надо блокировать», мы можем сказать, что все уже заблокировано. Ребята радуются.
И снова к нюансам. Если сделать npm update и там попадется та самая версия, которую мы забанили, все сломается. Команда отработает локально, а мы можем потерять ноутбук разработчика, если там есть шифровальщик. Push мы отклоним, но это неудобно.
Так мы пришли к отдельному флоу для библиотек.
Отдельный флоу для библиотек
Разработчик делает запросы из среды разработки на апдейт библиотек или добавление новых. Похожим образом работает CI/CD: он берет список библиотек и идет их скачивать. Все скачивание происходит из репозитория, где хранятся сторонние (third‑party) библиотеки. Встроимся туда!
Красивого Nexus, который сам настроит политики, подтянет фиды и все заблокирует, у нас нет. Есть четыре разных репозитория, и они очень простые. Надо либо самим писать отдельную фичу и коммитить в open source, либо придумывать что‑то рядом.
В крупных компаниях часто работает логика: если система должна учитывать ваши ограничения, иногда проще написать под свою архитектуру ровно то, что нужно.
Мы подумали: окей, у нас есть Nginx, есть домен с репозиторием. Когда мы пишем poetry update (например, для библиотеки реквестов), Poetry идет в репозиторий и запрашивает список доступных версий. Локальный репозиторий возвращает список.
А что если сначала отправлять запрос с Nginx на наш сервис? Условно назовем его Pacman. Видим, что разработчик хочет скачать реквесты с неправильной версией, и возвращаем ошибку. 403, 401 — что угодно.
Тут тоже не все гладко. Если у вас сетевой протокол HTTP/2, вы не можете сделать кастомное сообщение об ошибке в reason phrase, а nginx auth_request не позволяет влиять на другие части запроса. Нужно либо даунгрейдиться до HTTP/1.1, либо придумывать что‑то новое. И вы не можете скрыть версию от Poetry. То есть poetry update все еще ломается.
Нам очень не хотелось идти в проксирование, но пришлось. Сначала Nginx идет в Pacman — это имя нашего самописного сервиса, не путаем с известным пакетным менеджером. Pacman идет в локальный репозиторий за списком версий, фильтрует его и убирает все версии, которые нам не нравятся. В итоге Poetry вообще никогда не узнает о существовании плохой версии. Он спокойно отработает и обновится на доступную версию, подходящую под регулярку. Если человек пытается обновиться именно на запрещенную версию, он все так же получит ошибку.
Такой подход можно реализовать практически для любого репозитория. Общий паттерн похож: пакетный менеджер сначала получает metadata/index (список версий библиотеки), затем скачивает конкретную версию. Но реализация зависит от экосистемы: npm, PyPI, Go, Maven и Cargo требуют отдельных адаптеров и тестов на совместимость.
Мы перехватываем запросы и ответы, модифицируем их как хотим и отправляем обратно. Потом выкатываем это и... ломаем продакшн: оказывается, у Go‑зависимостей есть третья ручка API. Goland при резолве дергает что‑то похожее на go list ‑m all ‑json, которая дергает ручки вида /@latest. Ладно, поддерживаем ее тоже. После этого все работает.
Как запросы npm update попадают в наш сервис? С локальным реестром (local registry) все немного хитрее. Когда платформа или разработчик создает сервис, пакетные менеджеры автоматически конфигурируются использовать по умолчанию наш registry. В случае Python это Poetry, в случае других языков — другие пакетные менеджеры.
Технический контроль простой: CI/CD изолирован от интернета. Если пакет не положили в наш registry, сервис просто не соберется. Конфиги каждого пакетного менеджера кладутся прямо туда, и их можно ранжировать. Локально у вас что‑то заведется, но как только вы запушите код и попробуете собрать и выкатить его через CI/CD, CI/CD скажет: «Таймаут, у меня нет доступа к интернету».
Важная заметка про сервера. Строго говоря, можно найти сервер с доступом к интернету, и там попробовать убрать настройку на локальный registry или переопределить локальную настройку на глобальный. Чтобы защититься от подобных мисконфигураций, пришлось написать целого агента с бейзлайнами безопасности, но это уже совсем другая история…
Как это выглядит на практике? Возьмем JavaScript и npm. В этой экосистеме десятки тысяч библиотек, поэтому мы в первую очередь беспокоились за нее. В пике у нас около 80 тысяч запросов. Задержка — 75 мс на каждый запрос. Можно подумать, что это много, но сам репозиторий отвечает в десять раз медленнее, поэтому для пользователя это незаметно.


С точки зрения UX у нас работает npm update. Он не видит тех библиотек, которые мы не хотим ему показывать. Если нужна версия, которой нет в списках, можно прийти в наш канал и получить одобрение этой версии — мы добавим ее в исключение.

Что делать с уже используемыми библиотеками
Есть проблема, которую таким гейтом не решить. Это уже используемая библиотека, где есть уязвимость. Вы не можете просто взять и сломать людям деплой: прямо сейчас они могут катить фичу или чинить аварию. Не надо им мешать.
Но мы все равно хотим, чтобы все быстро обновились. Гейт помог бы, если бы мы на 100% знали, зачем скачивается библиотека. Но разработчик делает это в локальной разработке, и мы видим, кто скачивает, но не знаем, для какого сервиса, а также — используется уже библиотека или это новая зависимость.
Поэтому мы пошли по пути алертов: делаем неблокирующий алерт на pre-receive и на скачивание. Говорим: «Ребята, в этой версии очень критичная уязвимость, обновляйтесь». И, конечно, репортим задачи. Как только в задачах заканчивает тикать SLA, начинает работать гейт. Но пока SLA тикает, мы просто шлем алерты.
Как мы не покрыли гейтом половину сервисов
Первый гейт мы сделали внутри нашей системы Platform as a Service (PaaS). Это история про упрощение разработки. Фактически создание нового микросервиса сводится к одной команде: вы пишете service create, выбираете имя сервиса, а дальше репозитории, CI/CD, настройки деплоя — все создается само.
Плюс в том, что вы встраиваете проверку гейта в одно место пайплайна и сразу покрываете все сервисы на этой платформе. Дешево, быстро, классно.
Но в какой‑то момент мы пошли смотреть платформенные сервисы и все, что не совсем является продуктом. Доля такого добра разрослась до 50%. А большого гейта там нет.
Спойлер: код и зависимости сервисов при этом сканировались, задачи по ним создавались, то есть не было только блокирующего гейта.
Куда бы встроиться, чтобы устранить этот обход? Мы вспомнили: продукт — это API. API у нас часто меняется.
Есть сервисы, есть gateway, есть Nginx. Куда встраиваться? Если речь про сервисы, далеко не весь API публикуется наружу. SAST все покроет при условии использования общепринятых способов объявления API в коде. С динамическим тестированием (DAST) уже сложнее: нужна какая‑то выборка, поэтому пока туда не лезем. На Nginx очень много wildcard путей вроде prefix*. Соответственно, Nginx вообще не видит появление API по конфигурации.
Остается gateway. Мы обнаружили несколько пользовательских путей для разных gateway. Где‑то публикация — это конфигурационный файл с pull request и деплоем gateway. Где‑то это API, которое нужно дернуть. Где‑то — админка.
Мы решили: если это сервис, который приоткрывает доступ наружу, значит, это калитка. Мы покрыли все пользовательские пути. В случае пул‑реквестов встраиваемся в процесс их апрува. В случае API подготовили свой API. В случае админки появляется зеленая галочка: API проверен, безопасность в порядке, можно публиковаться.
С точки зрения реализации это момент публикации API. Все статические сканеры к этому моменту уже отработали, потому что мы запустили их, когда случился push. Поэтому результаты мы просто забираем из SOAR или ASOC.
А вот с DAST веселее. DAST можно запустить прямо на конкретные API, которые готовятся к публикации.
Почему это важно? Если запустить DAST на весь Авито, вы будете очень долго ждать, и, скорее всего, у вас упадет spidering, потому что он пойдет по объявлениям.
Задачка на разминку:
Реализовать алгоритм спайдеринга страниц на Авито так, чтобы все объявления дедуплицировались по категориям. У объявлений бывают просто ссылки с id /<digits> с локацией и категорией в URL /moskva/koshki/elf_kotenok_<digits>, локацию надо игнорировать. И немного погрустить с того, что DAST‑решения нечасто умеют настолько гибко настраивать свой спайдеринг.
Так что запускаем конкретный API и экономим много времени и ресурсов.
За 90 дней у нас случилось почти 250 блокировок именно при публикации сервиса. Внутри могло быть больше уязвимостей, но мы блокируем, если нашли хотя бы одну.
Казалось бы, замечательно. Но нам мало. Не всегда дело во внешнем API, еще есть как минимум очереди и внутренние RPC‑контракты; хочется иметь механизм для проверки и предотвращения деплоя уязвимого кода еще и посервисно.
Kubernetes, Kyverno и попытка встроиться в деплой
У нас все (почти) живет в Kubernetes — почему бы не встроиться туда? Даже если у вас нет PaaS, вы все равно живете в Kubernetes. Там есть политика приема, контроллеры и прочее.
Если писать политику для Kyverno, которая решает нашу задачу, как она должна выглядеть? Нужно все еще учитывать SLA, соблюдать UX и идти по правильному пути.
Наивный путь такой: есть service deploy, есть Kubernetes. Kubernetes скачивает образ из registry, Kyverno говорит: «Сейчас поищем в этом образе уязвимости». Это минут на десять. Супермедленно, не пойдет.
Давайте заранее просканируем образы, положим метаинформацию в registry рядом и будем проверять ее? Тоже не пойдет, потому что так мы не учтем фолзы. Фолзы в текущей архитектуре обрабатываются на этап позже — в SOAR или ASOC.
Окей, давайте забирать данные из SOAR или ASOC. На первый взгляд рабочая схема. Но не совсем: SOAR или ASOC работает с репозиториями и из коробки не понимает, как docker image мапится на репозитории. Его нужно этому учить...
Сервис между CI/CD, Kyverno и SOAR
Мы решили сделать сервис, который решает проблему подготовки всех необходимых артефактов для принятия решения о блокировке. Требования были следующие:
быстрый ответ;
рубильник на случай аварий;
учет отсутствия фиксов уязвимостей: если фикса нет, мы не можем блокировать;
SLA не тикает, если мы не поставили задачу;
задача не ставится, если мы не знаем, кто владелец (поддержание актуальности владельцев ассетов — большая отдельная тема, она выходит за рамки статьи);
поддержка маппинга на сервис и репозитории, чтобы уязвимости из образа с пакетами и результаты SAST по коду агрегировались и учитывались при блокировках;
многоразовые предупреждения, чтобы блокировка не стала для пользователя сюрпризом;
возможность пропуска деплоя.
Последняя функция — самая спорная и труднореализуемая. Кнопку «Пропустите меня, пожалуйста» нужно жестко контролировать, иначе на нее будут нажимать всегда. Поэтому она срабатывает ограниченное количество раз.
Родился Constable. CI/CD и Kyverno ходят в этот сервис. Он кэширует результаты проверок и регулярно обновляет кэш. В SOAR мы делаем два запроса: первый — по имени репозитория, которое Constable выводит из имени образа; второй — по образу. По образу мы получаем все уязвимости, по сервису — все, что нашлось в коде, включая метаинформацию про SLA, дедлайны, исключения и false positive.
Всегда думаем, что может пойти не так! Например, может случиться баг в системе и Constable начнет блокировать сам себя. Чтобы этого не случилось, в логике сервиса предусмотрено исключение и тест на работоспособность; деплой этого сервиса никогда не блокируется самим сервисом.
Дальше логика такая. SLA тикает. Алерты обязательно летят все время, пока он тикает. Нет фикса — мы ничего не блокируем. (Тут важна оговорка: по критичной проблеме при этом проводится отдельная работа: сделать эксплуатацию невозможной, не пускать проблему в новые сервисы и так далее) SLA не нарушен — мы ничего не блокируем. До блокировки нужно отправить человеку много уведомлений, иначе он скажет, что ничего не знал и не видел. Если SLA нарушен, мы блокируем непосредственный деплой конкретного сервиса.
Так мы создали не очень большую красную кнопку.
Все, что здесь описано, не было одним коротким проектом. Самый первый гейт мы сделали шесть лет назад. Последний, с Kubernetes, еще не раскатан в прод: он работает в режиме алертинга и пока не блокирует. Если считать чистое время разработки, последняя версия заняла у нас примерно полгода. Секреты на pre-receive — тоже около полугода. Registry контейнерных образов — еще полгода. В сумме получается около полутора лет.
Метрики и подводные камни, о которых еще стоит упомянуть
Когда в системе появляется новый элемент, мы должны вспомнить про надежность как элемента отдельно, так и системы в целом.
Сначала мы обеспечиваем прозрачность, то есть создаем дашборд и метрики:
95-й и 99-й перцентиль latency гейта;
доступность;
доля ложноположительных сработок;
override count — количество запросов, которые подошли под условия блокировочного правила, но были только посчитаны и пропущены, а не заблокированы (это очень удобно для отладки новых правил в режиме мониторинга);
заблокированные деплои — наш реальный импакт на безопасность;
возраст кэша.
А вот эти метрики скорее косвенные и больше относятся к управлению уязвимостями, но по ним видно, какой эффект оказывает блокировка деплоя и как долго сработка ждала своего часа:
время до триажа;
время до исправления.
False positive rate. Если гейт будет регулярно и часто блокировать за ложноположительные детекты — никакой UX его не спасет. Поэтому создаем процесс мониторинга за тикетами вне SLA — их мало, а значит — есть возможность проверить их дополнительно с помощью человека. Да, даже после всех предыдущих этапов валидации иногда случаются ложноположительные сработки.
Режимы отказа, или failure modes. Что происходит, если SOAR недоступен? Если кэш устарел? Если registry proxy решил отдохнуть? Если владелец не найден и мы не можем поставить задачу на исправление? Если сканер упал по таймауту? На каждом этапе жизненного цикла уязвимости что‑то может пойти не так. Важно это учесть.
Мы сканируем каждый пуш в репозиторий. Если сканер не смог просканировать текущий пуш, в котором была уязвимость (сеть, система контроля версий не смогла сформировать веб‑хук) — мы увидим проблему на следующем пуше. Если следующего пуша нет — мы увидим проблему на регулярном рескане всей кодобазы.
Сам гейт должен быть очень быстрым. Ему нельзя полагаться на ответы SOAR, который содержит много данных и иногда отвечает небыстро. Для этого держим кэш в гейте, но это ведет к проблеме устаревания кэша: разработчик хочет разблокировку выкатки, как только он исправил проблему, а не спустя сутки. Поэтому делаем API для немедленной синхронизации по конкретному репозиторию.
Если детект есть, репозиторий есть, но владельца нет (привет легаси‑системам), задача не сможет завестись. Нет задачи — нет блокировки, однако есть риски ИБ. Чтобы обработать эту массу, понадобится человек — дежурный с дашбордом подобных проектов «без владельца».
Итог: что важно помнить про гейты
У вас есть две группы пользователей.
Первая — ответственные разработчики. Они понимают, что безопасность важна для бизнеса. Их вы спасаете от невнимательности. Разработчик случайно попытался закоммитить приватный ключ или другой секрет, вы заблокировали действие, и ему не нужно перевыпускать ключ из‑за компрометации.
Вы спасаете и от незнания. Разработчик захотел скачать библиотеку, а там скомпрометированный репозиторий, malware или шифровальщик. Вы его заблокировали и спасли. Это хорошо работает на авторитет процессов безопасности.
Вторая группа — те, кто осознанно пытается обойти процесс. Например, запушить секрет, потому что агент решил, что это единственный путь. Защита от злоупотреблений — обратная сторона гейта.
Для обеих групп очень важно строить адекватный пользовательский опыт, уже на этапе первого запуска. (С оговоркой из начала текста: если речь не про гейты, без которых компанию закрывают.) От этого зависит, как вашу систему будут воспринимать долгие годы.
Те, кто внимательно читал статью, заметили, что мы не блокируем до нарушения SLA, но секреты и malware блокируются сразу. Этот нюанс важно прояснить:
SLA применим там, где уязвимость уже была в сервисе или риски низкие, а стоимость исправления высокая;
SLA не применим там, где уязвимость не попадает в сервис и стоимость исправления низкая.
Несколько коротких рекомендаций:
не блокируйте разработчика раньше, чем нарушен SLA, если он применим;
не запускайте тяжелые сканеры там, где пользователь ждет синхронный ответ;
обязательно проектируйте работу с ложноположительными срабатываниями и дедупликацией;
оставляйте рубильник на случай аварий, но ограничивайте его там, где он превращается в способ обойти процесс;
думайте о UX заранее, а не после того, как вас уже начали проклинать.
И напоследок: если вы встраиваетесь в разрез любого процесса, сразу подготовьте прозрачные метрики, которые будут показывать, что у вас на стороне все работает. Иначе к вам будут регулярно приходить со словами: «Опять эта ваша прокся». Хотя на самом деле лежит репозиторий, а с прокси все в порядке.
Делитесь в комментариях своими хитростями работы с гейтами. Очень жду вашу обратную связь!