На проверке Secrets Management в CI/CD: обнаружение утечек ключей и токенов с Trufflehog, Gitleaks и Vault

Латунный ключ наполовину погружён в растекающуюся чернильную лужу на тёмной матовой поверхности. Тёплый свет лампы выхватывает гравировку на ключе, по краям кадра — бирюзовая темнота.


По данным GitGuardian State of Secrets Sprawl Report 2024 (данные за 2023 год) - 12,8 миллионов новых секретов на публичном GitHub. И значительная часть скомпрометированных сред приходится не на рабочие станции разработчиков, а на CI/CD-раннеры. Пайплайны - главная поверхность атаки, а не ноутбук условного джуна.

На одном из инцидентов, которые я разбирал, один AWS-токен в публичном репозитории превратился в десятки EC2-инстансов для майнинга - за ночь. Токен был закоммичен в .github/workflows/deploy.yml, удалён через несколько часов. Бот использовал его в первые минуты после появления. Удаление из HEAD не удаляет секрет из git-истории - и вот тут начинается настоящая работа по secrets management в CI/CD и обнаружению утечек ключей.

Как атакующий крадёт секреты из CI/CD-пайплайна​

Прежде чем прикручивать сканеры, стоит разобраться с атакующей стороной. CI/CD-пайплайн - система с максимальными привилегиями: она деплоит в продакшн, пушит образы в реестры, создаёт инфраструктуру. По матрице MITRE ATT&CK атаки на секреты пайплайнов покрываются сразу несколькими техниками.

Reconnaissance - сканирование публичных репозиториев (T1593.003, Code Repositories). Боты прочёсывают GitHub и GitLab непрерывно, и по данным исследований NCSU и GitGuardian, от появления секрета в публичном коде до первой попытки его использования проходит от секунд до нескольких минут. Если атакующий уже получил доступ к приватному репозиторию и вытаскивает оттуда секреты - это T1213.003 (Code Repositories, Collection).

Credential Access - извлечение учётных данных из файлов конфигурации (T1552.001, Credentials In Files) и приватных ключей (T1552.004, Private Keys). Сюда же - атака на облачные хранилища секретов (T1555.006, Cloud Secrets Management Stores).

Execution - Poisoned Pipeline Execution (концепция OWASP CICD-SEC-4; соотносится с ATT&CK T1195.002, Compromise Software Supply Chain). Атакующий модифицирует YAML-конфигурацию пайплайна - через pull request, через компрометацию зависимости, через перехват third-party action - и заставляет раннер слить секреты. Исследователи Synacktiv показали этот подход с инструментом Nord Stream: он автоматически создаёт ветку, внедряет pipeline YAML, запускает сборку, забирает секреты из output и зачищает следы. Красиво и страшно одновременно.

Initial Access - использование утёкших учётных данных как Valid Accounts (T1078) или компрометация зависимостей (T1195.001, Compromise Software Dependencies). Инцидент с Codecov - классика: скомпрометированный Bash Uploader собирал переменные окружения из CI-раннеров тысяч компаний.

Конкретные вектора утечки секретов GitHub Actions и других платформ:
  • Hardcoded secrets в YAML/Dockerfile - ключ прямо в env-блоке workflow. Даже в приватных репозиториях это больно: по оценкам GitGuardian (State of Secrets Sprawl 2024), приватные репозитории содержат hardcoded-секреты значительно чаще, чем публичные.
  • Секреты в логах сборки - printenv, verbose-флаг terraform или echo $SECRET выплёвывают значения переменных в build log, доступный всей команде. Я видел это в продакшн-пайплайнах чаще, чем хотелось бы.
  • Docker layers - COPY .env /app/.env с последующим RUN rm /app/.env бесполезно: секрет остаётся в промежуточном слое образа. Удаление файла - не удаление данных.
  • pull_request_target в GitHub Actions - этот event имеет доступ к секретам репозитория, и если workflow чекаутит код автора PR - прямой путь к эксфильтрации.
  • Чрезмерный scope секретов - org-level секрет в GitHub Actions доступен всем репозиториям организации. Компрометация одного repo = утечка всех org-секретов.

