секреты, учётные данные и сертификаты в CI/CD пайплайнах DevOps

Управление секретами в DevOps: учётные данные и сертификаты в CI/CD

11 минут

Пайплайнам нужны секреты, но размазанные копии и логи многократно увеличивают риск. В статье — централизованный подход, Vault с GitLab, CSI в Kubernetes и предохранители для ротации, доступа и аудита.

Почему секреты ломаются в первую очередь в автоматизированных пайплайнах

Приложениям и платформам нужны пароли к БД, API-ключи, TLS-материал, SSH-ключи и учётные данные внешних сервисов. В CI/CD это противоречие обостряется: джобам нужен программный доступ, а каждая копия в репозитории, скрипте или логе расширяет зону поражения. Команды сталкиваются с расползанием секретов по конфигам и переменным окружения, случайными коммитами, ручной ротацией, избыточными правами и слабым аудитом. Без явной архитектуры автоматизация ускоряет утечки, а не надёжность.

Проектирование централизованной плоскости секретов

Опирайтесь на выделенное хранилище — HashiCorp Vault, AWS Secrets Manager, Azure Key Vault или аналог. Предпочитайте краткоживущие учётные данные по запросу, выдавайте каждой стадии пайплайна минимально необходимый набор, автоматизируйте ротацию, журналируйте каждое чтение и изолируйте dev, staging и production по путям и политикам. На схеме ниже — общий поток: пайплайн аутентифицируется в менеджере секретов, получает ограниченный материал и только затем обращается к целевым системам.

Опорный поток
[CI/CD pipeline]
        |  authenticated request
        v
[Secrets manager] <-> [Encrypted storage]
        |  short-lived token / dynamic secret
        v
[Targets: apps & infra]

Bootstrap Vault для пайплайнов (AppRole и KV v2)

Ниже — ориентир на shell-команды Vault: включаем AppRole для машин, задаём роль CI с жёсткими TTL для токена и Secret ID, монтируем KV v2 на secret/ и заполняем пути, согласованные с джобами GitLab ниже. Значения только как заглушки — не переносите демо-пароли в реальные среды.

Vault CLI · AppRole и KV
# Enable AppRole for CI/CD
vault auth enable approle

vault write auth/approle/role/cicd-role \
    secret_id_ttl=20m \
    token_num_uses=10 \
    token_ttl=1h \
    token_max_ttl=2h \
    secret_id_num_uses=40

vault secrets enable -path=secret kv-v2

vault kv put secret/dev/database username="dbadmin" password="supersecret"
vault kv put secret/staging/api-service api_key="staging-key"
vault kv put secret/prod/api-service api_key="prod-key-12345" cert="@/path/to/cert.pem"

GitLab CI: одна аутентификация, чтение по джобам

Идентификаторы AppRole храните в защищённых переменных GitLab. В before_script обмениваем их на токен Vault, затем каждый джоб читает только свой путь через jq и обёртку KV v2 (.data.data.*). Staging и production разделены; production остаётся за ручным утверждением.

.gitlab-ci.yml
stages:
  - test
  - deploy

variables:
  VAULT_ADDR: "https://vault.example.com:8200"
  VAULT_ROLE_ID: "$CICD_ROLE_ID"
  VAULT_SECRET_ID: "$CICD_SECRET_ID"

before_script:
  - apt-get update && apt-get install -y jq curl
  - |
    VAULT_TOKEN=$(curl -s \
      --request POST \
      --data "{\"role_id\":\"$VAULT_ROLE_ID\",\"secret_id\":\"$VAULT_SECRET_ID\"}" \
      "${VAULT_ADDR}/v1/auth/approle/login" | jq -r '.auth.client_token')
    export VAULT_TOKEN

test:
  stage: test
  script:
    - |
      export DB_USERNAME=$(curl -s \
        --header "X-Vault-Token: $VAULT_TOKEN" \
        "${VAULT_ADDR}/v1/secret/data/dev/database" | jq -r '.data.data.username')
      export DB_PASSWORD=$(curl -s \
        --header "X-Vault-Token: $VAULT_TOKEN" \
        "${VAULT_ADDR}/v1/secret/data/dev/database" | jq -r '.data.data.password')
      echo "Running tests with database user: $DB_USERNAME"
      ./run-tests.sh
  only:
    - merge_requests
    - branches

