Skip to content

Глава 15. Безопасность CI/CD

«Если злоумышленник контролирует вашу пайплайн-систему сборки, он контролирует всё, что вы поставляете пользователям.» — NSA/CISA, «Defending Continuous Integration/Continuous Delivery (CI/CD) Environments», июнь 2023

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

15.1. Зачем: CI/CD как привилегированный вектор

Атаки на CI/CD-пайплайны

ИнцидентДатаВекторПоследствия
Codecov Bash UploaderЯнв–Апр 2021Модификация скрипта загрузки через утечку credentialsЭксфильтрация CI-переменных 29 000 клиентов
CircleCIДек 2022 — Янв 2023Infostealer → кража 2FA session cookie инженераУтечка customer secrets (env vars, API tokens, SSH keys)
tj-actions/changed-files (CVE-2025-30066)Мар 14, 2025Компрометация PAT @tj-actions-bot, модификация теговИзвлечение секретов из памяти Runner, 23 000+ репозиториев
reviewdog/action-setup (CVE-2025-30154)Мар 2025Скомпрометированный контрибьютор с write-доступомЧасть цепочки tj-actions, дамп секретов в логи

Общий паттерн: злоумышленник получает доступ к системе сборки и через неё — к секретам, артефактам и облачным ресурсам. NSA/CISA выделяют три ключевых сценария угроз (CSI «Defending CI/CD Environments», 28 июня 2023):

  1. Компрометация учётных данных разработчика для доступа к репозиторию.
  2. Supply chain compromise библиотеки или образа в пайплайне.
  3. Компрометация самой CI/CD-среды — модификация конфигов, инъекция зависимостей.

Принцип: нулевое доверие к пайплайну

Каждый компонент CI/CD должен следовать принципам Zero Trust:

15.2. OIDC-федерация: CI/CD без секретов

15.2.1. Проблема статических credentials

Традиционно CI/CD-системы хранят долгоживущие секреты (AWS Access Keys, GCP Service Account JSON, Azure Client Secret) в переменных среды. Это создаёт риски:

  • Секреты не ротируются автоматически.
  • Утечка одного секрета даёт постоянный доступ к облаку.
  • Нет связи между конкретным запуском и использованным credential.

OIDC-федерация решает эту проблему: CI-система выступает поставщиком идентичности (IdP), а облачный провайдер обменивает короткоживущий OIDC-токен на временные credentials.

15.2.2. GitHub Actions OIDC

GitHub Actions предоставляет OIDC-токен через встроенный провайдер https://token.actions.githubusercontent.com.

Ключевые характеристики токена:

ПараметрЗначение
Время жизни5 минут (только для обмена, не для прямого использования)
Обязательное разрешениеpermissions: id-token: write
Issuerhttps://token.actions.githubusercontent.com
Subject (примеры)repo:ORG/REPO:environment:prod, repo:ORG/REPO:ref:refs/heads/main
Аудитория (по умолчанию)URL организации/владельца

Ключевые claims:

  • repository — полное имя репозитория
  • repository_owner — организация
  • ref — ветка/тег, вызвавший запуск
  • environment — среда деплоя (если задана)
  • job_workflow_ref — ссылка на reusable workflow (для SLSA L3)
  • runner_environmentgithub-hosted или self-hosted

Кастомизация sub claim (REST API):

bash
# На уровне организации
curl -X PUT \
  -H "Authorization: Bearer $TOKEN" \
  https://api.github.com/orgs/MY-ORG/actions/oidc/customization/sub \
  -d '{"include_claim_keys": ["repo", "context", "job_workflow_ref"]}'

15.2.3. Настройка для AWS

Шаг 1: OIDC-провайдер в IAM

bash
aws iam create-open-id-connect-provider \
  --url https://token.actions.githubusercontent.com \
  --client-id-list sts.amazonaws.com \
  --thumbprint-list 6938fd4d98bab03faadb97b34396831e3780aea1

Примечание: Один OIDC-провайдер на issuer URL на аккаунт.

Шаг 2: IAM Role с trust policy

json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::ACCOUNT_ID:oidc-provider/token.actions.githubusercontent.com"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
        },
        "StringLike": {
          "token.actions.githubusercontent.com:sub": "repo:MY-ORG/MY-REPO:environment:production"
        }
      }
    }
  ]
}