Trufflehog и Gitleaks: сканирование секретов и поиск ключей в git​

Два основных open-source инструмента для обнаружения утечек токенов в репозитории - TruffleHog (Truffle Security Co., активно поддерживается) и Gitleaks (Zach Rice, активный проект). Оба бесплатны, оба прикручиваются к CI, но работают принципиально по-разному.

Entropy-based vs regex-based: почему это важно​

TruffleHog комбинирует regex-паттерны для известных форматов (AWS AKIA…, GitHub ghp_…) и entropy-анализ для высокоэнтропийных строк, которые могут оказаться секретами. Главная фича - верификация: TruffleHog пытается валидировать найденный секрет через API провайдера (например, делает тестовый вызов к AWS STS). Это радикально режет false positives, но требует сетевого доступа.

Gitleaks делает ставку на regex-правила с поддержкой кастомных конфигураций. Из коробки покрывает несколько сотен паттернов: ключи AWS, GCP, Azure, Slack, Stripe и десятки других. Entropy-анализ доступен как опция, но основной режим - pattern matching. Gitleaks быстрее на больших репозиториях и работает полностью offline.

По моему опыту, в боевых условиях лучше использовать оба: Gitleaks в pre-commit (быстрый, offline), TruffleHog в CI (медленнее, зато с верификацией - и шума меньше).

Требования к окружению​

  • ОС: Linux, macOS, Windows (Gitleaks - нативный бинарь; TruffleHog - Go-бинарь или Docker)
  • RAM: 2 ГБ минимум, 4 ГБ рекомендуется для репозиториев с историей >10 000 коммитов
  • Зависимости: Git 2.x+; Docker (опционально, для TruffleHog через trufflesecurity/trufflehog)
  • Сеть: TruffleHog с верификацией требует интернет; Gitleaks работает полностью offline
Запуск TruffleHog по всей git-истории: trufflehog git file://./repo --only-verified - флаг --only-verified оставляет только подтверждённые секреты. Для сканирования GitHub-организации целиком: trufflehog github --org=your-org --token=$GH_TOKEN.

Gitleaks для полного аудита репозитория: gitleaks detect --source=./repo --report-format=json --report-path=results.json. Для проверки только незакоммиченных изменений: gitleaks protect --staged.

Trufflehog vs Gitleaks: trade-off таблица​

КритерийTruffleHogGitleaks
Метод детекцииRegex + entropy + API-верификацияRegex + entropy (опционально)
False positive rateНизкий (за счёт верификации)Средний (зависит от конфигурации правил)
СкоростьМедленнее (сетевые вызовы верификации)Быстрее (чистый pattern matching)
Offline-режимЧастично (без верификации)Полностью
Кастомные правилаЧерез detectors на GoTOML-конфиг, быстрая настройка
CI-интеграцияGitHub Actions, GitLab CI, pre-commitGitHub Actions, GitLab CI, pre-commit
Покрытие источниковGit, GitHub/GitLab orgs, S3, файловая системаGit-репозитории, файловая система
Когда использоватьАудит с минимумом шума; нужна верификация активности секретаБыстрое сканирование в CI; кастомные паттерны; offline-среды
Когда НЕ использоватьИзолированные среды без сетевого доступа; нужна скоростьНужна верификация активности секрета; сканирование облачных хранилищ

Защита API ключей в пайплайне: три слоя secret scanning DevSecOps​

Один инструмент - не стратегия. Работающая защита от утечки секретов в пайплайне строится из трёх слоёв, каждый ловит то, что пропустил предыдущий.

Слой 1: Pre-commit - блокировка до попадания в git-историю​

Pre-commit хук с Gitleaks останавливает коммит, если в staged-файлах обнаружен паттерн секрета. Настройка через .pre-commit-config.yaml:
YAML:
repos:
  - repo: https://github.com/gitleaks/gitleaks
    rev: v8.21.2
    hooks:
      - id: gitleaks
После pre-commit install каждый git commit прогоняется через Gitleaks. Если разработчик случайно добавил строку вида AKIAIOSFODNN7EXAMPLE - коммит не пройдёт. Но есть нюанс: pre-commit работает только на машине разработчика и легко обходится через --no-verify. Поэтому это первая линия обороны, а не замена CI-сканированию.