deploy_staging:
  stage: deploy
  script:
    - |
      export API_KEY=$(curl -s \
        --header "X-Vault-Token: $VAULT_TOKEN" \
        "${VAULT_ADDR}/v1/secret/data/staging/api-service" | jq -r '.data.data.api_key')
      echo "Deploying to staging with API key: ${API_KEY:0:4}..."
      ./deploy-to-staging.sh
  environment:
    name: staging
    url: https://staging.example.com
  only:
    - main

deploy_production:
  stage: deploy
  script:
    - |
      export API_KEY=$(curl -s \
        --header "X-Vault-Token: $VAULT_TOKEN" \
        "${VAULT_ADDR}/v1/secret/data/prod/api-service" | jq -r '.data.data.api_key')
      echo "Deploying to production"
      ./deploy-to-production.sh
  environment:
    name: production
    url: https://example.com
  only:
    - main
  when: manual

Kubernetes: CSI-драйвер Vault и SecretProviderClass

Для нагрузок в кластере монтируйте секреты через Secrets Store CSI, а не прошивайте файлы в образ. SecretProviderClass сопоставляет пути Vault с ключами; pod монтирует CSI-том только для чтения в /mnt/secrets-store и при необходимости синхронизирует материал в обычный Secret Kubernetes, если заданы secretObjects.

SecretProviderClass
# SecretProviderClass for Vault (CSI)
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
  name: app-secrets-vault
spec:
  provider: vault
  secretObjects:
  - secretName: app-secrets
    type: Opaque
    data:
    - objectName: db-username
      key: username
    - objectName: db-password
      key: password
  parameters:
    vaultAddress: "https://vault.example.com:8200"
    roleName: "k8s-role"
    objects: |
      - objectName: "db-username"
        secretPath: "secret/data/prod/database"
        secretKey: "username"
      - objectName: "db-password"
        secretPath: "secret/data/prod/database"
        secretKey: "password"
Pod
# Pod mounting the CSI volume
apiVersion: v1
kind: Pod
metadata:
  name: app-pod
spec:
  containers:
  - name: app
    image: myapp:latest
    volumeMounts:
    - name: secrets-store01-inline
      mountPath: "/mnt/secrets-store"
      readOnly: true
  volumes:
  - name: secrets-store01-inline
    csi:
      driver: secrets-store.csi.k8s.io
      readOnly: true
      volumeAttributes:
        secretProviderClass: "app-secrets-vault"

Операционные предохранители: жизненный цикл, доступ, пайплайны

По возможности используйте динамические учётные данные от СУБД или облачного IAM. Ротируйте по графику с учётом риска, инвалидируйте старые значения и храните версии для отката. Выдавайте короткоживущие токены, применяйте mTLS между сервисами, опирайтесь на управляемые идентичности облака (IRSA, Azure Managed Identity). В пайплайнах не логируйте значения секретов, маскируйте переменные CI, ограничивайте секреты нужными джобами, сканируйте репозиторий на утечки и разделяйте инфраструктурные и прикладные секреты, если так требует модель ответственности.

Доказательства, соответствие требованиям и удобство разработки

Журналируйте успешные и неуспешные чтения, настройте оповещения по аномалиям геолокации или частоты, проводите квартальные пересмотры доступа и готовьте выгрузки для SOC 2, HIPAA или PCI. Делайте зашифрованные резервные копии с проверенным восстановлением. Локально используйте dev-режим Vault или docker-compose, подставляйте секреты на рантайме через envsubst и подобное, для переключения поведения предпочитайте feature flags вместо долгоживущих секретов, документируйте пути и владельцев ротации. Обучение превращает политику в привычку.

Начните с узкого контура, затем расширяйте покрытие

Зрелое управление секретами — это система, а не разовая установка Vault: централизация, принцип наименьших привилегий, автоматизация и аудит вместе сужают поверхность атаки и сохраняют скорость DevOps. Пилотируйте на самых чувствительных учётных данных — продакшен-БД и платёжные API — затем распространяйте те же контракты на все окружения. Относитесь к программе как к непрерывной: пересматривайте политики по мере эволюции угроз и стека.

Гигиена секретов больше всего страдает там, где уже есть трения в поставке, поэтому согласуйте контроли с чеклистом узких мест release-пайплайна.

Аудит имеет смысл только при качественной телеметрии; сопоставьте следы доступа к Vault с гайдом по наблюдаемости для небольших платформенных команд.