Безопасность и VPN

Пароль удалили из Git, но он всё ещё там. Главные ошибки безопасности CI/CD

15 сентября 2026 г.Павел Игнатьев11 мин

Для компрометации приложения часто не нужны сложные уязвимости в коде. Достаточно токена в конфигурации, устаревшей библиотеки в контейнере или чрезмерно широкой облачной роли. Сборка проходит, тесты успешны, а инфраструктура разворачивает опасные настройки. Автоматизация выполняет ровно то, что ей предписано.

Сканеры секретов находят пароли и ключи в файлах. Сканирование образов проверяет компоненты контейнера. Анализ инфраструктуры как кода (IaC) выявляет опасные параметры до создания ресурсов. Эти проверки дополняют друг друга, но их ценность проявляется, когда результаты меняют логику конвейера.

Пароль удалили, история Git осталась

Секреты редко попадают в репозиторий с прямым указанием на их важность. Обычно разработчики вставляют рабочие значения в примеры конфигурации, сохраняют в .env для локального использования или оставляют в тестовых скриптах. К таким секретам относятся приватные SSH-ключи, строки подключения к базам данных, файлы авторизации пакетных менеджеров и облачных клиентов. Даже закрытые репозитории нуждаются в проверке, ведь секрет становится доступен всем, кто имеет право читать историю изменений.

Удаление файла новым коммитом не убирает его предыдущие версии из истории. Добавление пути в .gitignore не прекращает отслеживать файл, если он уже под контролем Git. Если ключ утёк, сначала отозовите старое значение и выдайте новое. Затем проверьте журналы его использования и места, куда могли попасть копии. Переписывание истории лишь решает проблему внутри репозитория, не отзывая секрет из чужих клонов.

Первая проверка должна срабатывать ещё до коммита. Для этого подойдут Gitleaks и подобные сканеры секретов, которые можно интегрировать как локальный Git-хук. Проверяйте изменения, подготовленные к коммиту в индексе, а не только файлы в рабочем каталоге. При частичном добавлении файла эти области могут содержать разное содержимое.

#!/usr/bin/env bash
set -euo pipefail

exec gitleaks git --pre-commit --staged --redact .

Включите хук для локального репозитория этими командами. Если проект уже использует свои хуки, интегрируйте эту проверку в существующий механизм, сохраняя остальные действия.

chmod +x .githooks/pre-commit
git config --local core.hooksPath .githooks

Дополните проверку перед коммитом отдельным сканированием всей истории и правилами для внутренних форматов токенов. Локальный хук легко отключить или забыть установить, поэтому CI должен дублировать проверку входящих коммитов. Серверная защита при отправке изменений, если платформа её поддерживает, создаст дополнительный барьер. Проверка после push не предотвратит попадание секрета на сервер, но поможет остановить дальнейшее распространение.

Ложные срабатывания разбирают индивидуально. Разрешите исключить тестовую строку с чётким обоснованием, но опасно исключать весь каталог tests или примеры конфигурации. Рабочие значения часто копируют именно туда. Отчёты также не должны создавать новую утечку; поэтому маскируйте найденные секреты и ограничивайте доступ к детальным результатам.

Переменная окружения не превращает токен в защищённый секрет

Перемещение пароля из исходного кода в переменную окружения убирает его одну копию из Git. Далее важно понять, кто передаёт значение процессу, какие задачи его получают и где оно может сохраниться. Отладочная печать окружения, подробные логи команд или диагностические дампы могут снова раскрыть пароль. Маскирование в CI помогает, но не гарантирует сокрытие любых преобразованных строк.

Разделение доступа — отправная точка. Задача, запускающая тесты, обычно не требует пароля от производственной базы. Задача публикации образа нуждается в доступе к конкретному репозиторию образов. Процессу приложения нужны собственные учётные данные с минимально необходимыми операциями. Единый токен для тестов, сборки и развёртывания превращает каждую из этих задач в потенциальную точку компрометации всей цепочки.

