Коммитить нельзя сканировать: как мы боремся с секретами в коде
Привет Хабр! Меня зовут Александр Карпов, я работаю в команде защиты приложений ИБ VK. Сегодня я хочу рассказать про наш процесс поиска секретов в каждом коммите в GitLab. У нас, как и у большинства компаний, был классический процесс борьбы с секретами — различные инструменты сканировали кодовую базу, и при обнаружении паролей, токенов и т.д. нам приходилось их ротировать. Главная проблема такого подхода в том, что проверить, действительно ли секрет был инвалидирован, не всегда возможно. По этой причине мы решили, что оптимально будет вычищать кодовую базу от любых секретов.
Предпосылки
Во-первых, Git history. Все, что когда-то попало в VCS, остается в ней навсегда. Многие считают, что если удалить секрет, то его никто не увидит. Но это не так. В истории коммитов этот секрет сохранится. Единственный способ его полностью удалить — это найти и вырезать тот коммит, в котором был добавлен данный секрет.
Во-вторых, Merge Request Conflicts. Как я уже сказал, для удаления секрета из GitLab необходимо вырезать коммит, а это приведет к изменению дерева коммитов, и при слиянии веток возникнут конфликты. Кроме того, если в удаленном коммите были важные изменения кода, то есть риск, что после удаления коммита функциональность приложения будет нарушена.
В-третьих, перевод всех проектов в видимость internal. Как это связано? Раньше все проекты были в видимости private, и если секрет попадал в репозиторий, его могла увидеть только заранее известная группа сотрудников. В internal же к проекту, а значит, и к секрету, могут получить доступ все сотрудники, которые авторизовались в GitLab. Поэтому, чтобы вычищать кодовую базу от секретов, нам необходимо как можно скорее проверять все новые коммиты на наличие в них паролей, токенов и т.д.
Цели
Итак, наша задача — сканировать каждый новый коммит на наличие секретов. В нашем GitLab более 20 000 проектов. Проанализировав активность разработчиков, мы установили для себя, что наш сервис должен выдерживать нагрузку в 500 коммитов в минуту, так как большинство команд вносят изменения под вечер. Дополнительно, нам необходимо укладываться в 5 минут — с момента попадания коммита в GitLab до момента обнаружения и уведомления сотрудника об этом. Связано это с тем, что за это время разработчик не успеет распространить секрет по веткам, и его будет достаточно просто удалить.
Сразу скажу, что классический процесс по борьбе с секретами мы сохранили, но дали разработчикам возможность следовать новому процессу для минимизации трудозатрат на устранение. Сейчас расскажу как, но сперва о том, что у нас уже было.
Что мы уже делали с секретами?
Для поиска секретов в коде у нас есть платформа VK Security Gate, которая различными инструментами ищет секреты в исходном коде. Это процесс не быстрый, так как помимо секретов код проверяется на наличие уязвимостей.
Как я уже говорил, после обнаружения секрета мы создавали задачу на их ротацию. Проверить, действительно ли секрет стал невалидным, в некоторых проектах достаточно проблематично, например, при валидации токена, используемого в межсервисной аутентификации в продуктах с микросервисной архитектурой.
Поэтому мы стали искать возможность, как анализировать все доставляемые изменения в репозитории на наличие в них секретов быстро и удобно для разработчиков.
Какие точки контроля в GitLab у нас есть?
Нам необходимо как-то узнавать о том, что новый коммит попал в репозиторий. Для этого есть несколько механизмов: CI/CD Jobs, Server-side hooks, Webhooks и Git hooks.
Да, можно еще проходиться рекурсивно по проектам и отслеживать дату их обновления с использованием GitLab API, но это очень небыстрый процесс и могут быть ограничения из-за Rate Limits.
CI/CD Jobs
Если использовать CI/CDJobs, то можно встроить проверки в pipeline, который будет каждый раз триггериться при push в репозиторий/ветку. У этого подхода есть несколько плюсов:
Более гибкая настройка под особенности каждого проекта.
Не замедляет push, так как pipeline отрабатывает после того, как изменения попали в репозиторий.
Не блокирует/замедляет процесс выкатки hotfix. При правильной настройке pipeline, проверка коммитов может выполняться параллельно процессу сборки и выкатки.
Но к минусам можно отнести:
Слишком легко обойти/отключить. Если файл конфигурации pipeline хранится в том же репозитории, то разработчик может внести в него изменения и отключить все проверки. Кроме того, даже если файл конфигурации вынесен в отдельный репозиторий, сотрудник с ролью Maintainer и выше может изменить настройки CI/CD, указав свой файл конфигурации pipeline;
Дополнительные затраты по ресурсам на runner. Все jobs выполняются на runner, а значит для добавления проверок придется добавлять на них ресурсы или подключать новые, чтобы не замедлять основные задачи.
Server-side hooks
Server-side hooks — это промежуточные скрипты, которые отрабатывают на стороне GitLab при доставке изменений в репозиторий. Они бывают трех видов:
Pre-receive, Update, Post-receive.
Pre-receive и Update отличаются тем, что Pre-receive запускается один раз на каждый push, а Update запускается на каждую ветку (если в одном push было много веток). Схематично это показано на рисунке ниже.
Post-receive отрабатывает после того, как изменения попали в GitLab, и это не даст возможности блокировать доставку изменений с секретами в репозиторий.
Плюсы Pre-receive и Update в том, что они дают возможность блокировать push и возвращать пользователю ошибку.
А минусы:
Задержки при каждом push в репозиторий. Каким бы оптимальным ни был процесс проверки изменений на наличие в них секретов, все равно это накладывает дополнительное время на обход каждого коммита и проверки с помощью регулярных выражений. При большом количестве изменений данный процесс проверки может занимать достаточно много времени (несколько минут). Не каждый разработчик захочет ждать завершение команды git push больше нескольких секунд.
Риск заблокировать или замедлить hotfix. При обнаружении секретов в коммитах мы можем заблокировать push в репозиторий. Если командам разработки нужно срочно выкатить hotfix, мы можем замедлить данный процесс, что для бизнеса критично. Да, можно написать правила для игнорирования hotfix, но такое возможно только при условии, что команд не много и/или процессы у всех команд едины. В случае, если каждая команда выкатывается так, как ей удобно, писать под всех исключения очень непростая задача.
Сложность поддержки на стороне серверов GitLab. В GitLab нет удобного механизма для поддержки Server-side hooks. Если нам понадобится доработка скриптов, то администраторам придется каждый раз обновлять файлы вручную непосредственно на сервере.
Git hooks
Git hooks — это скрипты, которые выполняются локально на компьютерах разработчиков при создании коммитов. Они бывают нескольких видов: Pre-commit, Prepare-commit-msg, Commit-msg и Post-commit. Что они дают?
Выполняются до попадания изменений в репозиторий.
Выполняются на компьютере разработчика. Когда разработчик хочет локально закоммитить изменения, запускаются git hooks (при наличии).
Но к минусам можно отнести:
Сложно распространять и поддерживать. Для распространения необходимо попросить каждого разработчика скачать скрипты и положить их в определенную директорию ».git/hooks». И при каждом обновлении разработчикам необходимо поддерживать данные скрипты в актуальном состоянии.
Легко отключить. Разработчикам достаточно указать флаг »--no-verify» для того, чтобы отключить проверки.
Webhooks
Механизм в GitLab CE/EE, который позволяет при определенных триггерах отправлять информацию о push на сторонние сервисы. Три вида Webhooks: на проект, на группу и системные (то есть на все проекты и группы в VCS). Что здесь хорошего:
Не замедляют push и выкатки, выполняются в фоновом режиме.
Системные webhook не требуют дополнительных ресурсов при внедрении со стороны команд DevOps-продуктов.
Но Webhooks отрабатывают после того, как изменения попали в VCS, и тут нет возможности блокировать push.
Наш путь
Мы выбрали Webhooks, потому что при внедрении этого механизма меньше рисков (блокировка hotfix, замедление работы GitLab), их легче поддерживать и есть гарантии, что их не отключат (только если по заявке администраторов GitLab). Приятная особенность еще и в том, что они не подвержены подмене автора коммита.
Но по мере использования Webhooks мы столкнулись с некоторыми особенностями данного механизма:
GitLab может отключить отправку на наш сервис уведомлений. Сделать он это может на время (Temporarily disabled) при условии, что наш сервис вернет код ответа 5xx, а может отключить сразу при получении 4xx кода и до момента, пока мы вручную не активируем данный webhook (Permanently disabled webhooks).
Webhook не содержит сами изменения. В нем содержится вся необходимая информация для получения diff у коммита. Однако diff может быть пустой, если изменений слишком много. Это настраивается в GitLab администраторами.
GitLab может отправить два совершенно одинаковых webhooks на один push. В результате могут появиться дубликаты данных.
Учитывая все ограничения и особенности, мы разработали следующую архитектуру для нашего сервиса:
В Gitlab настроены системные hooks (Webhooks) для отправки уведомлений о событиях push на наш Listener. Listener максимально простой сервис, задача которого поймать событие и положить его в Kafka. Core забирает события, получает diff по API, так как Webhooks не содержат сами изменения, и отдает всю необходимую информацию в Scanner. Scanner анализирует diff на предмет наличия в нем секретов. После этого он возвращает в Core массив сработок (если такие были). Далее Core подготавливает информацию для нотификации и укладывает ее в БД. Данный процесс является частью платформы VK Security Gate и использует те же таблицы для укладки данных.
Модуль Notifier с определенной периодичностью забирает из базы данных новые секреты и отправляет уведомление пользователям в корпоративный мессенджер VK Teams. Если сотрудник захочет обработать сработки в мессенджере, Bot обработает нажатие на кнопки, проверит валидность данных (проверка JWT) и переведет статус сработки в нужный.
При этом для поиска секретов используется Gitleaks с некоторыми доработками:
Мы убрали обвязку cmd и оставили только движок для поиска секретов в фрагменте. Это сделано для уcкорения и удобства обработки сработок и ошибок, а также недопущения зомби процессов.
Расширили модель Finding для добавления информации о реальном авторе коммита.
Используем свои собственные Fingerprints.
Немного про Fingerprint
Мы отказались от идеи использовать Fingerprint-инструментов по умолчанию, так как внутри платформы VK Security Gate уже есть много различных инструментов и нам нужен устойчивый универсальный Fingerprint, который бы позволил проводить дедупликацию сработок от разных инструментов.
Для секретов мы долгое время использовали AST Fingerprints. Для их формирования необходимо выкачивать весь проект с исходным кодом, так как в формировании участвует полный путь до секрета. Так как в новом процессе поиска секретов мы работает только со сниппетом кода и нам необходимо использовать тот же Fingerprint для дедупликации сработок, нам бы пришлось выкачивать исходный код проекта. Процесс скачивания не быстрый и на больших объемах с ним может быть много проблем. Поэтому сейчас используется новый Fingerprint, который состоит из пути до файла и захэшированных частей секрета.
Core-компонент отвечает за:
Взаимодействие с Kafka.
Дедупликация не только на уровне конкретного Webhook, но и на уровне найденных ранее секретов.
Интеграция с платформой VK Security Gate.
В случае возникновения ошибок при обработке конкретного webhook вывод его в отдельную очередь ошибок Kafka. При возникновении ошибок нам важно не блокировать основной поток, чтобы другие события могли и дальше оперативно обрабатываться.
Многопоточность. N потоков для обработки Webhooks.
Подготовка данных для уведомления пользователей.
Нотификация пользователей. Здесь при реализации мы уделили внимание следующим моментам:
Rate Limit для конкретного пользователя.
Отключение/включение нотификации всех пользователей для проекта/группы
Группировка секретов по Fingerprint в одно сообщение.
Предоставление возможности обработки секретов как в мессенджере, так и в UI платформы VK Security Gate.
К чему мы пришли
Как я ранее говорил, мы оставили классический процесс поиска секретов. При этом мы даем разработчикам возможность устранять секреты более оперативно и с меньшими затратами путем уведомления их в течение 5 минут. Это значит, что нет необходимости ротировать секрет — достаточно вырезать коммит из истории и сделать force push в репозиторий, чтобы изменить всю историю. При этом не будет конфликтов в дальнейших Merge Request, так как это новые изменения.