Конвейер сборки без утечек критичных данных или защита от Уильяма Тестера в цифровую эпоху

Конвейер сборки без утечек критичных данных или защита от Уильяма Тестера в цифровую эпоху

Изображение: recraft

«Трое могут хранить тайну, если двое из них мертвы.»
Бенджамин Франклин, Альманах Бедного Ричарда, 1735 год

1885 год

Шёл по-майски тёплый лондонский дождь. В почтовый вагон поезда, уходящего в Париж, только что погрузили несколько железнодорожных сейфов, набитых золотом. Тяжёлые металлические шкафы с толщиной стенок в дюйм выдерживали любой удар и отпирались двумя ключами. Взломать сейфы было невозможно, а ключи хранились в разных местах: первый — у охранника, второй — у начальника станции. Но по прибытии поезда золото превратилось в свинец.

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

2025 год

Информация — золото XXI века: внимание к безопасности в сфере информационных технологий с каждым годом усиливается. Но грабители, как и 140 лет назад, могут рассчитывать на беспечное отношение к хранению «ключей»: люди продолжают держать их у всех на виду.

В современном DevOps/CI/CD под секретами обычно понимают любые конфиденциальные данные, которые дают доступ к системам и ресурсам: пароли, API-ключи, токены (GitHub/GitLab, NPM, Docker- Registry), ключи облаков (AWS/GCP), приватные SSH-ключи, сертификаты, строки подключения к БД, webhook-секреты и т. п. Они нужны конвейеру сборки, чтобы забирать зависимости, публиковать артефакты, деплоить и подключаться к инфраструктуре. Но ровно по этой причине секреты — одна из самых «дорогих» целей для атакующего: утечка часто означает прямой доступ к данным, деньгам, инфраструктуре и цепочке поставки (supply chain).

По данным отчета «GitGuardian State of Secrets Sprawl 2025» компании GitGuardian (ведущий поставщик решений для обнаружения секретов), масштаб проблемы впечатляет:

  • В 2024 году было обнаружено 23,770,171 новых захардкодированных секретов в публичных коммитах GitHub — рост на 25% по сравнению с 2023 годом.
  • 15% авторов коммитов хотя бы один раз допустили утечку секрета
  • 4.6% всех публичных репозиториев содержат секреты
  • 35% приватных репозиториев содержат захардкодированные секреты (что в 8 раз выше, чем у публичных!)
  • анализ 15 миллионов публичных Docker-образов выявил 100,000 действующих секретов, включая AWS-ключи, GCP-ключи и GitHub-токены, принадлежащие компаниям Fortune 500
  • Самая тревожная находка: 70% секретов, обнаруженных в 2022 году, остаются активными по сей день. Это означает, что разработчики не отзывают скомпрометированные ключи даже спустя годы.

Нельзя так просто взять и удалить файл из GIT

Проблема хранения чувствительных данных в репозитории — вопрос культуры работы с кодом. Но даже при соблюдении правил участниками команды секрет может попасть в репозиторий «случайно»: неверная команда, забытый .env, токен, вставленный «на минутку» для отладки. И удалить такой секрет из истории — задача не из простых, если используется система контроля версий Git.

Git опирается на криптографически защищённые хэши коммитов, поэтому любое изменение прошлого автоматически ломает всю последующую историю. С точки зрения целостности это плюс (сложнее незаметно подменить код), но для конфиденциальности есть побочный эффект: чтобы убрать случайно добавленный .env или токен интеграции, прописанный прямо в коде, приходится переписывать историю репозитория, синхронизироваться с командой и удалять/пересоздавать клоны, содержащие секретные данные. На практике обычно проще и надёжнее произвести ротацию скомпрометированных данных (отозвать токен, перевыпустить ключ, сменить пароль), чем надеяться, что «где‑то» не осталось копии истории.

