Перейти к содержимому

Настройка staging окружения GitLab CI для пентеста: изоляция без риска для прода

Настройка staging окружения GitLab CI для пентеста: изоляция без риска для прода
Время чтения: 12 мин.

На проекте для финтех-компании внешние пентестеры получили доступ к staging-среде через GitLab CI — и через 40 минут вытащили из переменных пайплайна продовые креденшлы AWS. Один общий runner на оба окружения, CI/CD-переменные без привязки к environment, отсутствие protected environments. Три ошибки конфигурации — и контролируемый тест безопасности стал реальным инцидентом. Ниже — пошаговая настройка staging-окружения, при которой пентестер получает всё необходимое для работы, а продакшен остаётся за закрытой дверью.

Зачем staging нужна отдельная изоляция при тестировании безопасности

Staging-окружение (промежуточная среда, копирующая продакшен для тестирования перед релизом) по определению содержит код, близкий к продовому. Когда внешний пентестер получает доступ к staging-пайплайну без разграничения окружений DevOps, он автоматически видит:

  • CI/CD-переменные — хранилище секретов в GitLab: API-ключи, токены баз данных, credentials облачных провайдеров. Без привязки к окружению они доступны из любого джоба
  • Артефакты сборки — скомпилированные файлы, Docker-образы, отчёты. Если staging и production собираются в одном пайплайне, артефакты пересекаются
  • Конфигурацию runners — агентов, физически выполняющих задачи пайплайна. Общий runner — общие привилегии

В терминах MITRE ATT&CK (открытая база тактик и техник атак; T-коды вроде T1552 — её идентификаторы) это соответствует технике T1552.001 Credentials In Files — извлечение учётных данных из конфигурационных файлов и переменных окружения. Если staging-runner имеет доступ к продовым секретам, пентестер — или реальный злоумышленник — использует легитимные облачные учётные данные (T1078.004 Cloud Accounts) для эскалации в продакшен.

По данным IBM X-Force Threat Intelligence Index 2025, атаки с использованием действительных учётных данных выросли на 71% за год. CrowdStrike Global Threat Report 2025 фиксирует рост cloud intrusion-случаев на 26%. Утечка AWS_SECRET_ACCESS_KEY из staging-пайплайна — типовой сценарий в обоих отчётах. OWASP относит такие конфигурационные ошибки к категории A05:2021 Security Misconfiguration (неправильная настройка безопасности) — одной из самых частых в вебе.

Разграничение окружений в GitLab CI/CD: три уровня защиты продакшена

GitLab CI/CD поддерживает environments — именованные окружения (development, staging, production), привязанные к деплой-джобам. По документации GitLab, environments позволяют контролировать deployment variables per environment и отслеживать, какой код задеплоен куда. Проблема: по умолчанию окружения различаются только именем. Runner, переменные и артефакты — общие. Для реальной изоляции staging окружения от продакшена нужны три уровня:

Уровень Что изолируем Инструмент GitLab
Секреты API-ключи, токены, пароли Environment-scoped variables
Вычисления Среда выполнения джобов Выделенные tagged runners
Доступ Кто может деплоить Protected Environments + роли

Protected Environments и безопасность переменных CI/CD

Protected Environment (защищённое окружение) — механизм GitLab, который ограничивает круг лиц, имеющих право запускать деплой-джобы в конкретное окружение. Настраивается через Settings → CI/CD → Protected Environments.

На практике: даже если пентестер имеет роль Developer в проекте, он не запустит деплой в production, когда это окружение защищено и доступ разрешён только для Maintainer и Owner. По руководству по hardening GitLab, по умолчанию только ветка main получает protected pipeline — остальные ветки не защищены без явной настройки.

Environment-scoped variables (переменные, привязанные к окружению) — основной механизм secrets management в CI/CD. Переменная DATABASE_URL с scope staging видна только в джобах, где указано environment: name: staging. Джобы production-окружения получат свою версию DATABASE_URL или не получат ничего.

Настройка: Settings → CI/CD → Variables → при создании переменной в поле Environment scope выберите конкретное окружение вместо All (default). Именно дефолтное значение All — корневая причина большинства утечек: переменная с этим scope доступна из любого джоба любого окружения. Я видел это на каждом втором проекте — никто не меняет дефолт при быстрой настройке.