Слой 2: CI-пайплайн - сканирование в Pull Request​

Второй слой - запуск TruffleHog или Gitleaks как шаг CI при каждом PR. В GitHub Actions это step с trufflehog git file://. --since-commit=$BASE_SHA --fail - сканируются только новые коммиты PR, не вся история. Для PR из форков $BASE_SHA может отсутствовать в локальном клоне; добавьте git fetch origin $BASE_REF перед сканированием. Если секрет найден - PR блокируется.

Важно: синтаксис флагов может отличаться между версиями TruffleHog; проверьте trufflehog git --help для вашей версии. И ещё - actions/checkout@v4 нужно вызывать с fetch-depth: 0, иначе при shallow clone $BASE_SHA недоступен и --since-commit не сработает. Более надёжный вариант - официальный action trufflesecurity/trufflehog@main, который сам определяет base/head коммиты.

Для GitLab CI аналогично: stage test с вызовом gitleaks detect --log-opts="$CI_MERGE_REQUEST_DIFF_BASE_SHA..$CI_COMMIT_SHA" (step запускается только в merge request pipeline через rules: - if: $CI_MERGE_REQUEST_IID).

Слой 3: Continuous scanning - полная git-история и артефакты​

Периодическое сканирование всей истории репозитория командой trufflehog git file://./repo по расписанию (cron или scheduled pipeline). Этот слой ловит секреты, закоммиченные до внедрения pre-commit и CI-сканирования. По данным GitGuardian, значительная доля исторических секретов остаётся валидной годами. Continuous scanning находит именно такие «забытые» ключи - и они-то самые опасные.

HashiCorp Vault CI/CD интеграция: ротация токенов и динамические секреты

Сканеры - это safety net. Они ловят последствия, а не причину. Причина - существование статических секретов. Vault решает проблему на архитектурном уровне: пайплайн не хранит секретов, а запрашивает их на лету, получает на минуты, после чего они автоматически отзываются.

JWT/OIDC: аутентификация пайплайна без статических секретов​

Рекурсивная проблема: чтобы получить секрет из Vault, пайплайн должен аутентифицироваться - а значит, нужен ещё один секрет? JWT/OIDC-федерация разрывает этот цикл.

GitHub Actions при каждом запуске workflow выпускает короткоживущий JWT с claims: репозиторий, ветка, workflow, автор. Vault проверяет этот токен через JWKS-эндпоинт GitHub - никаких статических ключей. Аналогично работает GitLab CI: начиная с версии 15.7 (GA в 15.9) используется блок id_tokens: в .gitlab-ci.yml с явным audience, а переменная с токеном задаётся произвольным именем (например, $VAULT_ID_TOKEN). Устаревшая $CI_JOB_JWT_V2 удалена в GitLab 17.0 - если у вас она ещё используется, пора мигрировать.

Настройка: vault auth enable jwt, затем создание роли, привязанной к конкретному репозиторию и ветке. В workflow пайплайн вызывает vault write auth/jwt/login role=ci-deploy jwt=$ACTIONS_ID_TOKEN и получает Vault-токен с TTL 15 минут. Этот подход полностью исключает необходимость хранить VAULT_TOKEN как GitHub Secret.

AppRole - более старый подход, где Role ID (аналог username) хранится открыто, а Secret ID (аналог одноразового пароля) генерируется с TTL и лимитом в одно использование. Рабочий вариант для Jenkins и других платформ без нативной поддержки OIDC, но JWT/OIDC предпочтительнее - меньше секретов в обороте.

Dynamic secrets: одноразовые учётные данные с TTL​

Vault умеет генерировать учётные данные для PostgreSQL, MySQL, AWS IAM, GCP и других сервисов по запросу. Каждый запуск пайплайна получает уникальную пару username/password с TTL - скажем, 1 час. После TTL Vault автоматически отзывает credentials.