Получайте реальные значения из менеджера секретов или защищённых механизмов платформы, выдавая их только нужным задачам. Если облако и CI поддерживают федерацию через OIDC, конвейер может обменять подтверждённую идентичность запуска на временные учётные данные. Ограничивайте доверие конкретным проектом, веткой или окружением, а выданной роли давайте только необходимые права. Короткий срок жизни сокращает окно для злоупотреблений, но не помешает злоумышленнику использовать токен до его истечения.

Kubernetes таит ещё одну ловушку: значения в поле data объекта Secret обычно кодированы в Base64. Это кодирование обратимо и не защищает секрет от того, кто прочитает YAML-файл. В Git можно хранить ссылки на внешние секреты или их зашифрованные представления, управляя ключами отдельно. Доступ к объектам Secret и шифрование данных кластера при хранении настраиваются независимо.

Секрет может пережить удаление из контейнера

Dockerfile может упаковать в образ то, что команда удалила из репозитория. Например, локальный .env остаётся на диске, команда COPY . . переносит весь подходящий контекст сборки, а правила .gitignore не работают для Docker. Для исключения файлов из контекста создайте .dockerignore. В него добавьте локальные секреты, каталог .git и другие служебные файлы, не нужные для сборки.

Распространённая попытка исправить ошибку выглядит логично, пока не вспомнить о слоях образа.

COPY .env /app/.env
RUN ./build.sh
RUN rm /app/.env

В финальной файловой системе файла уже нет. Но содержимое .env сохранилось в предыдущем слое, доступном получателю образа. Не записывайте секрет в слой с расчётом удалить его позже отдельной командой. Многоэтапная сборка также не даёт повода для небрежного обращения с секретами, так как промежуточные результаты и кэш могут сохраняться отдельно.

Для паролей, необходимых только во время сборки, BuildKit предлагает secret mounts. Секрет временно доступен указанной инструкции RUN и не попадает в слой образа. Например, этап сборки с npm может получить файл настроек для доступа к приватному реестру пакетов.

RUN --mount=type=secret,id=npmrc,required=true \
    npm ci --userconfig=/run/secrets/npmrc

Запускайте docker build, передавая соответствующий файл через --secret. Это сохраняет исходную копию вне репозитория и контекста сборки. Токену достаточно прав только на чтение нужных пакетов. Механизм монтирования не помешает самой команде вывести секрет в лог, скопировать его или передать вредоносному коду. Здесь важны проверка зависимостей до выполнения установочных скриптов и минимальные права сборочной задачи. ARG и ENV в Dockerfile не подходят для передачи секретов.

Минимальный образ должен ещё и обновляться

Проверка зависимостей приложения не всегда охватывает системные пакеты базового образа. Внутри могут скрываться устаревшие библиотеки, интерпретаторы и утилиты, отсутствующие в манифесте проекта. Поэтому анализируйте собранный образ, включая доступные сканеру пакеты ОС и прикладные зависимости. Учтите, что контейнер использует ядро хоста, и чистый отчёт по образу не подтверждает актуальность обновлений самого узла.

Минимизация образа начинается с вопроса: что нужно процессу для работы? Компиляторы, заголовочные файлы и инструменты сборки оставляйте на этапе сборки. В финальный этап копируйте только приложение, необходимые библиотеки и служебные данные, например, доверенные сертификаты. Distroless-образы сокращают набор системных утилит, но требуют совместимости приложения и особого подхода к диагностике. Сам по себе малый размер не гарантирует безопасность.

Выбирайте базовый образ из поддерживаемого и доверенного источника, фиксируя его по digest — идентификатору содержимого. Тег вроде latest может начать указывать на другой образ без изменений в Dockerfile. Digest обеспечивает воспроизводимость выбранной версии, но одновременно закрепляет её старые уязвимости. Поэтому дополните фиксацию регулярным обновлением ссылки, пересборкой, тестами и повторным сканированием.

