На аудите финтех-компании получили доступ к внутреннему GitLab через скомпрометированную учётку разработчика - стандартный grey box, low-priv credentials. Первое, что проверили -
.gitlab-ci.yml в ключевых репозиториях. В трёх из пяти - захардкоженные AWS-ключи, токен Docker Registry и пароль от staging-базы открытым текстом. От initial access до контроля над S3-бакетами с продуктовыми дампами прошло меньше часа, и CI/CD-пайплайн был главным вектором lateral movement. CI/CD для начинающих обычно подаётся как "автоматизируй деплой и радуйся". По документам - да. На практике без security gates это автоматизация раздачи ключей от инфраструктуры.CI/CD-пайплайн как поверхность атаки
CI/CD (Continuous Integration / Continuous Delivery) - конвейер, который автоматически собирает, тестирует и доставляет код от репозитория до продакшена. Для разработчика это ускорение релизов. Для пентестера - цепочка серверов, токенов и конфигурационных файлов, где каждое звено может стать точкой входа.Зачем атакующему пайплайн? Три сценария с прямым финансовым импактом: credentials из CI/CD дают доступ к production и данным клиентов; injection кода в процесс сборки превращается в supply chain атаку на всех пользователей продукта; захват Runner-серверов открывает путь к криптомайнингу или lateral movement по сети. По данным Verizon DBIR 2025, 38% утечек данных связаны с кражей учётных данных. IBM X-Force зафиксировал рост атак с использованием валидных credentials на 71% год к году (2024 vs 2023). Цифры говорят сами за себя.
Вот как пайплайн ложится на kill chain при внутреннем пентесте:
- Initial Access - компрометация учётной записи разработчика или supply chain атака на зависимости (T1195.001). Атакующий получает доступ к репозиторию с
.gitlab-ci.yml. - Credential Access - извлечение секретов из конфигурационных файлов, переменных окружения, Docker-конфигов (Credentials In Files, T1552.001).
- Execution - внедрение вредоносного кода в пайплайн (Poisoned Pipeline Execution, T1677, OWASP CICD-SEC-4). Атакующий модифицирует
.gitlab-ci.yml, добавляя шаг экфильтрации, и на следующем пуше Runner выполнит этот код автоматически. Отдельный вектор - заражённый Docker-образ (Malicious Image, T1204.003): pull вредоносного образа из публичного registry. - Privilege Escalation - побег из контейнера на хост (Escape to Host, T1611). Runner с привилегированным Docker-сокетом - прямой путь к root на хосте.
- Lateral Movement - через деплой-токены и service account пайплайна атакующий перемещается в production-среду, Kubernetes-кластер или облачную инфраструктуру.
Пайплайн - точка, где сходятся credentials, исходный код и доступ к production. Умение читать
.gitlab-ci.yml и находить в нём дыры - базовый навык пентестера наравне с анализом конфигов nginx.Docker безопасность: от ошибок конфигурации до побега на хост
Docker - основа большинства CI/CD-пайплайнов. GitLab Runner по умолчанию исполняет задачи в Docker-контейнерах. Создаётся иллюзия изоляции, но реальность сложнее.
[Применимо: внутренний пентест, любая инфраструктура с Docker. Ограничение: Docker rootless mode и Podman снижают описанные риски]
Контейнер от root. Если
Dockerfile не содержит инструкции USER, контейнер работает от root. При доступе к Docker-сокету (типовая конфигурация для сборки образов в CI) атакующий эскалирует привилегии на хост. На GTFOBins задокументированы два сценария, и оба используют одну команду: docker run -v /:/mnt --rm -it alpine chroot /mnt sh. (а) Пользователь в группе docker на хосте - privesc до root через запуск контейнера с монтированием корневой ФС. (б) Контейнер с примонтированным /var/run/docker.sock - escape to host (T1611) через ту же команду, выполненную внутри контейнера против хостового daemon. Оба вектора дают root на хосте, но предусловия разные. Не работает, если Docker запущен в rootless mode или среда использует Podman.Заражённые базовые образы (Supply Chain Compromise, T1195.001). Джуны пишут
FROM python:latest или тянут образы из публичных реестров без верификации. Атакующий, контролирующий популярный Docker-образ, получает выполнение кода на каждом билде каждого проекта, использующего этот образ - это supply chain атака (T1195.001). Прямое пересечение с OWASP A06:2021 (Vulnerable and Outdated Components) и A08:2021 (Software and Data Integrity Failures). Я на одном проекте видел, как тянули образ с 50 звёздами на Docker Hub без единой проверки. Повезло, что он оказался чистым.Побег из контейнера (Escape to Host, T1611). Привилегированный режим (
--privileged), доступ к /var/run/docker.sock, монтирование чувствительных путей хоста - стандартные конфигурации "чтобы CI работал". Каждая из них - вектор побега. На пентестах регулярно попадаются Runner-ы с --privileged "для совместимости с Docker-in-Docker". Это как отключить файрвол для удобства.Минимальный безопасный Dockerfile для пайплайна:
Код:
FROM python:3.11-slim AS builder
WORKDIR /app
RUN adduser --disabled-password --gecos '' appuser
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY --chown=appuser:appuser . .
USER appuser
CMD ["python", "main.py"]
slim-образ вместо latest - меньше пакетов, меньше attack surface. Выделенный appuser вместо root. --no-cache-dir убирает кэшированные данные из образа. Multi-stage сборка через AS builder отделяет build-зависимости от runtime: в финальном образе нет gcc, make и прочего, что атакующий использует для post-exploitation.Когда Docker-изоляция не спасает. Контейнеры разделяют ядро с хостом. Уязвимость в containerd или runc даёт root на хосте вне зависимости от настроек контейнера. Для критичных сред - gVisor или Kata Containers. В облаке - GKE Shielded Nodes, AKS с Pod Security Standards. Container and Resource Discovery (T1613) позволяет атакующему, попавшему в контейнерную среду, перечислить остальные контейнеры и сервисы - поэтому сетевая сегментация между контейнерами обязательна.
Настройка CI/CD с нуля: GitLab CI пайплайн с security gates
Требования к окружению
- Аккаунт: GitLab.com (бесплатный тариф, shared runners включены) или self-hosted GitLab CE 16+
- Docker: Docker Engine 24+ (GNU/Linux) или Docker Desktop (macOS/Windows)
- RAM: минимум 4 ГБ для локального Runner с Docker executor; 8 ГБ при параллельном запуске Trivy и Semgrep
- ОС: Ubuntu 22.04+, macOS 13+, Windows 10+ с WSL2
- Сеть: онлайн - доступ к registry.gitlab.com и Docker Hub для загрузки образов сканеров
- Локальный Runner (опционально): установка через
gitlab-runner install, регистрация с Docker executor
Структура пайплайна
Файл.gitlab-ci.yml в корне репозитория определяет стадии и задачи. Минимальный безопасный gitlab ci пайплайн - четыре стадии: build, test, security, deploy. Каждая стадия - security gate: провал на предыдущей блокирует следующую. Принцип shift-left security: проблему ловим до попадания в продакшен.Стадия
build собирает Docker-образ. Используйте image: docker:24 с сервисом docker:dind. Команды: docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA . и docker push. Переменная $CI_REGISTRY_IMAGE автоматически указывает на встроенный Container Registry GitLab. $CI_COMMIT_SHA пинит образ к конкретному коммиту вместо плавающего тега latest, который может быть перезаписан.Стадия
test запускает юнит-тесты. Единственное правило безопасности: не прокидывайте production-секреты в тестовую стадию. Фиксчуры или мок-данные - и хватит.Стадия
security - то, что редко встречается в русскоязычных туториалах по CI/CD. Security gate, который блокирует деплой при обнаружении уязвимостей:
YAML:
security-scan:
stage: security
image:
name: aquasec/trivy:latest
entrypoint: [""]
before_script:
- export TRIVY_USERNAME=$CI_REGISTRY_USER
- export TRIVY_PASSWORD=$CI_REGISTRY_PASSWORD
script:
# Trivy обращается к registry напрямую (DinD не нужен),
# auth через TRIVY_USERNAME/PASSWORD для приватных registry
- trivy image --exit-code 1 --severity HIGH,CRITICAL
$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
- trivy fs --exit-code 1 --severity HIGH,CRITICAL .
allow_failure: false
--exit-code 1 заставляет Trivy вернуть ненулевой код при обнаружении уязвимостей HIGH или CRITICAL. GitLab интерпретирует это как провал задачи - стадия deploy не запустится. allow_failure: false - явное указание, что gate нельзя проигнорировать. trivy image сканирует Docker-образ на известные CVE в системных пакетах и зависимостях. trivy fs проверяет файловую систему проекта, включая IaC-конфиги.Для проверки секретов добавьте задачу с
gitleaks detect --source . --exit-code 1 в ту же стадию. GitLeaks сканирует файлы и git-историю на паттерны API-ключей, паролей и токенов. Рекомендую запускать его и как pre-commit хук: pre-commit install с конфигом в .pre-commit-config.yaml. Так секреты ловятся до коммита, а не после пуша.Стадию
deploy защитите правилом rules: - if: $CI_COMMIT_BRANCH == "main" - деплой только из основной ветки. Добавьте environment: production для отслеживания деплоев в интерфейсе GitLab.GitLab Ultimate vs Free: встроенные SAST, DAST, Dependency Scanning и Secret Detection доступны только в GitLab Ultimate. Согласно документации GitLab, эти инструменты автоматически встраиваются через
include шаблонов с полной интеграцией в UI. На бесплатном тарифе тот же функционал закрывается open-source инструментами - Trivy, Semgrep, GitLeaks. Разница в интеграции с Merge Request комментариями, но по детектированию - сопоставимо.Инструменты сканирования: выбор и ограничения
| Инструмент | Что сканирует | Ограничения | Когда использовать |
|---|---|---|---|
| Trivy (Aqua Security) | Docker-образы, зависимости, IaC | Только известные CVE; zero-day не увидит; логику кода не анализирует | Каждый build: образ перед push в registry |
| Semgrep (r2c) | Исходный код - паттерны SAST | Ложные срабатывания на сложной бизнес-логике; runtime не покрывает | MR: проверка нового кода до слияния |
| GitLeaks | Секреты в коде и git-истории | Кастомные форматы токенов не детектирует без дополнительных правил | Pre-commit хук + CI: каждый коммит |
| Bandit (Python) | Python-код на типичные уязвимости | Только Python; не заменяет полноценный SAST для других стеков | CI для Python-проектов |
Bandit упоминается как узкоспециализированная альтернатива Semgrep для Python: по разбору на dev.to, он ловит hardcoded passwords (B105) и shell injection (B605) с минимальной настройкой.
Ни один из этих инструментов не заменяет ручной пентест. Они ловят low-hanging fruit: известные CVE, утёкшие секреты, типовые паттерны уязвимого кода. Сложную бизнес-логику, IDOR, race conditions - не видят. SAST без DAST - слепая зона. Security gates поднимают порог входа для атакующего, но не закрывают его полностью.
DevOps для джуна: где теряются секреты пайплайна
Credentials In Files (T1552.001) - одна из самых эксплуатируемых техник в CI/CD. Три паттерна, которые встречаются на пентестах раз за разом:Секреты в ENV-инструкции Dockerfile. Строка
ENV DATABASE_URL=postgres://admin:password@prod-db:5432/main попадает в каждый слой Docker-образа и видна через docker history. Любой, кто получит доступ к Container Registry, получит и пароль от базы. Решение: передавайте секреты через --build-arg и не сохраняйте их в финальном образе, или используйте Docker Secrets для runtime.Переменные в .gitlab-ci.yml открытым текстом. GitLab CI/CD Variables (Settings -> CI/CD -> Variables) позволяют хранить секреты вне кода. Отметьте переменную как
Masked (не отображается в логах) и Protected (доступна только в protected-ветках). Без маскирования секрет утечёт в лог. Я видел случай, когда echo $DATABASE_URL в debug-скрипте отправлял пароль от production-базы в публичный лог GitLab. Разработчик забыл убрать debug, а ревью конфига никто не делал.Секреты в git-истории. Разработчик закоммитил
.env с credentials, потом удалил файл в следующем коммите. Файла нет в HEAD, но он живёт в git-истории. Команда git log --all --full-history -- .env покажет коммит, git show <hash>:.env - содержимое. GitLeaks обнаруживает такие артефакты, но только если его запустить.Для зрелых проектов стоит прикрутить HashiCorp Vault: секреты запрашиваются Runner-ом в момент выполнения через JWT-аутентификацию GitLab, не хранятся в переменных и не попадают в образы. Но для первого пайплайна GitLab CI/CD Variables с флагами
Masked + Protected - минимально необходимый уровень.Чеклист безопасного пайплайна
📚 Часть контента скрыта. Этот материал доступен участникам сообщества с рангом One Level или выше
Получить доступ просто — достаточно зарегистрироваться и проявить активность на форуме
Получить доступ просто — достаточно зарегистрироваться и проявить активность на форуме
Этот чеклист покрывает минимум DevOps безопасности для первого пайплайна. Для зрелых проектов ориентируйтесь на OWASP DSOMM - модель зрелости DevSecOps с конкретными метриками по категориям Build, Test, Patch, Culture.
Большинство русскоязычных туториалов по настройке CI/CD с нуля учат запускать пайплайн. Не защищать. Сотни статей объясняют, как написать
stages: [build, test, deploy] и радоваться зелёной галочке. Security gate? Trivy? GitLeaks? Почему --privileged в Docker-in-Docker - вектор побега на хост? Тишина. Shift-left security в отечественном DevOps-сообществе до сих пор воспринимается как "то, что добавим потом".На практике добавляют после инцидента. На одном из проектов я наблюдал, как команда два месяца катала в production Docker-образы от root с захардкоженным API-ключом к payment gateway. GitLab CI работал, деплой шёл, всё зелёное. Пока мы не показали, что одна скомпрометированная учётка джуна = полный доступ к платёжным данным.
Автоматизация деплоя без автоматизации проверок безопасности ускоряет не доставку фич, а доставку проблем. Security gate на семь строк YAML - тот самый Trivy с
--exit-code 1 - прикручивается за пять минут и блокирует категорию атак, которую я вижу на каждом втором аудите. Пять минут, семь строк, а в половине пайплайнов их нет. Если хочется выстроить путь от docker build до production-деплоя с пониманием, где именно ломается и как защитить - курс "Основы DevOps" на Codeby Academy собран людьми, которые сами ломали и строили пайплайны.
Последнее редактирование модератором: