Как забытые токены в истории коммитов, невидимые слои контейнеров и избыточные роли подрывают защиту системы ещё до первого запуска. Чтобы скомпрометировать приложение, не всегда требуется сложная ошибка в коде. Иногда достаточно оставленного в конфигурации токена, устаревшей библиотеки внутри контейнера или облачной роли с избыточными правами. Сборка проходит успешно, тесты проходят, инфраструктура применяет опасные настройки. Автоматизация работает так, как её настраивали. Поиск секретов выявляет пароли и ключи в файлах. Сканирование образов проверяет компоненты выпускаемого контейнера. Анализ инфраструктуры как кода (IaC) помогает заметить опасные параметры до создания ресурсов. Эти проверки дополняют друг друга, а смысл появляется, когда результат меняет поведение конвейера. ### Пароль удалили, но история Git сохранила его Секрет редко появляется в репозитории с комментарием «боевой ключ, не публиковать». Чаще рабочее значение подставляют в пример конфигурации, сохраняют в .env для локального запуска или оставляют в тестовом сценарии. К той же категории относятся приватные SSH-ключи, строки подключения к базам, файлы авторизации пакетных менеджеров и конфигурации облачных клиентов. Даже закрытый репозиторий требует проверки, так как секрет доступен всем, кто получил доступ к истории коммитов. Удаление файла новым коммитом не стирает его из предыдущих версий. Добавление пути в .gitignore не прекращает отслеживать файл, который уже попал под контроль Git. Поэтому при утечке ключа сначала нужно отозвать старое значение и выдать новое, затем проверить журналы его использования и места, куда могли разойтись копии. Переписывание истории решает отдельную задачу и не отзывает секрет из чужого клона. Первая проверка должна срабатывать до коммита. Для этого подходят Gitleaks и другие сканеры секретов, которые можно подключить к локальному Git-хуку. Проверять следует подготовленные к коммиту изменения в индексе, а не только файлы в рабочем каталоге. При частичном добавлении файла содержимое этих двух мест может различаться. Для среды с Bash и актуальным Gitleaks хук может выглядеть так. Скрипт сохраняют в .githooks/pre-commit. Режим —staged проверяет изменения из индекса, а параметр —redact скрывает найденные значения в выводе. «`bash #!/usr/bin/env bash set -euo pipefail exec gitleaks git —pre-commit —staged —redact . «` Хук включают для локального репозитория следующими командами. Если проект уже использует собственные хуки, проверку добавляют в существующий механизм, сохраняя остальные действия. «`bash 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 и ненужные сборке служебные файлы. Популярная попытка исправить промах выглядит убедительно, пока не вспомнишь про слои образа. «`dockerfile COPY .env /app/.env RUN ./build.sh RUN rm /app/.env «` В финальном представлении файловой системы файла уже нет. Но содержимое .env осталось в предыдущем слое, доступном получателю образа. Поэтому секрет нельзя сначала записывать в слой, рассчитывая затем убрать отдельной командой. Многоэтапная сборка тоже не даёт права обращаться с секретами небрежно, поскольку промежуточные результаты и кэш могут сохраняться отдельно. Для пароля, который нужен именно во время сборки, BuildKit поддерживает secret mounts. Секрет временно доступен указанной инструкции RUN и сам по себе не включается в слой. Например, сборочный этап с npm может получить файл настроек для доступа к приватному реестру пакетов. «`dockerfile 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 исправно создаёт неправильные права Инфраструктура как код делает настройки повторяемыми. Ошибка в общем модуле тоже становится повторяемой. Широкая облачная роль, публичный доступ к хранилищу или открытый административный порт могут разойтись по нескольким окружениям одним изменением шаблона. Успешный 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 внутри описания контейнера может выглядеть так. «`yaml 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-режима нужна правка либо согласованное исключение с владельцем и сроком. Уязвимость пакета оценивают с учётом серьёзности, условий эксплуатации и доступного исправления. Сбой сканера отделяют от результата «ничего не найдено». Работу процесса проверяют несколькими безопасными контрольными изменениями на тестовом проекте. Учебная строка должна вызвать нужное правило поиска секретов, запрещённый параметр должен остановить проверку манифеста, а нежелательное сетевое соединение должно завершиться отказом на стенде. Затем проверяют права на изменение самих правил и исключений. Если любой автор запроса на слияние может незаметно отключить обязательную проверку, конвейер защищает только от случайности.
Популярное:
- Идеальный код с трояном внутри: как доверенный конвейер CI/CD превращается в оружие против клиентов
- В Аромашевском округе Тюменской области жители узнали о семейных фермах
- Слепые зоны SAST, DAST и SCA: как формальные проверки создают опасную иллюзию защищенности сервиса
- Определен самый некурящий регион России
- Дмитрий Чернышенко: Россия — союз почти 200 национальностей и народов
- Reuters: крупнейшая авиакомпания Латвии AirBaltic начала процедуру банкротства
- Секреты, контейнеры и IaC. Где CI/CD оставляет лишний доступ
- В Омске оштрафовали водителя BMW за попытку подкупа полицейского