Используйте Trivy или Grype для проверки образов. Эти инструменты имеют разные функции, поэтому поиск уязвимостей, секретов и проверку конфигурации нельзя считать одной автоматически включённой операцией. Проверяйте именно тот артефакт, который пойдёт в развёртывание. Если после сканирования образ собрали заново, прежний отчёт уже не подтверждает состав новой сборки.

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

Terraform исправно создаёт неправильные права

Инфраструктура как код (IaC) обеспечивает повторяемость настроек. Но ошибка в общем модуле также становится повторяемой. Широкая облачная роль, публичный доступ к хранилищу или открытый административный порт могут распространиться на несколько окружений одним изменением шаблона. Успешный terraform validate подтверждает синтаксическую корректность конфигурации, но не доказывает, что разрешённый доступ соответствует задаче.

Для прав доступа опишите конкретные действия приложения. Если сервис читает объекты из одного бакета и определённого префикса, ему не нужно полное управление объектным хранилищем, удаление чужих данных или изменение политик доступа. В AWS это выражают разрешением s3:GetObject для нужных объектов. Дополнительные операции, например перечисление объектов, добавляйте отдельно, если приложение действительно их выполняет. Синтаксис ресурсов и допустимые условия зависят от облака и сервиса.

Сеть требует того же контекста. Источник 0.0.0.0/0 в разрешающем IPv4-правиле охватывает все адреса. Для публичного HTTPS-сервиса такое правило может быть ожидаемым, но для административного доступа или базы данных оно требует иного решения. Фактическую публичную доступность определяют маршруты, внешние адреса, балансировщики и настройки самого сервиса. Одной строки в шаблоне недостаточно.

Проверяйте исходные .tf-файлы и план с вычисленными значениями. Шаблон может иметь безопасное значение по умолчанию, которое переопределили для конкретного окружения. Checkov анализирует Terraform-конфигурацию и JSON-представление плана. Учитывайте, что план может содержать секреты. Поэтому не публикуйте его целиком в открытом комментарии к запросу на слияние или в общедоступном артефакте CI.

Получайте план в контролируемом окружении. Terraform взаимодействует с провайдерами и может обращаться к внешним источникам данных. Запуск планирования по произвольному чужому изменению с производственными учётными данными создаёт риск ещё до apply. Задачи предварительного анализа и применения изменений должны иметь разные условия запуска и необходимые права.

Sensitive скрывает вывод, но не обязательно содержимое state

Terraform использует файл состояния (state), который связывает описанные ресурсы с реальной инфраструктурой. Туда могут попасть пароли и другие чувствительные значения вместе с атрибутами ресурсов. Пометка sensitive скрывает значение из обычного вывода, но не удаляет его из state и сохранённого плана. Некоторые машинные форматы вывода также раскрывают эти значения.

Храните файл состояния отдельно от Git, защищайте его при передаче и хранении, ограничивайте права чтения и ведите аудит доступа. Резервные копии, предыдущие версии и выгрузки требуют тех же ограничений. Простого переноса в удалённый backend недостаточно, если содержимое может прочитать вся команда или посторонний сервисный аккаунт.

Современные версии Terraform поддерживают ephemeral-значения и write-only-аргументы. В предусмотренных сценариях они помогают не сохранять секрет в состоянии. Поддержка зависит от версии Terraform, провайдера и конкретного ресурса. Поэтому передача значения из менеджера секретов не гарантирует отсутствия его копии в state. Проверьте, как значение проходит через используемые аргументы.

Kubernetes. Приложению не нужен доступ ко всему кластеру

В Kubernetes права процесса внутри контейнера и права сервисной учётной записи в API кластера — это разные уровни. Запуск без root не решает проблему избыточного ClusterRoleBinding. Узкая роль RBAC не компенсирует privileged-контейнер с доступом к чувствительным каталогам узла. Проверяйте оба уровня.

