По данным 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-паттерны для известных форматов (AWSAKIA…, 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 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 таблица
| Критерий | TruffleHog | Gitleaks |
|---|---|---|
| Метод детекции | Regex + entropy + API-верификация | Regex + entropy (опционально) |
| False positive rate | Низкий (за счёт верификации) | Средний (зависит от конфигурации правил) |
| Скорость | Медленнее (сетевые вызовы верификации) | Быстрее (чистый pattern matching) |
| Offline-режим | Частично (без верификации) | Полностью |
| Кастомные правила | Через detectors на Go | TOML-конфиг, быстрая настройка |
| CI-интеграция | GitHub Actions, GitLab CI, pre-commit | GitHub 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, который не менялся полгода - считайте, что этот ключ уже скомпрометирован. Вопрос только в том, знаете ли вы об этом.