Для сред с повышенными требованиями к безопасности GitLab рекомендует внешний secrets manager — например, HashiCorp Vault — вместо встроенных переменных. Vault выдаёт короткоживущие токены (TTL 1–4 часа), что дополнительно сужает окно доступа пентестера к секретам.

Настройка runners GitLab для тестирования безопасности

Runner (раннер) — агент, установленный на сервере или в контейнере, который забирает и выполняет задачи пайплайна. По данным PwnedLabs, безопасность GitLab CI/CD сводится к контролю над тремя вещами: runner registration tokens, job tokens и изоляция executor.

Два типа executor, которые нужно различать:

  • Shell executor — команды выполняются прямо на хост-машине раннера, без изоляции. По данным PwnedLabs, это самый опасный вариант: скомпрометированный пайплайн даёт полный доступ к хосту
  • Docker executor — команды выполняются внутри контейнера. Безопаснее, но Docker в режиме privileged: true открывает путь к container escape (побег из контейнера на хост-систему)

Правило для staging-пентеста: staging-раннер и production-раннер — разные машины (или как минимум разные контейнеры с разными привилегиями). Разделение достигается через теги (tags): раннеру при регистрации присваивается уникальный тег, и только джобы с этим тегом попадут на него.

Доступ пентестера к staging: пошаговая настройка изолированной среды

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

Перед началом убедитесь:

  • GitLab CE/EE версии 16.0+ (для нового workflow регистрации раннеров с glrt- токенами; legacy registration tokens deprecated в GitLab 17.0, запланировано удаление в GitLab 20.0)
  • Права Maintainer или Owner в проекте
  • Отдельный сервер или VM для staging-раннера: Ubuntu 22.04+, минимум 2 ГБ RAM
  • Docker 20.10+ установлен на сервере раннера
  • Сетевой доступ от сервера раннера к GitLab-инстансу (порт 443 для HTTPS)

Работает если: GitLab Self-Managed или GitLab.com с правами на управление runners и переменными. Не работает если: используется только shared runner от GitLab SaaS без возможности регистрации собственных раннеров — уровень изоляции вычислений ограничен.

Шаг 1 — создаём изолированное staging Environment

В GitLab откройте проект → OperateEnvironmentsCreate an environment. Укажите имя pentest-staging и URL стейджинга (например, https://pentest-staging.internal.company.com).

Зачем отдельное имя, а не стандартный staging: чтобы переменные и runner-теги для пентеста не пересекались с переменными команды разработки. Это позволяет использовать auto_stop_in и on_stop для пентест-окружения независимо от основного staging.

Ожидаемый результат: в списке Environments появится pentest-staging со статусом available.

Затем защитите production: Settings → CI/CD → Protected Environments → добавьте production, разрешите деплой только для ролей Maintainer и Owner.

Шаг 2 — регистрируем выделенный runner с Docker executor

На сервере, выделенном под staging-пентест (не на продовом сервере!), установите GitLab Runner и зарегистрируйте его с уникальным тегом:

curl -L "https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh" | sudo bash
sudo apt install gitlab-runner
sudo gitlab-runner register \
  --url "https://gitlab.company.com" \
  --token "glrt-XXXXXXXXXXXX" \
  --executor "docker" \
  --docker-image "alpine:latest" \
  --tag-list "pentest-staging"

Что здесь происходит: раннер регистрируется с тегом pentest-staging. Только джобы с tags: [pentest-staging] попадут на этот раннер. Продовые джобы с другим тегом физически не могут выполниться на этой машине. Параметр --token — authentication token формата glrt-... из Settings → CI/CD → Runners → New project runner.

Ожидаемый результат: в Settings → CI/CD → Runners появится новый раннер с тегом pentest-staging и зелёным индикатором. Команда sudo gitlab-runner status на сервере вернёт gitlab-runner: Service is running.

После регистрации откройте config.toml на сервере раннера (обычно /etc/gitlab-runner/config.toml) и убедитесь, что privileged стоит в false — это предотвращает container escape.

Шаг 3 — привязываем секреты к конкретному окружению

Settings → CI/CD → Variables. Создайте переменные для пентест-среды:

  • DATABASE_URL → Environment scope: pentest-staging → значение: строка подключения к тестовой базе с синтетическими данными
  • AWS_ACCESS_KEY_ID → Environment scope: pentest-staging → значение: ключ от изолированного AWS-аккаунта или IAM-роль с минимальными правами
  • Флаг Masked — скрывает значение в логах пайплайна (в логе отобразится [MASKED])
  • Флаг Protected — делает переменную доступной только из protected branches

Продовые переменные (с scope production) остаются невидимыми для джобов в окружении pentest-staging. Проверить просто: добавьте в staging-джоб строку printenv | grep -i prod — если продовых переменных нет, вывод будет пустым.

Шаг 4 — конфигурируем .gitlab-ci.yml с разделением окружений

deploy_pentest_staging:
  stage: deploy
  tags: [pentest-staging]
  script:
    - ./scripts/deploy.sh
  environment:
    name: pentest-staging
    auto_stop_in: 2 weeks
    on_stop: stop_pentest_staging
  rules:
    - if: '$CI_COMMIT_BRANCH == "pentest"'

Разберём каждую строку — если вы только начинаете работать с GitLab CI, это важно:

  • tags: [pentest-staging] — джоб выполнится только на раннере с этим тегом. Продовый раннер не затрагивается
  • environment: name: pentest-staging — GitLab подставит переменные с scope pentest-staging
  • auto_stop_in: 2 weeks — окружение автоматически остановится через две недели. Удобно для ограниченного по времени доступа пентестера — не нужно помнить про ручную очистку
  • on_stop — ссылка на джоб очистки, который запустится при остановке окружения (удалит ресурсы)
  • rules — деплой срабатывает только из ветки pentest, push в main не затрагивает пентест-окружение

Ожидаемый результат: push в ветку pentest запускает пайплайн, деплой выполняется на выделенном раннере с изолированными секретами. В Operate → Environments видно pentest-staging с таймером обратного отсчёта (14 дней до автоостановки).

Шаг 5 — выдаём пентестеру ограниченный доступ

В проекте → Members → добавьте пентестера с ролью Developer (если он будет пушить свои тесты в ветку pentest) или Reporter (если нужен только read-access к коду и логам).

Что может каждая роль:

  • Reporter — не может запускать пайплайны вручную, не видит значения masked-переменных в логах
  • Developer — может пушить в незащищённые ветки и запускать пайплайны, но не может деплоить в protected environments (production)
  • Maintainer/Owner — видит всё, включая продовые секреты. Эту роль пентестеру давать нельзя

Установите дату истечения членства (Project → Members → Expiration date). После завершения пентеста доступ отзовётся автоматически — не придётся вспоминать про ручное удаление через месяц.

Безопасность CI/CD pipeline: когда изоляция staging НЕ работает

Даже при правильной настройке environments есть сценарии, где изоляция ломается:

Shared runners без тегов. Если в проекте включены shared runners (общие раннеры GitLab-инстанса) и джоб не содержит тегов — он может попасть на любой доступный раннер, включая тот, что обслуживает продакшен. Решение: в Settings → CI/CD → Runners отключите shared runners для проекта или сделайте все джобы tagged.

Wildcard scope у переменных. Переменная с scope All (default) доступна из любого джоба. Если кто-то создал PROD_DB_PASSWORD без scope — пентестер увидит её в staging-джобе. Решение: ревизия всех переменных через Settings → CI/CD → Variables. Убедитесь, что ни одна продовая переменная не имеет scope All. Это типичная ошибка — дефолт GitLab выставляет All, и при быстрой настройке его просто не меняют.

Docker-in-Docker с privileged: true. Если staging-раннер запускает Docker executor в привилегированном режиме — атакующий может выполнить container escape и получить доступ к хосту (T1610 Deploy Container). По данным PwnedLabs, это один из типовых путей от пайплайна к shell на раннере. Решение: privileged = false в config.toml; для сборки Docker-образов используйте kaniko или buildah — они не требуют привилегированного режима.

Артефакты между стейджами. Артефакты (файлы, создаваемые джобами — сборки, Docker-образы, отчёты) по умолчанию доступны между стейджами одного пайплайна. Если staging и production деплоятся в рамках одного пайплайна — артефакт staging-джоба может содержать данные для эскалации в продакшен (T1613 Container and Resource Discovery). Решение: разнесите окружения в разные пайплайны через rules — разные ветки или триггеры.

Защита продакшена от staging на сетевом уровне

CI/CD-изоляция на уровне GitLab — необходимый, но недостаточный слой. Staging-сервер, который может обращаться к продовой базе данных по сети, остаётся слабым звеном независимо от настроек пайплайна.

Минимум сетевой изоляции для трёх типичных инфраструктур:

В облаке (AWS/GCP): staging и production размещаются в разных VPC (Virtual Private Cloud — изолированная виртуальная сеть в облаке). Между VPC нет peering-соединения. Security groups staging-сервера разрешают входящий трафик только от staging-раннера и IP пентестера.

В Kubernetes: staging и production — разные namespaces (логические разделы кластера). Network Policy (сетевая политика) запрещает трафик между namespace’ами. Без явно заданной Network Policy pods из staging свободно обращаются к сервисам production — это поведение по умолчанию, и про него часто забывают.

На физических серверах / VM: VLAN-сегментация. Staging-сегмент не имеет маршрутов к продовому сегменту. Файрволл пропускает только порты, необходимые для работы staging-приложения.

Если staging-раннер находится в одной подсети с продовым сервером — никакие protected environments не спасут от бокового перемещения по сети (T1021.004 SSH, то есть lateral movement через SSH-соединения к соседним хостам).

Чеклист: перед допуском пентестера к staging-окружению

Шаг Действие Как проверить
1 Production — protected environment Settings → CI/CD → Protected Environments содержит production
2 Все продовые переменные имеют scope production Settings → CI/CD → Variables, колонка Environment scope, ни одна не All
3 Staging-раннер — отдельная машина с тегом Settings → CI/CD → Runners, раннер с тегом pentest-staging
4 Docker executor без privileged mode config.toml на раннере: privileged = false
5 Staging в отдельной сети от прода VPC / namespace / VLAN без маршрутов к продакшену
6 Пентестер — роль Developer или Reporter Project → Members, проверить роль и Expiration date
7 Auto-stop на staging environment .gitlab-ci.yml: auto_stop_in задан
8 Staging-БД с синтетическими данными Нет копии продовой базы, только тестовые данные

Большинство команд, которые приглашают пентестеров, считают настройку доступа административной задачей: «создадим юзера, дадим VPN — пусть работают». На практике подавляющее число инцидентов при тестировании безопасности staging происходит не потому, что пентестер «сломал» что-то неожиданно. Staging просто не был изолирован с самого начала — и то, что нашёл пентестер, мог найти любой сотрудник с доступом к CI/CD.

Я настраивал ephemeral-окружения (временные среды, создаваемые под конкретную задачу и удаляемые после) для внешних пентестеров на четырёх проектах за последний год. Закономерность одна: если у команды нет привычки разделять переменные по scope — продовые ключи лежат в All (default) и доступны из любой ветки. Не злой умысел — дефолтное поведение GitLab, которое никто не менял. Protected environments, scoped variables, tagged runners — три механизма, которые закрывают основную массу рисков. Но каждый из них требует осознанного действия: дефолт GitLab — это удобство разработки, не безопасность инфраструктуры.

Отдельно про тестовые данные. В каждом втором проекте staging-среда работала с копией продовой базы — «чтобы тестировать на реальных данных». Это антипаттерн, который создаёт не только технические, но и юридические риски: пентестер с доступом к персональным данным клиентов — прямое нарушение требований конфиденциальности ПДн (ст. 7 ФЗ-152). Синтетические данные сложнее в подготовке, но альтернатива — объяснять регулятору, почему внешний подрядчик имел доступ к базе с фамилиями и паспортными данными. Если разбираешься в DevOps и хочешь системно выстроить базу по безопасности инфраструктуры — на IB Basics эту связку CI/CD + изоляция разбирают в первых модулях с лабами.

Эту тему и смежные навыки разбирают на практике в курсе «Основы практик DevOps» Codeby Academy.