Безопасность: Используйте StringEquals для aud и максимально специфичный subenvironment:production вместо *. Паттерн repo:ORG/* даёт доступ любому репозиторию в организации.

Шаг 3: Workflow

yaml
name: Deploy to AWS
on:
  push:
    branches: [main]

permissions:
  id-token: write
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production
    steps:
      - uses: actions/checkout@v4
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsRole
          aws-region: us-east-1
          # role-duration-seconds: 900  # по умолчанию 3600
      - run: aws sts get-caller-identity

STS-сессия по умолчанию — 1 час, диапазон 15 минут – 12 часов.

15.2.4. Настройка для GCP

Workload Identity Federation:

bash
# 1. Пул Workload Identity
gcloud iam workload-identity-pools create "github-pool" \
  --location="global" --project="$PROJECT_ID"

# 2. OIDC-провайдер
gcloud iam workload-identity-pools providers create-oidc "github-provider" \
  --location="global" \
  --workload-identity-pool="github-pool" \
  --issuer-uri="https://token.actions.githubusercontent.com" \
  --attribute-mapping="google.subject=assertion.sub,\
attribute.repository=assertion.repository" \
  --attribute-condition="assertion.repository=='MY-ORG/MY-REPO'"

# 3. Привязка к сервисному аккаунту
gcloud iam service-accounts add-iam-policy-binding \
  deploy-sa@$PROJECT_ID.iam.gserviceaccount.com \
  --role="roles/iam.workloadIdentityUser" \
  --member="principalSet://iam.googleapis.com/\
projects/$PROJECT_NUMBER/locations/global/\
workloadIdentityPools/github-pool/\
attribute.repository/MY-ORG/MY-REPO"
yaml
# GitHub Actions workflow
- uses: google-github-actions/auth@v3
  with:
    workload_identity_provider: "projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/github-pool/providers/github-provider"
    service_account: "deploy-sa@PROJECT_ID.iam.gserviceaccount.com"

15.2.5. Настройка для Azure

Azure использует Entra ID Federated Credentials:

  1. Зарегистрируйте приложение (или создайте Managed Identity) в Entra ID.
  2. Добавьте federated credential с issuer https://token.actions.githubusercontent.com.
  3. Subject должен точно совпадать с sub claim из GitHub-токена.
yaml
# GitHub Actions workflow
- uses: azure/login@v2
  with:
    client-id: ${{ secrets.AZURE_CLIENT_ID }}
    tenant-id: ${{ secrets.AZURE_TENANT_ID }}
    subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

Flexible Federated Identity Credentials (preview, март 2025): поддержка wildcards в sub и job_workflow_ref claims. Пример: claims['sub'] matches 'repo:contoso/*:environment:*'. Доступно только для Application objects (не для Managed Identity).

15.2.6. GitLab CI/CD OIDC

GitLab использует ключевое слово id_tokens (с GitLab 15.7). CI_JOB_JWT deprecated в 15.9, удалён в 17.0.

yaml
deploy:
  id_tokens:
    AWS_TOKEN:
      aud: https://sts.amazonaws.com
    VAULT_TOKEN:
      aud: https://vault.example.com
  script:
    - >
      export $(printf "AWS_ACCESS_KEY_ID=%s AWS_SECRET_ACCESS_KEY=%s AWS_SESSION_TOKEN=%s"
      $(aws sts assume-role-with-web-identity
      --role-arn arn:aws:iam::ACCOUNT:role/GitLabRole
      --role-session-name "gitlab-${CI_PROJECT_ID}-${CI_PIPELINE_ID}"
      --web-identity-token "$AWS_TOKEN"
      --query "Credentials.[AccessKeyId,SecretAccessKey,SessionToken]"
      --output text))

Sub claim GitLab: project_path:GROUP/PROJECT:ref_type:branch:ref:BRANCH_NAME.

15.2.7. Безопасность OIDC-федерации

Исследование Unit 42 «OH-MY-DC» (DEF CON 32, август 2024) выявило типичные ошибки:

ОшибкаРискИсправление
sub: "repo:ORG/*"Любой репозиторий организации получает доступУказать конкретный репозиторий и ветку/environment
Нет проверки audToken reuse между провайдерамиВсегда проверять audience
sub без веткиЛюбая ветка может деплоитьrepo:ORG/REPO:ref:refs/heads/main или environment:production
Fork PR с OIDCВнешний контрибьютор получает credentialsGitHub не генерирует OIDC-токены для форков (по умолчанию)

Сводка времени жизни credentials:

КомпонентTTLПримечание
GitHub OIDC-токен5 минутТолько для обмена
AWS STS-сессия1 час (по умолчанию)До 12 часов
GCP Access Token1 часПо умолчанию
Azure Entra ID-токен1 часПо умолчанию

15.3. SLSA: верификация цепочки сборки

15.3.1. Фреймворк SLSA

SLSA (Supply-chain Levels for Software Artifacts) — фреймворк OpenSSF для обеспечения целостности цепочки поставок ПО. В отличие от SBOM (инвентаризация), SLSA отвечает на вопрос: кто, как и из чего построил этот артефакт?

Версии спецификации:

ВерсияДатаСтатусКлючевое изменение
v1.0Апрель 2023ApprovedBuild Track L0-L3; концепция треков
v1.1Апрель 2025ApprovedУточнения Build Track, VSA
v1.2Ноябрь 2025ApprovedSource Track (authoring, review, management)

15.3.2. Build Track: уровни

УровеньТребованияПример
Build L0Нет гарантийСборка на ноутбуке разработчика
Build L1Provenance существуетАвтоматическая генерация provenance (может быть unsigned)
Build L2Hosted + signedСборка на хостинговой платформе, provenance подписан платформой
Build L3Hardened + isolatedBuilds изолированы друг от друга; ключи подписи недоступны для пользовательских шагов

15.3.3. Source Track (v1.2)

SLSA v1.2 (24 ноября 2025) ввёл Source Track, адресующий атаки типа xz-utils:

УровеньФокус
Source L0Нет требований
Source L1Source provenance существует
Source L2История и provenance (VCS с верифицированным происхождением)
Source L3Непрерывное применение технических контролей

15.3.4. Provenance: формат in-toto

SLSA provenance использует формат in-toto attestation с предикатом https://slsa.dev/provenance/v1:

json
{
  "_type": "https://in-toto.io/Statement/v1",
  "subject": [{
    "name": "my-app",
    "digest": { "sha256": "abc123..." }
  }],
  "predicateType": "https://slsa.dev/provenance/v1",
  "predicate": {
    "buildDefinition": {
      "buildType": "https://actions.github.io/buildtypes/workflow/v1",
      "externalParameters": {
        "workflow": { "ref": "refs/heads/main", "repository": "..." }
      },
      "resolvedDependencies": [
        { "uri": "git+https://github.com/...", "digest": {"gitCommit": "..."} }
      ]
    },
    "runDetails": {
      "builder": { "id": "https://github.com/actions/runner" },
      "metadata": {
        "invocationId": "https://github.com/.../actions/runs/12345",
        "startedOn": "2026-01-15T10:00:00Z"
      }
    }
  }
}

15.3.5. SLSA на практике

GitHub Actions: Artifact Attestations (GA июнь 2024)

yaml
jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read
      attestations: write
    steps:
      - uses: actions/checkout@v4
      - run: go build -o my-app .
      - uses: actions/attest-build-provenance@v2
        with:
          subject-path: my-app
  • По умолчанию: SLSA Build Level 2 (hosted + signed).
  • С reusable workflows: SLSA Build Level 3 (изоляция сборки от вызывающего workflow).

Верификация:

bash
gh attestation verify my-app --repo MY-ORG/MY-REPO

Google Cloud Build: нативный SLSA L3

Cloud Build автоматически генерирует provenance, соответствующий SLSA Build Level 3. Верификация интегрирована с Binary Authorization для GKE.

Docker BuildKit:

bash
docker buildx build --provenance=true -t my-app:v1.0 .

Attestation сохраняется в OCI-манифесте образа.

Принятие в экосистеме (Sigstore-signed SLSA provenance):

ПлатформаУровеньДата GA
GitHub ActionsBuild L2/L3Июнь 2024
Google Cloud BuildBuild L3Нативно
npmBuild L2Октябрь 2023
PyPIBuild L2Ноябрь 2024
HomebrewBuild L2Май 2024
Maven CentralBuild L2Январь 2025

15.4. Защита CI/CD-раннеров

15.4.1. GitHub Actions

GITHUB_TOKEN: принцип минимальных привилегий

Организации, созданные после 2 февраля 2023, получают GITHUB_TOKEN с read-only доступом по умолчанию. Для старых организаций — переключите в Settings → Actions → General.

yaml
# Явно объявляйте необходимые разрешения
permissions:
  contents: read
  packages: write
  id-token: write  # для OIDC

Ephemeral runners:

bash
# Регистрация self-hosted runner как ephemeral
./config.sh --url https://github.com/ORG/REPO \
  --token $REG_TOKEN \
  --ephemeral

GitHub назначает ровно один job ephemeral runner'у; после завершения — автоматическая дерегистрация.

Actions Runner Controller (ARC) на Kubernetes:

yaml
apiVersion: actions.github.com/v1alpha1
kind: AutoscalingRunnerSet
metadata:
  name: production-runners
spec:
  githubConfigUrl: "https://github.com/MY-ORG"
  githubConfigSecret: github-secret
  maxRunners: 10
  minRunners: 0
  template:
    spec:
      containers:
        - name: runner
          image: ghcr.io/actions/actions-runner:latest
          securityContext:
            runAsNonRoot: true
            readOnlyRootFilesystem: true

ARC-раннеры ephemeral по умолчанию — новый Pod для каждого Job.

Рекомендация: Раннер-поды — в отдельном namespace от оператора. НЕ размещайте на production-кластере.

15.4.2. GitLab Runner

Новая модель регистрации (GitLab 18+, май 2025):

Старые registration tokens (GR1348941...) удалены. Используйте runner authentication tokens (префикс glrt-):

bash
gitlab-runner register \
  --url https://gitlab.example.com \
  --token glrt-XXXXXXXXXXXXXXXXXXXX \
  --executor kubernetes

Kubernetes executor: создаёт новый Pod для каждого CI-задания (аналог ephemeral runners). Рекомендация: namespace_per_job для изоляции на уровне namespace.

15.4.3. Принципы hardening

ПринципРеализация
EphemeralОдин job — один runner. Нет persistent state
Network isolationЗапретить доступ к metadata-сервисам (169.254.169.254), production DB
Least privilegeGITHUB_TOKEN: read, явные permissions
Secret maskingАвтоматическая маскировка *** в логах (best-effort)
Pin versionsuses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
МониторингStepSecurity Harden-Runner (обнаружил tj-actions, ноябрь 2025)
Runner groupsИзоляция по trust level: production vs staging vs PR

15.4.4. Tekton Chains

Для Kubernetes-нативных пайплайнов Tekton Chains — контроллер, который автоматически генерирует SLSA provenance после завершения TaskRun/PipelineRun.

yaml
# Настройка Tekton Chains
kubectl patch configmap chains-config -n tekton-chains \
  -p='{"data":{
    "artifacts.taskrun.format":"slsa/v2alpha3",
    "artifacts.taskrun.storage":"oci",
    "transparency.enabled":"true",
    "signers.x509.fulcio.enabled":"true"
  }}'

Keyless-подпись через Fulcio (OIDC Kubernetes ServiceAccount → Fulcio certificate → Rekor log). Достижимый уровень: SLSA Build Level 2.

15.5. Лаборатория: OIDC-федерация + Sigstore

Цель

Настроить GitHub Actions workflow, который:

  1. Аутентифицируется в AWS без секретов через OIDC.
  2. Собирает контейнерный образ с SLSA provenance.
  3. Подписывает образ через Sigstore keyless.
  4. Верифицирует подпись и provenance локально.

Предварительные требования

  • GitHub-репозиторий с разрешением Actions
  • AWS-аккаунт с правами на IAM
  • Docker registry (ECR / ghcr.io)

Шаг 1: Создание OIDC-провайдера в AWS

bash
# Terraform
resource "aws_iam_openid_connect_provider" "github" {
  url             = "https://token.actions.githubusercontent.com"
  client_id_list  = ["sts.amazonaws.com"]
  thumbprint_list = ["6938fd4d98bab03faadb97b34396831e3780aea1"]
}

resource "aws_iam_role" "github_actions" {
  name = "github-actions-deploy"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect = "Allow"
      Principal = {
        Federated = aws_iam_openid_connect_provider.github.arn
      }
      Action = "sts:AssumeRoleWithWebIdentity"
      Condition = {
        StringEquals = {
          "token.actions.githubusercontent.com:aud" = "sts.amazonaws.com"
        }
        StringLike = {
          "token.actions.githubusercontent.com:sub" = "repo:MY-ORG/MY-REPO:ref:refs/heads/main"
        }
      }
    }]
  })
}

resource "aws_iam_role_policy_attachment" "ecr_push" {
  role       = aws_iam_role.github_actions.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryPowerUser"
}

Шаг 2: GitHub Actions workflow

yaml
# .github/workflows/build-sign-deploy.yaml
name: Build, Sign & Deploy

on:
  push:
    branches: [main]

permissions:
  id-token: write      # OIDC
  contents: read
  packages: write       # ghcr.io push
  attestations: write   # SLSA provenance

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  build-and-sign:
    runs-on: ubuntu-latest
    environment: production
    outputs:
      digest: ${{ steps.build.outputs.digest }}

    steps:
      # 1. Checkout
      - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6

      # 2. Аутентификация в AWS (OIDC, без секретов)
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/github-actions-deploy
          aws-region: us-east-1

      # 3. Login в container registry
      - uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      # 4. Сборка и push образа
      - uses: docker/build-push-action@v6
        id: build
        with:
          push: true
          tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
          provenance: true  # SLSA provenance в OCI-манифесте

      # 5. SLSA Build Provenance (GitHub Artifact Attestations)
      - uses: actions/attest-build-provenance@v2
        with:
          subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          subject-digest: ${{ steps.build.outputs.digest }}
          push-to-registry: true

      # 6. Подпись образа через Sigstore keyless
      - uses: sigstore/cosign-installer@v3
      - run: |
          cosign sign --yes \
            ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }}

Шаг 3: Локальная верификация

bash
# Верификация SLSA provenance через GitHub CLI
gh attestation verify \
  oci://ghcr.io/MY-ORG/MY-REPO@sha256:abc123... \
  --repo MY-ORG/MY-REPO

# Верификация подписи Cosign (keyless)
cosign verify \
  --certificate-identity-regexp="https://github.com/MY-ORG/MY-REPO/.*" \
  --certificate-oidc-issuer="https://token.actions.githubusercontent.com" \
  ghcr.io/MY-ORG/MY-REPO@sha256:abc123...

Шаг 4: Admission control (Kyverno)

Интеграция с Kyverno (глава 13) для блокировки неподписанных образов:

yaml
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: require-cosign-signature
spec:
  validationFailureAction: Enforce
  rules:
    - name: check-signature
      match:
        any:
          - resources:
              kinds: ["Pod"]
      verifyImages:
        - imageReferences: ["ghcr.io/MY-ORG/*"]
          attestors:
            - entries:
                - keyless:
                    issuer: "https://token.actions.githubusercontent.com"
                    subject: "https://github.com/MY-ORG/*"
          mutateDigest: true
          verifyDigest: true
          required: true

15.6. Чеклист безопасности CI/CD

КатегорияКонтрольПриоритет
CredentialsOIDC-федерация для всех облаковВысокий
CredentialsНет долгоживущих секретов в CI-переменныхВысокий
CredentialsРотация оставшихся секретов < 90 днейСредний
RunnersEphemeral runners (один job — один runner)Высокий
RunnersGITHUB_TOKEN: read по умолчаниюВысокий
RunnersPin actions по SHA (не по тегу)Высокий
Supply chainSLSA provenance для артефактовСредний
Supply chainSigstore keyless signingСредний
Supply chainAdmission control (верификация подписей)Средний
Supply chainSBOM-генерация в CI (см. главу 13)Средний
AccessBranch protection rules + required reviewsВысокий
AccessDeployment environments с approval gatesСредний
МониторингAudit log для CI/CD actionsСредний
МониторингNetwork egress мониторинг раннеровНизкий

15.7. Итоги

Принцип Zero TrustРеализация в CI/CD
Никогда не доверяйOIDC-федерация вместо статических секретов
Всегда проверяйSLSA provenance, подпись артефактов, admission control
Минимальные привилегииScoped GITHUB_TOKEN, специфичный sub claim, ephemeral runners
Предполагай взломPin actions по SHA, мониторинг раннеров, автоматическая ротация

Связь с другими главами:

  • Глава 7 (SPIFFE/SPIRE): Workload identity для CI/CD → облако.
  • Глава 13 (Безопасность приложений): SBOM, Sigstore, admission control.
  • Глава 14 (Kubernetes): Vault CSI для секретов, RBAC для CI ServiceAccount.

Источники

  • NSA/CISA: Defending CI/CD Environments (June 28, 2023): media.defense.gov/2023/Jun/28/2003249466/-1/-1/0/CSI_DEFENDING_CI_CD_ENVIRONMENTS.PDF
  • SLSA v1.2: slsa.dev/spec/v1.2/
  • GitHub Actions OIDC: docs.github.com/en/actions/concepts/security/openid-connect
  • GitHub Artifact Attestations: docs.github.com/en/actions/concepts/security/artifact-attestations
  • AWS OIDC для GitHub: docs.github.com/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services
  • GCP Workload Identity Federation: cloud.google.com/iam/docs/workload-identity-federation-with-deployment-pipelines
  • Azure Federated Credentials: learn.microsoft.com/en-us/entra/workload-id/workload-identity-federation
  • Unit 42 OH-MY-DC: unit42.paloaltonetworks.com/oidc-misconfigurations-in-ci-cd/
  • Tekton Chains: tekton.dev/docs/chains/signed-provenance-tutorial/
  • Sigstore: docs.sigstore.dev/

Опубликовано под лицензией CC BY-SA 4.0