Для обычного Linux-приложения начните с запуска без root, запретите повышение привилегий, удалите ненужные Linux capabilities и используйте профиль seccomp. Корневую файловую систему по возможности делайте только для чтения. Фрагмент securityContext внутри описания контейнера выглядит так:

securityContext:
  runAsNonRoot: true
  runAsUser: 10001
  allowPrivilegeEscalation: false
  readOnlyRootFilesystem: true
  capabilities:
    drop:
      - ALL
  seccompProfile:
    type: RuntimeDefault

Согласуйте UID 10001 из примера с образом и правами на файлы. Для временных данных выделяйте отдельный записываемый том, а приложению назначайте подходящие пути. Если процессу действительно нужна дополнительная capability, добавьте конкретную. Снятие всех ограничений из-за одной ошибки доступа быстро превращает диагностику в постоянную конфигурацию.

В RBAC предпочтительны конкретные действия над нужными ресурсами и ограничение по namespace, когда задача это позволяет. Приложению, не обращающемуся к API Kubernetes, не стоит автоматически монтировать токен сервисной учётной записи. Для соответствующего Pod или ServiceAccount установите automountServiceAccountToken в false, предварительно проверив требования интеграций.

Отдельно рассмотрите учётную запись развёртывания. Право создавать Pod может дать косвенный доступ к секретам и привилегированным сервисным аккаунтам того же namespace. Поэтому «может только развёртывать приложение» не всегда означает узкий доступ. Закрепите ограничения на создаваемые Pod политиками допуска в кластер. Доступ к Docker socket, privileged-режим и чувствительные hostPath-монтирования требуют отдельного обоснования.

Ограничивайте сетевые связи по назначению. Приложению разрешайте необходимые обращения к базе данных, DNS и внешним сервисам, проверяя остальной трафик на соответствие выбранной политике. NetworkPolicy работает только при поддержке сетевого компонента кластера. Принятый Kubernetes YAML не подтверждает, что нежелательное соединение действительно блокируется.

Где ставить проверки в конвейере

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

Этап Что проверять Результат
До коммита Подготовленные изменения на секреты Хук предотвращает случайное добавление распознанного пароля или токена
Запрос на слияние Поступившие коммиты, Dockerfile, Terraform и Kubernetes-манифесты Команда исправляет проблему до объединения изменений
После сборки Конкретный образ на известные уязвимости и секреты Результат проверки привязан к digest выпускаемого артефакта
Перед развёртыванием Terraform plan и итоговые манифесты после Helm или Kustomize Политика проверяет фактические параметры целевого окружения
При допуске в кластер Создаваемые и изменяемые ресурсы Запрещённые настройки блокируются независимо от локальных проверок
После выпуска Работающие образы, права, сетевой доступ и расхождения с шаблонами Новые уязвимости и ручные изменения не проходят незамеченными

Для исходных IaC-файлов используйте Checkov и режим проверки конфигураций Trivy. Для Helm и Kustomize проверяйте результат подстановок для конкретного окружения. В Kubernetes закрепите базовые ограничения на Pod через Pod Security Admission, а дополнительные правила задавайте отдельными механизмами политик. Режим предупреждений поможет настроить процесс, но только включённый запрет остановит опасное действие.

Правила блокировки должны быть понятны разработчику. Подтверждённый рабочий секрет требует немедленной реакции. Публичный административный порт или необоснованный privileged-режим — правки или согласованного исключения с указанием владельца и срока. Оценивайте уязвимость пакета с учётом серьёзности, условий эксплуатации и доступного исправления. Отличайте сбой сканера от результата «ничего не найдено».

Проверьте работу процесса несколькими безопасными контрольными изменениями на тестовом проекте. Учебная строка должна активировать нужное правило поиска секретов, запрещённый параметр — остановить проверку манифеста, а нежелательное сетевое соединение — завершиться отказом на стенде. Затем проверьте права на изменение самих правил и исключений. Если любой автор запроса на слияние может незаметно отключить обязательную проверку, конвейер защищает только от случайностей.