Большинство «DevSecOps-пайплайнов», которые я разбирал в командах, ломаются на одном и том же месте: проверки есть, но они в режиме warn-and-continue. Сканер находит CRITICAL — pipeline зелёный. Через полгода это уже не пайплайн, а ритуал.
Чтобы security-конвейер действительно блокировал прод, нужны две вещи: пять стадий, каждая с exit-code 1 по политике, и второй gate в кластере — на admission. Один gate в CI без admission-проверки — false sense of security: CI ловит то, что собрали; кластер принимает то, что задеплоили. Эти множества пересекаются, но не совпадают.
Пять стадий, и почему именно столько
Меньше пяти — оставляешь дыру; больше — это уже не стадии, а отдельный пайплайн.
1. Secret scan gitleaks / trufflehog / detect-secrets fail на любом match
2. SAST Semgrep / SonarQube / CodeQL fail на новых HIGH/CRITICAL
3. Dependency scan Trivy fs / Snyk / OWASP DC fail при CVSS ≥ 7 без VEX
4. Container scan Trivy image / Grype fail на HIGH/CRITICAL CVE без fix
5. Manifest / IaC Trivy config / Checkov / Conftest fail на security-misconfig
Логика — shift-left: чем раньше выловлен дефект, тем дешевле. Секреты ловим до того, как они попадут в историю git. SAST — до того, как код соберётся. Dependency-скан — до того, как уязвимый пакет окажется в образе. Image-скан — до push в registry. Manifest-скан — до того, как kubectl apply дойдёт до кластера.
Каждая стадия — отдельный bucket ответственности. Размывать их в «один большой scanner» — типичная ошибка: когда всё в Trivy, никто не отвечает за SAST. Когда всё в SonarQube — никто не отвечает за CVE-базы. Команды отдельные, баги — разные.
Stage 1: секреты, но не только в git
Базовый набор для git-уровня — gitleaks в pre-commit hook плюс detect-secrets с baseline в CI. Baseline нужен: без него каждый legitimate AKIA* в тесте будет ломать сборку.
Но есть второй слой, который пропускают чаще всего — секреты в слоях образа. RUN echo $TOKEN > .npmrc && ... && rm .npmrc оставляет токен в нижележащем immutable-слое; rm стирает только верхний view. docker history --no-trunc или просто docker save | strings | grep token достаёт его из registry. Лечится BuildKit-секретами:
RUN --mount=type=secret,id=npmrc cp /run/secrets/npmrc ~/.npmrc && npm ci
И в pipeline — trivy image --scanners secret поверх git-уровневого gitleaks. Два разных сканера, разные поверхности.
Stage 3–4: dependency vs container, не одно и то же
trivy fs сканирует package-lock.json, go.sum, requirements.txt — то, что в коде. trivy image сканирует собранный образ — там есть всё, что добавил базовый слой (apt-get install), и поверх — то, что положил pip install. CVE в libc базового образа trivy fs не увидит. CVE в дев-зависимости, которая в финальный образ не попадает, trivy image не покажет.
Канон в CI:
trivy image --severity HIGH,CRITICAL --exit-code 1 --ignore-unfixed $IMAGE
trivy fs --severity HIGH,CRITICAL --exit-code 1 .
trivy config --severity HIGH,CRITICAL --exit-code 1 ./k8s/
--ignore-unfixed — прагматика: пока апстрим не выпустил fix, блокировать сборку бесполезно. Но это создаёт .trivyignore как технический долг — каждая suppress’нутая CVE должна иметь комментарий why и JIRA-ticket. Без этого через год там будет помойка.
Stage 5 + admission: единственный надёжный паттерн
Stage 5 в CI — trivy config / Checkov / Conftest на манифесты и terraform plan -json. Ловит то, что в репозитории. Не ловит то, что задеплоили в обход CI или подменили tag — myapp:v1.2.3 в registry мог быть собран сегодня без сканов.
Поэтому второй gate — на admission:
- Kyverno verifyImages + cosign — образ принимается, только если он подписан в CI после прохождения сканов. Сборка без подписи → admission deny.
- OPA Gatekeeper / registry whitelist — pulling только из доверенных registries (ECR, Harbor, private GHCR). Образ из публичного
docker.io/random/xотбрасывается. - ValidatingAdmissionPolicy (CEL) — с K8s 1.36 встроено в API server, без webhook’а. Подходит для простых политик: no
:latest, no privileged, requiredLabels. Сложное (image verify, multi-resource generate) остаётся на Kyverno.
Дублирование? Намеренное. CI-scan ловит CVE в момент сборки; admission verify ловит deploy образа, обошедшего CI. Два слоя, каждый закрывает class отказов другого.
IaC: pre-apply gate как последний барьер
Terraform — отдельный риск: один terraform apply создаёт десятки ресурсов мгновенно. Pre-apply gate:
terraform plan -out plan.binary
terraform show -json plan.binary > plan.json
conftest test plan.json --policy ./policies/ # OPA-семантика на план
Rego-правила: deny cidr_blocks = ["0.0.0.0/0"] на 22/3389; deny Action: "*" в IAM; deny s3_bucket без server_side_encryption_configuration. Cheaper to fail PR, чем apply’нуть и потом revert.
Структурный страховочный слой — IAM Deny без тэга ManagedBy=Terraform на account-уровне: engineer в консоли физически не может изменить IaC-managed resource. Drift из возможности превращается в невозможность.
Что забывают подключить
exit-code 1везде. «Soft-fail» в CI — это «нет CI». Sonar quality-gate в режиме failure (а не warning) — обязательно..trivyignoreревьюится в PR. Suppression без обоснования — самый дешёвый способ обезоружить весь пайплайн.trivy db --skip-db-updateв CI — антипаттерн. CVE-база обновляется ~3 раза в сутки; вчерашняя — это другая база.- Audit log на admission deny. Без него admission-gate работает молча, и команда не видит, что Kyverno вчера блокировал три деплоя.
Главный сдвиг — перестать думать о DevSecOps как о «настроить сканер в CI». Это два слоя политики на одни и те же риски: shift-left в CI плюс admission-time enforce в кластере. Один без другого — это либо ритуал зелёных галочек, либо open kubeconfig для образа, обошедшего пайплайн.