Памятка «Как мне удалить секрет из Git» (на всякий случай):

  • Воспользуйтесь утилитой git-filter-repo для перезаписи истории локально,
  • Обновите репозиторий в своей центральной системе контроля версий, используя локально переписанную историю.
  • Скоординируйте свои действия с коллегами, чтобы удалить другие существующие клоны.
  • Предотвратите повторения и избегайте будущих утечек конфиденциальных данных

Культура работы с кодом: не доверяйте никому, даже себе

Последний пункт памятки предлагает избегать утечек конфиденциальных данных, но как? Важно заранее принять простую мысль: полагаться на то, что разработчики будут внимательны и самостоятельно не закоммитят секреты (и среди них не будет «Уильяма Тестера») — наивно. В условиях приближающегося дедлайна и опытные инженеры допускают ошибки: кто-то торопится, кто-то копирует пример из чата, кто-то «временно» вставляет токен, чтобы починить сборку. Вместо доверия нужна автоматизация и проверка. Вот несколько советов, которые помогут в культивации подобной культуры.

.gitignore — файл, в котором перечислены файлы или директории, которые не должны отслеживаться Git. Запишите в нём названия файлов и отправьте файл .gitignore в центральный репозиторий, чтобы другие разработчики его получили. Это снижает вероятность случайных коммитов «локальных» конфигураций, но важно помнить: .gitignore не удаляет уже добавленные файлы из истории — он предотвращает только будущие попадания.

Пример файла .gitignore — в вашем случае файлы с секретами могут быть другими:

.env
.env.local
*.key
.npmrc

Избегайте жёсткого кодирования (хардкода) секретов в исходном коде. Реквизиты подключения, токены и другие секретные данные должны передаваться приложению из переменных среды или внешней службы управления секретами во время запуска приложения. Это позволяет отделить код (логика) от конфигурации (секреты) и не переносить чувствительные данные по цепочке «IDE → git → CI → артефакты».

Неправильно:

typescript
const dbPassword = "prod_pass_xyz123";
const apiToken = "sk_live_abc";

Правильно:

typescript
const dbPassword = process.env.DB_PASSWORD;
const apiToken = process.env.API_TOKEN;

Избегайте добавления файлов в Git командой git add . в командной строке — вместо этого указывайте имя добавляемого файла явно командой git add filename. Эта привычка кажется мелочью, но именно она часто «перехватывает» случайно появившиеся .env, дампы, ключи и временные конфиги.

Создайте pre-commit hook для проверки конфиденциальных данных перед их фиксацией или отправкой. Хук — это техническая «страховка»: он не заменяет дисциплину, но превращает типовые ошибки в раннее предупреждение.

Настройте в центральном Git репозитории — платформе работы с кодом, которую вы используете как единую точку синхронизации разработки, правила Push операций, которые не пропустят контент, подпадающий под определенные правила. Такой контроль особенно полезен, когда команда большая, а уровень практик у участников разный: серверные проверки не обойти «случайно».

Добавьте правила защиты веток и этап проверки кодовой базы на наличие секретов в CI/CD-конвейер, чтобы обеспечить чистоту основных веток проекта. Важно, чтобы проверка была не разовой кампанией, а регулярной процедурой: секреты — «дрейфующая» проблема, она возвращается.

Когда культура есть, но секреты все равно утекли

Выпуск релиза программного продукта — процесс многоэтапный. И даже если вопрос с культурой работы с кодом решён, и кодовая база защищена от попадания в неё лишней информации, то на этапе подготовки к релизу могут возникнуть проблемы компрометации чувствительных данных. Причина проста: релиз — это не только git, это ещё и сборка, зависимости, контейнеризация, публикация артефактов и логирование. Секрет может «не жить» в репозитории, но проявиться в сборочном контексте — и этого достаточно для утечки.

В отчете GitGuardian есть упоминание найденных секретов в слоях Docker образов. Специалисты изучали Docker-образы, хранившиеся публично в Docker Hub (облачный сервис и централизованное хранилище Docker-образов), и обнаружили, что:

  • ENV, RUN & ARG‑инструкции отвечали за 78% утечек секретов
  • ENV‑инструкции составляли 65% всех утечек

Из 1,179,475 уникальных обнаруженных секретов 101,186 были автоматически проверены как действующие. Найденные секреты обеспечивают доступ к:

  • Базам данных
  • Инфраструктуре AWS
  • Экземплярам GitHub Enterprise
  • Репозиториям Artifactory

Расскажем историю из реальной практики GitGuardian. Слои Docker-образов представляют уникальные вызовы для управления секретами. Анализ образа для production выглядел изначально безопасно, но рассмотрение истории насторожило исследователей:

bash

$ docker history organization/application-production
# ...
<missing> 6 months ago /bin/bash -o pipefail -c rm -f
.npmrc || : 0B

Фрагмент вывода истории указывает на попытку удалить файл учётных данных после операций сборки. Однако извлечение соответствующего слоя показало следующее:

text

//npm.pkg.github.com/:_authToken=ghp_6e******
registry = https://npm.pkg.github.com/
always-auth=true

Фрагмент демонстрирует критический дефект безопасности: даже когда файлы, содержащие секреты, удаляются на более поздних этапах сборки, они остаются в слое Docker-образа, в котором были созданы. То есть удаление «сверху» не стирает прошлое — оно лишь добавляет новый слой, в котором файла нет, но старый слой всё ещё доступен тому, кто умеет смотреть историю/слои.

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

Безопасный конвейер сборки приложений

В основе архитектуры конвейера нужна надёжная платформа, обладающая функциональностью, достаточной для реализации принципа — «всё в одном». Идея не в «монолите ради монолита», а в том, чтобы сократить количество мест, где секреты могут появиться, и повысить управляемость: меньше интеграционных «стыков» — меньше случайных утечек.

Исходный код не покидает контур платформы: разработчик отправляет изменения в центральный репозиторий, внутри этой же платформы запускается CI/CD конвейер, прогоняются тесты, приложение собирается и результат сборки отправляется в хранилище, которое тоже является частью платформы. Так вы минимизируете «копии» и «ручные переносы», а значит — снижаете риск.

Структура проектов и групп пользователей должна быть организована по принципу разделения зон ответственности: проект для кода, проект конфигураций CI/CD, отдельные реестры для хранения артефактов сборки и зависимостей приложения. Это помогает применять разные политики доступа: например, не всем, кто читает код, нужен доступ к секретам деплоя или к реестру артефактов.

Дизайн CI/CD конвейера у каждого проекта может быть свой, но при его проектировании нужно учитывать общие принципы:

  • добавление проверки безопасности на каждой стадии конвейера
  • хранить секреты в CI системе или внешнем инструменте работы с секретами
  • маскировать CI переменные в логах выполнения задач конвейера
  • не допускать попадания секретов в переменные окружения результирующего образа
  • передача секретов, которые требуются для работы приложения, должна происходить только при его запуске
  • использование runner’ов без долговременного диска.

Добавление проверки безопасности на каждой стадии конвейера

На любом этапе — от тестов до сборки образа и выката в прод — должна работать автоматическая проверка безопасности. Это могут быть анализ кода, поиск секретов, сканирование зависимостей и образов. Чем раньше обнаружим проблему, тем дешевле и безопаснее её исправить: секрет, найденный до публикации образа, — это инцидент, которого не произошло.

Хранить секреты в CI системе или внешнем инструменте работы с секретами

Ключи и токены не должны жить ни в репозитории, ни в Dockerfile, ни в скриптах. Их стоит заводить как защищённые переменные CI или хранить во внешнем секрет-менеджере и подключать в задачу, выполняемую конвейером, только на время выполнения. Это позволит централизованно управлять доступом и ротацией секретов, не меняя код приложения, и одновременно уменьшит «площадь» утечки: секрет меньше путешествует и меньше копируется.