Пример Vault-политики с минимальными привилегиями для CI:
Код:
path "secret/data/myapp/*" {
  capabilities = ["read"]
}
path "database/creds/myapp-ci" {
  capabilities = ["read"]
}
Команда vault read database/creds/myapp-ci возвращает уникальный username и password, валидные в течение заданного TTL. Пайплайн завершился - учётные данные самоуничтожились. Если раннер скомпрометирован - окно эксплуатации ограничено минутами, а не месяцами.

Для AWS: Vault через secrets engine aws генерирует IAM-credentials с политикой, ограниченной конкретным S3-бакетом или ECR-реестром. Пайплайну не нужен AWS_SECRET_ACCESS_KEY с полными правами - Vault создаёт именно те permissions, которые нужны для конкретного шага. Ни больше, ни меньше.

Ограничения Vault в CI/CD​

Vault - не серебряная пуля. Он добавляет точку отказа: если Vault-кластер лёг, ни один пайплайн не соберётся. Нужен HA-кластер (минимум 3 ноды), мониторинг seal-статуса, auto-unseal через облачный KMS. Для команд до 50 человек с десятком сервисов - native platform secrets (GitHub Secrets, GitLab CI Variables) с OIDC-федерацией к облачному провайдеру могут быть достаточны. Vault оправдан при десятках пайплайнов, мультиплатформенной инфраструктуре и требованиях compliance (SOC 2, PCI DSS).

Ротация токенов CI/CD: чеклист для DevSecOps-команды​

Готовый чеклист - можно передать коллегам или включить в отчёт по аудиту безопасности пайплайнов:
📚 Часть контента скрыта. Этот материал доступен участникам сообщества с рангом One Level или выше
Получить доступ просто — достаточно зарегистрироваться и проявить активность на форуме

Инцидент 2023 года с CircleCI - показательный. При взломе были скомпрометированы все секреты, хранившиеся внутри платформы. Компании с OIDC-федерацией к внешним секрет-менеджерам пережили инцидент без ротации - их секреты в CircleCI не лежали. Компании с native platform secrets ротировали всё.

По моему опыту, самая частая ошибка - не отсутствие сканера, а убеждённость, что .env в .gitignore достаточно. .gitignore предотвращает добавление файла в индекс, но не защищает от git add -f .env, не работает при случайном переименовании файла и уж точно не поможет, если секрет попал в переменную окружения пайплайна и вылез в лог через set -x в bash-скрипте.

Позиция, которую разделяют не все: сканер секретов - не мера безопасности, а индикатор зрелости. Если Gitleaks находит в репозитории больше нуля активных секретов - архитектура управления секретами сломана. Сканер должен показывать ноль. Всегда. Каждый найденный секрет - инцидент, требующий ротации, а не «запись в backlog».

Команды, которые добавляют исключения в конфиг Gitleaks на каждый false positive вместо того, чтобы разобраться с root cause, по сути отключают защиту. Через год у них 200 исключений и ни одного реального срабатывания.

Настоящая цель - не «внедрить secret scanning», а выйти на zero static secrets в пайплайне. Vault с OIDC и dynamic secrets делает это возможным для большинства стеков. Пока есть хотя бы один AWS_SECRET_ACCESS_KEY в GitHub Secrets, который не менялся полгода - считайте, что этот ключ уже скомпрометирован. Вопрос только в том, знаете ли вы об этом.
 
Мы в соцсетях:

Взломай свой первый сервер и прокачай скилл — Начни игру на HackerLab

Похожие темы

🚀 Первый раз на Codeby?
Гайд для новичков: что делать в первые 15 минут, ключевые разделы, правила
Начать здесь →
🧭 Навигатор · ИБ 2026
Не знаешь, какой трек твой?
5 направлений ИБ, реальные зарплаты и точка входа для каждого — в одном треде.
JuniorSenior+
100K → 600K+ ₽ /мес
Открыть навигатор →
🔴 Свежие CVE, 0-day и инциденты
То, о чём ChatGPT ещё не знает — обсуждаем в реальном времени
Threat Intel →
💼 Вакансии и заказы в ИБ
Pentest, SOC, DevSecOps, bug bounty — работа и проекты от проверенных компаний
Карьера в ИБ →

HackerLab