Статья CI/CD для начинающих: Docker, GitLab CI и первый безопасный пайплайн

Ноутбук на тёмном антистатическом коврике с экраном, отображающим дашборд GitLab CI/CD с заблокированным этапом безопасности и подсвеченными ключами в терминале. Жёсткий голубой свет подчёркивает к...


На аудите финтех-компании получили доступ к внутреннему 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 при внутреннем пентесте:
  1. Initial Access - компрометация учётной записи разработчика или supply chain атака на зависимости (T1195.001). Атакующий получает доступ к репозиторию с .gitlab-ci.yml.
  2. Credential Access - извлечение секретов из конфигурационных файлов, переменных окружения, Docker-конфигов (Credentials In Files, T1552.001).
  3. Execution - внедрение вредоносного кода в пайплайн (Poisoned Pipeline Execution, T1677, OWASP CICD-SEC-4). Атакующий модифицирует .gitlab-ci.yml, добавляя шаг экфильтрации, и на следующем пуше Runner выполнит этот код автоматически. Отдельный вектор - заражённый Docker-образ (Malicious Image, T1204.003): pull вредоносного образа из публичного registry.
  4. Privilege Escalation - побег из контейнера на хост (Escape to Host, T1611). Runner с привилегированным Docker-сокетом - прямой путь к root на хосте.
  5. Lateral Movement - через деплой-токены и service account пайплайна атакующий перемещается в production-среду, Kubernetes-кластер или облачную инфраструктуру.
По модели OWASP DSOMM (DevSecOps Maturity Model) большинство команд сидят на первом уровне зрелости - безопасность добавляется post factum. Security Misconfiguration (OWASP A05:2021) в пайплайнах встречается чаще, чем в самих приложениях. Причина банальна: пайплайн настраивает джун, а ревьюит никто.

Пайплайн - точка, где сходятся credentials, исходный код и доступ к production. Умение читать .gitlab-ci.yml и находить в нём дыры - базовый навык пентестера наравне с анализом конфигов nginx.

Docker безопасность: от ошибок конфигурации до побега на хост​

1782000925364.webp

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​

1782000996022.webp

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

  • Аккаунт: 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.com shared runners идут бесплатно - для первого пайплайна ставить локальный Runner не обязательно.

Структура пайплайна​

Файл .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 собран людьми, которые сами ломали и строили пайплайны.
 
Последнее редактирование модератором:
Мы в соцсетях:

Взломай свой первый сервер и прокачай скилл — Начни игру на HackerLab

Похожие темы

🚀 Первый раз на Codeby?
Гайд для новичков: что делать в первые 15 минут, ключевые разделы, правила
Начать здесь →
🧭 Навигатор · ИБ 2026
Не знаешь, какой трек твой?
5 направлений ИБ, реальные зарплаты и точка входа для каждого — в одном треде.
JuniorSenior+
100K → 600K+ ₽ /мес
Открыть навигатор →
🔴 Свежие CVE, 0-day и инциденты
То, о чём ChatGPT ещё не знает — обсуждаем в реальном времени
Threat Intel →
💼 Вакансии и заказы в ИБ
Pentest, SOC, DevSecOps, bug bounty — работа и проекты от проверенных компаний
Карьера в ИБ →

HackerLab