Маскировать CI переменные в логах выполнения задач конвейера

Даже корректно хранимый секрет можно случайно отобразить в stdout выполняемой задачи: отладочный echo, ошибка клиента, verbose‑режим утилиты, вывод переменных окружения. Поэтому все чувствительные переменные в CI нужно помечать как «masked», чтобы их значения автоматически заменялись звездочками в логах. Это защищает от утечки через интерфейс платформы и от «случайных скриншотов» логов.

Не допускать попадания секретов в переменные окружения или файлы итогового Docker образа

Переменные окружения образа видны через docker inspect и доступны для просмотра специальными инструментами. Если в переменной окружения разместить токен или пароль, он будет встроен в слои образа навсегда. Секреты не должны оказываться в ARG и ENV одноэтапного Dockerfile, в котором происходит сборка и запуск приложения. Для безопасной сборки через Dockerfile создайте многоэтапную сборку и/или используйте механизмы временного монтирования секретов при сборке.

На примере сборки Nodejs приложения посмотрим, как решить проблему передачи секрета авторизации в реестре nodejs пакетов

Dockerfile

FROM node:20 AS builder
WORKDIR /app
# Копируем package.json
COPY package.json package-lock.json ./
# Передаём токен
ARG NPM_TOKEN
# Размещаем токен в файле npmrc
RUN echo "//npm.pkg.your-private-repo.ru /:_authToken=${NPM_TOKEN}" > ~/.npmrc
# Устанавливаем пакеты
RUN npm install
# Удаляем файл .npmrc
RUN rm ~/.npmrc
COPY src/* src/
RUN npm run build
CMD ["node", "dist/index.js"]

В примере две проблемы: передача токена через аргумент (build-arg), и сохранение файла .npmrc в слоях образа, даже несмотря на то, что файл был удален после установки пакетов. Это типичная ловушка контейнерной сборки: «удалить позже» не значит «не оставить следов».

Решить проблему можно несколькими способами. Выбор зависит от того, как устроен ваш pipeline, какой у вас сборщик (Docker/BuildKit), и можно ли разделять сборку на этапы.

Разные задачи в конвейере CI/CD

Разделение процессов сборки приложения и docker образа на разные этапы гарантирует, что в docker образе с приложением не будет токена авторизации в частном репозитории npm пакетов. Идея проста: всё, что требует секретов для скачивания зависимостей, выполняется до сборки образа, а в образ попадает только результат (артефакты сборки).

В примере ниже сценарий сборки Nodejs приложения в отдельной задаче build_project конвейера CI/CD.

.build_project:
stage: build
image: node:20
script:
script:
# Создаём файл .npmrc из маскированной переменной NPM_TOKEN
- echo "//npm.pkg.your-private-repo.ru/:_authToken=${NPM_TOKEN}" > ~/.npmrc
- npm install
- npm run build
# Сохраняем артефакты сборки для передачи в следующий этап
artifacts:
paths:
- dist/
expire_in: 1 hour

После того как проект собран, мы передаем в следующий этап артефакты сборки и выполняем сборку Docker образа.

.build_docker:
stage: build_docker
image:
name: moby/buildkit:rootless
entrypoint: [""]
script:
- echo "Сборка Docker образа"
- |
buildctl-daemonless.sh build
--output type=image,name=$DOCKER_IMAGE_NAME,push=true
--local context=.
--local dockerfile=.
--frontend dockerfile.v0

И Dockerfile, предназначенный для запуска собранного приложения, будет иметь вид:


FROM node:20
WORKDIR /app
# Копируем собранное этапом ранее приложение
COPY dist/ ./dist
CMD ["node", "app/dist/index.js"]

Важно: примеры схематичные и в сценарии в реальности могут сильно отличаться, но смысл должен быть понятен.

Docker Secrets и выполнение команд в одном слое

Для безопасной передачи чувствительных данных (паролей, ключей API, SSL-сертификатов) при сборке существует функция Secrets в системе сборки Docker образов BuildKit, позволяющая монтировать их как временные файлы в контейнер сборки без сохранения в слоях образа.

И для надёжности можно добавить удаление файла с токеном авторизации в том же слое, в котором он создаётся. Это снижает риск, что промежуточное состояние окажется сохранённым в отдельном слое.

Dockerfile

FROM node:20
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=secret,id=npm_token 
    NPM_TOKEN=$(cat /run/secrets/npm_token) && 
    echo "//npm.pkg.github.com/:_authToken=$NPM_TOKEN" > ~/.npmrc && 
    npm install && 
    rm ~/.npmrc
COPY src/ src/
RUN npm run build
CMD ["node", "dist/index.js"]

Многоэтапный Dockerfile

Если в CI/CD нужно выполнить сборку приложения и Docker образа в одной задаче, безопасно использовать многоэтапный Dockerfile, в котором сборка приложения будет отделена от итогового образа, в котором будет произведен запуск приложения. В примере ниже — гарантированный способ скрыть секретные данные, используя секреты сборки, слияние в один слой и отдельный образ для итогового приложения.

Dockerfile

FROM node:20 AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=secret,id=npm_token
NPM_TOKEN=$(cat /run/secrets/npm_token) &&
echo "//npm.pkg.github.com/:_authToken=$NPM_TOKEN" > ~/.npmrc &&
npm install &&
rm ~/.npmrc
COPY src/ src/
RUN npm run build
FROM node:20-slim
WORKDIR /app
COPY --from=builder /app/dist ./dist
CMD ["node", "dist/index.js"]

Автоматически генерируемые токены авторизации

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

- echo "${CI_REGISTRY}/project/{ownerAlias}/{projectAlias}/package/-/npm/:_authToken=${CI_JOB_TOKEN}"> ~/.npmrc

Передача секретов, которые требуются для работы приложения, должна происходить только при его запуске

Рабочие секреты (доступ к БД, API‑ключи, токены интеграций) должны поступать в приложение через переменные окружения рантайма, секрет‑менеджер или механизм мало живущих токенов, обновляемых в фоновом режиме. При этом подходе образ остаётся одинаковый для всех окружений, а различаются только значения секретов, подставляемые в момент запуска. Это дисциплинирует поставку: один и тот же артефакт проходит тестирование и прод, меняются только секреты и параметры окружения.

Использование runner’ов без долговременного диска

Исполняющая среда CI не должна накапливать чувствительные данные между запусками. Эфемерные runner’ы, которые поднимаются на время пайплайна и затем полностью уничтожаются, снижают риск того, что секреты или временные файлы останутся на диске и будут доступны другим заданиям или пользователям. Это особенно важно при сборке образов и работе с приватными ключами: «чистая» среда каждый запуск — это практическая реализация принципа минимального остатка данных.

Заключение

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

Главное, что нужно запомнить:

  • Секреты не должны жить в коде, только в памяти и на время выполнения
  • Каждый слой Docker-образа — это потенциальная утечка
  • Культура — это первый рубеж защиты, но автоматизация — это второй, который важнее.

Уильям Тестер знал: лучший момент для кражи — это когда никто не думает, что это возможно. В современном CI/CD это звучит так же: утечка происходит не тогда, когда «всё сломалось», а тогда, когда всем кажется, что «и так сойдёт».

Статью подготовил Роман Байталов, архитектор системных решений GitFlic (входит в экосистему «Группы Астра»).

Группа Астра
Автор: Группа Астра
ГК «Астра» (ООО «РусБИТех-Астра») — один из лидеров российской IT-индустрии, ведущий производитель программного обеспечения, в том числе защищенных операционных систем и платформ виртуализации. Разработка флагманского продукта, ОС семейства Astra Linux, ведется с 2008 года. На сегодня в штате компании более 1000 высококвалифицированных разработчиков и специалистов технической поддержки.
Комментарии: