MLSecRecommendations
Глава 08 · Безопасность

Рекомендации ИБ: Управление доступом и секретами в MLSec

~51 мин51 мин осталось 11 229 словОбновлено 13 мая 2026 г.VaultSOPSKeycloakRBACPoLPaudit

Рекомендации ИБ: Управление доступом и секретами в MLSec

Версия: 2.0 · Обновлено: 2026-04-23 · Теги: Vault, SOPS, Keycloak, RBAC, PoLP, audit, NHI, dynamic secrets · Tier: 1-4

TL;DR. HashiCorp Vault / OpenBao, SOPS, Keycloak, RBAC / ABAC, Row-Level Security, ротация секретов, audit log для ML-нагрузок. Отдельная ветвь - Non-Human Identity для агентов и автоматизации. Секрет, лежащий в env-переменной 6 месяцев, - это не секрет.

Навигация: обзор · инструменты · RBAC/ABAC · dynamic secrets · NHI для агентов · новый раздел интерактивных сценариев leak-атак · новый раздел "С чего начать по вашей роли".

1. Краткое содержание разделов

Глава охватывает десять связанных направлений. Ниже сводка ключевых идей каждого из них.

1.1. Цели и риски незащищённых секретов в ML-процессах

Незащищённые секреты (API-ключи, токены ML-трекинг-систем, пароли баз данных, ключи объектных хранилищ) в ML-пайплайнах формируют критические векторы атак:

  • Компрометация обучающей инфраструктуры - злоумышленник, получивший токен MLflow или ключ S3, может подменить обучающие данные (data poisoning) или внедрить backdoor в модель.
  • Утечка модельных артефактов - доступ к Model Registry позволяет извлечь проприетарные модели (model theft) или провести атаку model inversion для восстановления обучающих данных.
  • Несанкционированный доступ к персональным данным - секреты к хранилищам с PII/PHI открывают путь к массовым утечкам, влекущим за собой нарушение GDPR и иных регуляторных требований.
  • Lateral movement - статические, долгоживущие секреты, единожды скомпрометированные, позволяют атакующему перемещаться между компонентами инфраструктуры без ограничений по времени.

Цель - полностью исключить статические секреты из кода, артефактов и логов; минимизировать время жизни каждого секрета; обеспечить аудит каждого обращения к секрету.

1.2. Безопасное хранение ключей и токенов (Vault, SOPS)

HashiCorp Vault выступает центральным хранилищем секретов с поддержкой:

  • KV v2 - статические секреты (API-токены MLflow, ключи S3) с версионированием.
  • Database Secrets Engine - динамические учётные данные для PostgreSQL, MySQL, MongoDB с автоматическим отзывом по TTL.
  • PKI Engine - выпуск TLS-сертификатов для межсервисного mTLS.
  • Transit Engine - шифрование данных «на лету» без хранения открытого текста в Vault; используется как backend для SOPS.

Mozilla SOPS дополняет Vault, обеспечивая шифрование файлов конфигурации (YAML, JSON, ENV) непосредственно в Git-репозиториях. SOPS шифрует только значения, оставляя ключи читаемыми - это сохраняет возможность code review и diff.

1.3. Интеграция управления секретами с GitLab CI/CD

GitLab поддерживает нативную интеграцию с Vault через JWT-аутентификацию: каждый CI/CD-job получает JWT-токен через id_tokens: (стандарт с GitLab 15.7; legacy CI_JOB_JWT удалён в GitLab 17.0 в мае 2024 года), который обменивается на временный Vault-токен с ограниченными правами. Секреты можно получать через:

  • Директиву secrets: в .gitlab-ci.yml (нативная интеграция).
  • Vault CLI в before_script с явной аутентификацией через JWT.
  • SOPS с Vault Transit backend для расшифровки конфигов.

Дополнительно используются masked variables (маскирование в логах) и protected variables (доступ только из protected branches/tags).

1.4. Автоматическая ротация секретов и PoLP

Динамические секреты Vault с коротким TTL - основной механизм ротации. Рекомендуемые TTL:

Компонент TTL Обоснование
CI/CD job credentials 1h Ограничено временем выполнения job
Учётные данные для обучения модели 4h Длительные тренировки GPU-задач
Model serving credentials 15m Минимизация окна компрометации
Мониторинг и аудит 24h Непрерывный сбор метрик
SSH OTP для GPU-нод 30m Разовые операции администрирования

Статические секреты (API-токены MLflow, webhook-секреты) ротируются по расписанию: критичные - еженедельно, остальные - ежемесячно.

1.5. Принцип наименьших привилегий (PoLP) в MLSec

PoLP применяется на каждом уровне стека:

  • Vault - гранулярные HCL-политики с явными deny на нерелевантные пути.
  • Kubernetes RBAC - ServiceAccount с минимальными verbs (только get, list без create/delete).
  • PostgreSQL RLS - Row-Level Security на таблицах с данными обучения.
  • Keycloak - роли с минимальными scopes, привязанные к конкретным стадиям ML-цикла.

1.6. Аудит доступа

Аудит строится на централизованном сборе логов из всех компонентов:

Источник Что логируется Формат Интеграция
Vault Audit Log Все операции с секретами JSON (file + syslog) Filebeat -> ELK
GitLab Audit Events Deploy, изменение переменных, доступ JSON API Webhook -> ELK
Keycloak Events Аутентификация, авторизация, TOKEN_EXCHANGE JSON/Syslog Logstash
PostgreSQL logs + RLS Доступ к данным обучения SQL Log Filebeat
Kubernetes Audit API-вызовы к кластеру JSON Fluent Bit -> ELK
MinIO Access Logs Доступ к артефактам и датасетам JSON Filebeat
Istio EnvoyAccessLog Межсервисные вызовы JSON Fluent Bit
MLflow Эксперименты, модели, переходы стадий PostgreSQL Custom exporter

1.7. Политики доступа к данным, моделям и пайплайнам

Политики доступа строятся на трёх осях:

  • Данные - классификация по уровням чувствительности (Public, Internal, Confidential, Restricted) с привязкой к ролям через Vault-политики и PostgreSQL RLS.
  • Модели - контроль жизненного цикла (Experiment -> Staging -> Pre-production -> Production) с обязательным security gate перед промоцией в production.
  • Пайплайны - Separation of Duties через GitLab CODEOWNERS и multi-stage approvals.

1.8. Межсервисная аутентификация (Keycloak, Istio)

  • Keycloak выступает IdP (Identity Provider), выдавая JWT-токены сервисным аккаунтам через Client Credentials Grant. TTL access-токена - 5 минут, refresh - 30 минут.
  • Istio обеспечивает mTLS между всеми сервисами (PeerAuthentication с mode: STRICT) и контроль доступа через AuthorizationPolicy на уровне путей и HTTP-методов.
  • Vault аутентифицирует сервисы через JWT/OIDC auth method, привязанный к Keycloak.

1.9. Пример типовой архитектуры и ролей

flowchart TB
    IDP["Keycloak IdP<br/>Roles: data-engineer, ml-engineer,<br/>mlops, secops, pm"]
    subgraph PLANE[Control plane]
        direction LR
        CI["GitLab CI/CD"]
        V["Vault<br/>Secrets / PKI"]
        K8S["Kubernetes<br/>RBAC"]
    end
    MLP["ML Platform<br/>MLflow / Model Registry<br/>Training Data"]
    IDP -- OIDC / JWT --> CI
    IDP -- OIDC / JWT --> V
    IDP -- OIDC / JWT --> K8S
    CI --> MLP
    V --> MLP
    K8S --> MLP

Каждый компонент получает доступ только через Keycloak-аутентификацию и Vault-авторизацию. Прямой доступ к хранилищам секретов и данных запрещён.

1.10. Универсальные практики для open-source инфраструктуры

Все рекомендованные инструменты (Vault, SOPS, Keycloak, Istio, OPA, GitLab CE) - полностью open-source и self-hosted, что обеспечивает:

  • Полный контроль над данными и секретами без зависимости от облачных провайдеров.
  • Соответствие требованиям локализации данных.
  • Возможность глубокой кастомизации под специфику ML-инфраструктуры.
  • Воспроизводимость инфраструктуры через IaC (Terraform, Ansible, Helm).

2. Верификация инструментов и практик

2.1. Оценка зрелости предлагаемого стека

Инструмент Версия Назначение Зрелость Альтернативы
HashiCorp Vault 1.15+ Централизованное хранилище секретов Production-ready, широко используется в enterprise CyberArk Conjur, AWS Secrets Manager
Mozilla SOPS 3.8+ Шифрование конфигов в Git Стабильный, активно поддерживается Sealed Secrets, git-crypt
Keycloak 24+ IdP, SSO, OIDC/SAML Production-ready, CNCF-проект Authentik, Dex, Zitadel
Istio 1.20+ Service mesh, mTLS Production-ready, CNCF graduated Linkerd, Cilium
OPA/Gatekeeper 0.60+ Policy-as-Code Production-ready, CNCF graduated Kyverno, Polaris
GitLab CE 16+ CI/CD, source control Production-ready GitHub Actions, Jenkins

2.2. Критерии верификации

Каждый инструмент проверен по следующим критериям:

  1. Поддержка динамических секретов - Vault Database и PKI Engines подтверждённо генерируют credentials с автоматическим отзывом по TTL.
  2. JWT-интеграция - GitLab CI id_tokens корректно аутентифицируются в Vault через JWT auth method.
  3. Шифрование at-rest - SOPS с Vault Transit backend обеспечивает шифрование конфигурационных файлов без хранения ключей в репозитории.
  4. mTLS - Istio PeerAuthentication в режиме STRICT принудительно включает mTLS между всеми сервисами в namespace.
  5. Row-Level Security - PostgreSQL RLS корректно ограничивает доступ к строкам на основе роли текущего пользователя.
  6. Policy-as-Code - OPA Rego-политики интегрируются в CI/CD для автоматической проверки соответствия перед деплоем.

2.3. Известные ограничения

Ограничение Влияние Митигация
Vault - single point of failure Недоступность секретов при отказе HA-кластер (Raft), auto-unseal через HSM/Cloud KMS
SOPS - ручная ротация ключей шифрования Устаревание ключей Автоматизация через CI/CD job с Vault Transit key rotation
Keycloak - высокое потребление ресурсов Проблемы производительности при масштабировании Горизонтальное масштабирование, Infinispan кластер
Istio - операционная сложность Рост накладных расходов на сопровождение Ambient mesh (sidecarless), постепенное внедрение
OPA - кривая обучения Rego Задержки во внедрении Готовые библиотеки политик, обучение команды

3. Рекомендации ИБ

3.1. Обязательное внедрение HashiCorp Vault

Приоритет: КРИТИЧЕСКИЙ

Vault является центральным элементом управления секретами. Без него невозможно обеспечить динамическую выдачу, ротацию и аудит секретов.

Требования к внедрению:

  • Развернуть Vault в HA-конфигурации (минимум 3 ноды) с Raft-хранилищем.
  • Настроить auto-unseal через HSM или облачный KMS (недопустимо хранить unseal-ключи на тех же серверах).
  • Включить все Engines, необходимые для ML-стека:
    • KV v2 - для статических секретов (API-токены MLflow, webhook-секреты).
    • Database - для динамических credentials PostgreSQL, MongoDB.
    • PKI - для выпуска TLS-сертификатов (если Istio не покрывает все сценарии).
    • Transit - как backend для SOPS и для шифрования чувствительных полей данных.
    • SSH OTP - для доступа к GPU-нодам и training-серверам.
  • Настроить JWT auth method для интеграции с GitLab CI/CD.
  • Настроить OIDC auth method для интеграции с Keycloak (интерактивный доступ).
  • Включить audit-логирование на оба backend (file + syslog) для отказоустойчивости.

Пример активации ключевых компонентов:

# HA-кластер Vault с Raft
vault operator raft list-peers

# Включение движков секретов
vault secrets enable -path=secret kv-v2
vault secrets enable database
vault secrets enable pki
vault secrets enable transit
vault secrets enable ssh

# JWT auth для GitLab CI
vault auth enable jwt
vault write auth/jwt/config \
  jwks_url="https://gitlab.company.com/-/jwks" \
  bound_issuer="https://gitlab.company.com"

# OIDC auth для Keycloak
vault auth enable oidc
vault write auth/oidc/config \
  oidc_discovery_url="https://keycloak.company.com/realms/ml-platform" \
  oidc_client_id="vault-client" \
  oidc_client_secret="$OIDC_SECRET" \
  default_role="default"

3.2. Политика Zero Trust для ML-инфраструктуры

Приоритет: КРИТИЧЕСКИЙ

ML-инфраструктура должна работать в модели Zero Trust - ни один компонент не доверяет другому по умолчанию, каждый запрос аутентифицируется и авторизуется.

Принципы:

  1. Verify explicitly - каждый вызов между сервисами ML-платформы аутентифицируется через mTLS (Istio) или JWT (Keycloak).
  2. Least privilege access - каждый компонент получает минимально необходимый набор прав (см. раздел 3.4).
  3. Assume breach - архитектура проектируется с учётом возможной компрометации любого отдельного компонента. Blast radius минимизируется через сегментацию, короткие TTL и гранулярные политики.

Конкретные меры:

  • Запретить прямой доступ к базам данных из training-кода - только через Vault динамические credentials.
  • Запретить хранение секретов в переменных окружения Kubernetes - только через Vault Agent Injector.
  • Запретить межсервисное взаимодействие без mTLS - Istio PeerAuthentication в режиме STRICT.
  • Запретить доступ к Model Registry без JWT-токена с соответствующим scope.
  • Внедрить network policies в Kubernetes для ограничения сетевого доступа между namespace.
# Istio: обязательный mTLS для всех ML-сервисов
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
  name: ml-strict-mtls
  namespace: ml-production
spec:
  mtls:
    mode: STRICT
---
# Kubernetes: Network Policy для изоляции ML-namespace
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: ml-production-isolation
  namespace: ml-production
spec:
  podSelector: {}
  policyTypes:
    - Ingress
    - Egress
  ingress:
    - from:
        - namespaceSelector:
            matchLabels:
              purpose: api-gateway
        - namespaceSelector:
            matchLabels:
              purpose: monitoring
  egress:
    - to:
        - namespaceSelector:
            matchLabels:
              purpose: vault
        - namespaceSelector:
            matchLabels:
              purpose: database

3.3. Формализованная классификация данных (4 уровня)

Приоритет: ВЫСОКИЙ

Все данные, используемые в ML-процессах, классифицируются по четырём уровням. Классификация определяет требования к шифрованию, контролю доступа и аудиту.

Уровень Описание Примеры в ML Шифрование Доступ Аудит
Public Открытые данные Публичные датасеты, синтетические данные, open-source модели At-rest Все роли ML-платформы Стандартный
Internal Внутренние данные Агрегированные метрики, анонимизированные датасеты, параметры обучения At-rest + in-transit ML Engineers, Data Engineers Стандартный
Confidential Конфиденциальные данные Псевдонимизированные данные с бизнес-контекстом, проприетарные модели, feature stores At-rest + in-transit + field-level Senior ML Engineers, Data Engineers (с обоснованием) Обязательный
Restricted Ограниченные данные PII, PHI, финансовые данные, модели с встроенными PII At-rest + in-transit + field-level + Vault Transit Data Engineers + явное одобрение Юрист Обязательный + real-time alerts

Реализация через Vault-политики:

# Политика доступа по классификации данных
path "secret/data/datasets/public/*" {
  capabilities = ["read", "list"]
}

path "secret/data/datasets/internal/*" {
  capabilities = ["read"]
}

path "secret/data/datasets/confidential/*" {
  capabilities = ["deny"]
}

path "secret/data/datasets/restricted/*" {
  capabilities = ["deny"]
}

Реализация через PostgreSQL RLS:

-- Включение Row-Level Security
ALTER TABLE training_data ENABLE ROW LEVEL SECURITY;

-- Политика для ML-инженера: только public и internal
CREATE POLICY ml_engineer_data_access ON training_data
  FOR SELECT
  TO ml_engineer_role
  USING (
    classification IN ('public', 'internal')
  );

-- Политика для Data Engineer: полный доступ
CREATE POLICY data_engineer_full_access ON training_data
  FOR ALL
  TO data_engineer_role
  USING (true);

-- View-based доступ для изоляции столбцов с PII
CREATE VIEW training_data_anonymized AS
  SELECT id, feature_1, feature_2, label, classification
  FROM training_data
  WHERE classification IN ('public', 'internal');
-- PII-столбцы (name, email, phone) не включены в view

GRANT SELECT ON training_data_anonymized TO ml_engineer_role;

3.4. Обязательное применение PoLP на всех уровнях

Приоритет: КРИТИЧЕСКИЙ

Принцип наименьших привилегий применяется на каждом уровне стека без исключений.

Разделение прав по стадиям ML-цикла:

Роль Данные Модели Пайплайны Production Секреты
Data Scientist (Train) R (анонимизированные) R/W (experiments) R - R (training secrets)
Data Engineer R/W (все уровни) - R/W (data pipelines) - R (data secrets)
ML Engineer (Deploy) R (обезличенные) R/W (staging) R - R (staging secrets)
MLOps Engineer R R/W (production) R/W Deploy (с approval) R (prod secrets, ограничено)
Security/Compliance (Audit) Audit only Audit only R/W (security policies) Audit only R (audit paths)
CI/CD Service Account R (datasets) W (artifacts) Execute Controlled (с gates) R (ci/cd secrets)

Правило: ни одна роль не должна иметь прав, выходящих за рамки её прямых обязанностей. Доступ к production-секретам запрещён для ролей, не связанных с деплоем.

Реализация в Kubernetes RBAC:

# ServiceAccount для ML-сервиса с минимальными правами
apiVersion: v1
kind: ServiceAccount
metadata:
  name: ml-serving-sa
  namespace: ml-production
  annotations:
    vault.hashicorp.com/role: "ml-serving"
    vault.hashicorp.com/agent-inject: "true"
    vault.hashicorp.com/agent-inject-secret-db: "database/creds/ml-serving-role"
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: ml-serving-role
  namespace: ml-production
rules:
  - apiGroups: [""]
    resources: ["pods"]
    verbs: ["get", "list"]
  - apiGroups: [""]
    resources: ["configmaps"]
    verbs: ["get"]
  # Явный запрет: нет доступа к secrets Kubernetes напрямую
  # Секреты получаются только через Vault Agent Injector

Конструктор Vault-политики для роли ML Engineer (Deploy)

Включайте только те capabilities, которые реально нужны роли. Справа считается «privilege footprint» (чем меньше — тем ближе к PoLP) и появляются нарушения принципа least-privilege.

{
  "id": "ch08-vault-policy",
  "title": "Vault policy для ML Engineer (Deploy → Staging)",
  "description": "Каждый toggle — конкретная capability на конкретный path. Меньше включено — лучше PoLP. Включайте только то, что действительно требуется по матрице ролей §4.1.",
  "scoreLabel": "Privilege footprint (меньше — лучше)",
  "scoreMax": 100,
  "groups": [
    {
      "name": "Datasets",
      "items": [
        { "id": "ds-public-read", "label": "secret/data/datasets/public/* → read", "weight": 5, "default": true, "description": "Базовое чтение публичных датасетов — обычно нужно." },
        { "id": "ds-internal-read", "label": "secret/data/datasets/internal/* → read", "weight": 8, "default": true, "description": "Внутренние данные — часто нужно для validation/staging тестов." },
        { "id": "ds-conf-read", "label": "secret/data/datasets/confidential/* → read", "weight": 12, "description": "Confidential data — НЕ должно быть доступно ML Engineer (Deploy). Это роль Data Scientist." },
        { "id": "ds-restricted-read", "label": "secret/data/datasets/restricted/* → read", "weight": 18, "description": "Restricted (PII, биометрия) — категорически нет. Используйте Data Scientist роль с DPIA." },
        { "id": "ds-write", "label": "secret/data/datasets/* → create/update", "weight": 15, "description": "Запись датасетов — это роль Data Engineer. ML Engineer не должен модифицировать датасеты." }
      ]
    },
    {
      "name": "Models",
      "items": [
        { "id": "m-staging-rw", "label": "secret/data/models/staging/* → read/create/update", "weight": 6, "default": true, "recommended": true, "description": "Профильная capability — деплой в staging." },
        { "id": "m-staging-delete", "label": "secret/data/models/staging/* → delete", "weight": 10, "description": "Удаление staging-моделей — для cleanup. Лучше доверить CI/CD job, не интерактивному пользователю." },
        { "id": "m-prod-read", "label": "secret/data/models/production/* → read", "weight": 5, "default": true, "description": "Read-only на production — для отладки." },
        { "id": "m-prod-write", "label": "secret/data/models/production/* → write", "weight": 20, "description": "Запись в production — только для MLOps Engineer с MFA + approval." }
      ]
    },
    {
      "name": "Infrastructure secrets",
      "items": [
        { "id": "infra-db-staging", "label": "database/creds/ml-staging-role → read", "weight": 5, "default": true, "recommended": true, "description": "Динамические креды для staging БД — TTL 1 час." },
        { "id": "infra-db-prod", "label": "database/creds/ml-prod-role → read", "weight": 18, "description": "Production БД — нет для роли Deploy (Staging)." },
        { "id": "infra-cloud-staging", "label": "aws/creds/ml-staging → read", "weight": 6, "description": "Cloud creds для staging — если используется." },
        { "id": "infra-cloud-prod", "label": "aws/creds/ml-prod → read", "weight": 18, "description": "Production cloud — категорически нет." }
      ]
    },
    {
      "name": "Vault administration",
      "items": [
        { "id": "vault-policies", "label": "sys/policies/* → list/read/create", "weight": 25, "description": "Управление политиками Vault — только Vault Admins, не ML Engineer." },
        { "id": "vault-auth", "label": "auth/* → create", "weight": 25, "description": "Регистрация новых auth methods — admin-only." },
        { "id": "vault-leases", "label": "sys/leases/lookup → read (свои leases)", "weight": 3, "default": true, "description": "Просмотр своих lease'ов — безопасно." }
      ]
    }
  ],
  "rules": [
    { "id": "no-restricted", "type": "forbid-all", "items": ["ds-restricted-read"], "message": "ML Engineer (Deploy) не должен иметь доступа к restricted/PII данным — это роль Data Scientist с DPIA." },
    { "id": "no-prod-write", "type": "forbid-all", "items": ["m-prod-write"], "message": "Запись в production-модели — это роль MLOps Engineer, не Deploy. Promotion идёт через signed event с MFA." },
    { "id": "no-prod-cloud", "type": "forbid-all", "items": ["infra-cloud-prod"], "message": "Production cloud creds — для MLOps Engineer. Deploy в staging не требует prod-cloud." },
    { "id": "no-vault-admin", "type": "forbid-all", "items": ["vault-policies"], "message": "Управление Vault-политиками — только Vault Admin. ML Engineer не должен иметь sys/policies." },
    { "id": "min-staging", "type": "require-all", "items": ["m-staging-rw"], "message": "Без write на staging models роль не выполняет своё назначение." }
  ],
  "thresholds": [
    { "from": 0, "to": 25, "label": "Идеальный PoLP — минимум attack surface", "tone": "ok" },
    { "from": 25, "to": 50, "label": "Целевой уровень — стандартный профиль ML Engineer (Deploy)", "tone": "info" },
    { "from": 50, "to": 75, "label": "Повышенный footprint — proceed with caution, требует обоснования", "tone": "warn" },
    { "from": 75, "to": 100, "label": "Чрезмерные привилегии — типичный путь к incident, фактически Vault Admin", "tone": "err" }
  ]
}

3.5. Динамические секреты для CI/CD

Приоритет: КРИТИЧЕСКИЙ

Каждый CI/CD job должен получать уникальные, короткоживущие credentials через Vault. Статические токены в GitLab CI variables допустимы только для начальной аутентификации в Vault.

Схема интеграции GitLab CI + Vault:

# Вариант 1: нативная интеграция через secrets keyword
variables:
  VAULT_SERVER_URL: "https://vault.company.internal"

train_model:
  stage: train
  id_tokens:
    VAULT_ID_TOKEN:
      aud: https://vault.company.internal
  secrets:
    MLFLOW_TOKEN:
      vault: ml/cicd/mlflow_token@secret
      token: $VAULT_ID_TOKEN
    S3_ACCESS_KEY:
      vault: ml/cicd/s3_access_key@secret
      token: $VAULT_ID_TOKEN
  script:
    - python train.py
# Вариант 2: динамические credentials БД через Vault CLI
train_with_dynamic_creds:
  stage: train
  id_tokens:
    VAULT_ID_TOKEN:
      aud: https://vault.company.internal
  script:
    - export VAULT_TOKEN=$(vault write -field=token auth/jwt/login
        role=ml-cicd-role jwt=$VAULT_ID_TOKEN)
    - export DB_CREDS=$(vault read -format=json database/creds/ml-training-role)
    - export DB_USER=$(echo $DB_CREDS | jq -r '.data.username')
    - export DB_PASS=$(echo $DB_CREDS | jq -r '.data.password')
    - python train.py --db-user=$DB_USER --db-pass=$DB_PASS
  after_script:
    # Credentials автоматически отзываются по TTL,
    # но можно отозвать явно для немедленной очистки
    - vault lease revoke -prefix database/creds/ml-training-role
# Вариант 3: SOPS + Vault Transit для зашифрованных конфигов
decrypt_and_train:
  stage: train
  image: python:3.13
  before_script:
    - apt-get update && apt-get install -y sops
    # GitLab 15.7+: задайте id_tokens с aud=$VAULT_ADDR в job-блоке —
    # переменная VAULT_ID_TOKEN будет инжектирована автоматически.
    # Legacy CI_JOB_JWT удалён в GitLab 17.0 (май 2024).
    - export VAULT_TOKEN=$(vault write -field=token
        auth/jwt/login role=ml-cicd-role jwt=$VAULT_ID_TOKEN)
    - sops -d config/model_config.enc.yaml > config/model_config.yaml
  script:
    - python train.py --config config/model_config.yaml
  after_script:
    - rm -f config/model_config.yaml

Правила безопасности переменных GitLab CI:

Тип переменной Применение Видимость в логах Доступность
Обычная Нечувствительные данные (MODEL_NAME, BATCH_SIZE) Видна Все ветки
Masked Токены, пароли Скрыта (замена на ***) Все ветки
Protected + Masked Production-секреты Скрыта Только protected branches
File Сертификаты, ключи Записывается в файл Настраивается

3.6. Обязательная MFA для всех интерактивных аккаунтов

Приоритет: ВЫСОКИЙ

Все интерактивные (человеческие) аккаунты в ML-инфраструктуре должны использовать многофакторную аутентификацию (MFA).

Область применения:

  • GitLab - вход в UI, операции с protected branches.
  • Keycloak - вход в административную консоль и ML-платформу.
  • Vault UI - интерактивный доступ к секретам (для отладки и аудита).
  • Kubernetes Dashboard (если используется) - доступ к кластеру.

Конфигурация Keycloak:

{
  "realm": "ml-platform",
  "requiredActions": ["CONFIGURE_TOTP"],
  "otpPolicyType": "totp",
  "otpPolicyAlgorithm": "HmacSHA256",
  "otpPolicyDigits": 6,
  "otpPolicyPeriod": 30,
  "otpPolicyInitialCounter": 0,
  "bruteForceProtected": true,
  "maxFailureWaitSeconds": 900,
  "maxDeltaTimeSeconds": 43200,
  "failureFactor": 5,
  "permanentLockout": false,
  "waitIncrementSeconds": 60
}

Исключения: сервисные аккаунты (machine-to-machine) аутентифицируются через Client Credentials Grant с коротким TTL access-токена (5 минут) и refresh-токена (30 минут) и не требуют MFA.

Какой MFA-метод подходит вашему сценарию

{
  "id": "ch08-mfa-method",
  "title": "Подбор MFA-метода для роли в ML-инфраструктуре",
  "start": "q1",
  "nodes": {
    "q1": {
      "type": "question",
      "text": "Какой уровень доступа у пользователя?",
      "sub": "MFA-метод должен быть пропорционален риску. Production secrets требуют hardware token; обычный read-only — TOTP.",
      "choices": [
        { "label": "Production secrets (Vault prod, sys/policies, model registry write)", "next": "q2-prod" },
        { "label": "Sensitive read (audit logs, restricted datasets metadata)", "next": "q2-sensitive" },
        { "label": "Стандартный ML-инженер (staging, internal data)", "next": "q2-standard" },
        { "label": "Read-only / dashboards / SaaS-продукты ML-команды", "next": "leaf-totp" }
      ]
    },
    "q2-prod": {
      "type": "question",
      "text": "Готова ли организация раздать hardware tokens (~$50/чел)?",
      "choices": [
        { "label": "Да — есть бюджет на YubiKey / Titan / SoloKey для ~50 человек с prod-доступом", "next": "leaf-yubikey" },
        { "label": "Нет, но есть managed devices с TPM (Mac / iPhone / managed Windows)", "next": "leaf-passkey" },
        { "label": "Нет — только software-tokens", "hint": "Не рекомендуется для prod", "next": "leaf-totp-with-warning" }
      ]
    },
    "q2-sensitive": {
      "type": "question",
      "text": "Пользователи в основном работают с одного устройства или с разных?",
      "choices": [
        { "label": "Одно устройство (managed laptop)", "next": "leaf-passkey" },
        { "label": "Разные устройства (BYOD, работа из дома + офиса)", "next": "leaf-totp" }
      ]
    },
    "q2-standard": {
      "type": "question",
      "text": "Есть ли SSO с уже установленной MFA на корпоративном IdP?",
      "choices": [
        { "label": "Да — IdP MFA проверяется на уровне SSO (Keycloak / Okta / AD FS)", "next": "leaf-sso-mfa" },
        { "label": "Нет — каждый сервис аутентифицируется отдельно", "next": "leaf-totp" }
      ]
    },
    "leaf-yubikey": {
      "type": "leaf",
      "tone": "ok",
      "title": "Hardware token (YubiKey / Titan) — золотой стандарт",
      "summary": "FIDO2 / WebAuthn — phishing-resistant, не требует battery, не подвержен malware на устройстве. Стоимость ~$50/токен компенсируется снижением incident risk.",
      "details": [
        "FIDO2 (резидентные ключи) — поддержка большинства IdP с 2023",
        "Backup token каждому пользователю (потеря основного ≠ потеря доступа)",
        "Регистрация в IdP с supervisor approval — нельзя по email только",
        "Революция YubiKey 5C / 5Ci с NFC — работает на iOS / Android / desktop",
        "Compliance: соответствует NIST SP 800-63B AAL3 — высший уровень"
      ]
    },
    "leaf-passkey": {
      "type": "leaf",
      "tone": "info",
      "title": "Passkey (FIDO2 на TPM устройства)",
      "summary": "Современная альтернатива hardware token: использует TPM/Secure Enclave устройства. Phishing-resistant. Не требует отдельного device.",
      "details": [
        "Apple iOS 16+ / macOS 13+, Android 9+, Windows 10+ с TPM 2.0",
        "Sync через iCloud / Google / Microsoft Account (выбирайте корпоративную опцию для compliance)",
        "Backup: registered passkey на 2-х устройствах (laptop + phone)",
        "Compliance: NIST AAL2 minimum, AAL3 при device attestation",
        "Подходит когда не хочется выдавать hardware tokens"
      ]
    },
    "leaf-sso-mfa": {
      "type": "leaf",
      "tone": "info",
      "title": "SSO MFA — единая точка проверки",
      "summary": "Если IdP корректно проверяет MFA, то downstream-сервисы (MLflow, Jupyter, Vault через OIDC) наследуют её. Главное — установить session timeout.",
      "details": [
        "Session timeout 8 часов для standard, 1 час для prod-доступа",
        "Step-up authentication: при доступе к prod path — re-prompt MFA даже в активной сессии",
        "Audit log: все step-up events отдельной строкой для SIEM-корреляции",
        "Конфигурация Keycloak: см. §3.12 hardening"
      ]
    },
    "leaf-totp": {
      "type": "leaf",
      "tone": "warn",
      "title": "TOTP (Google / Microsoft / 1Password Authenticator)",
      "summary": "Базовый уровень MFA. Подходит для read-only / standard ML-engineer. Phishing-vulnerable, но лучше чем ничего.",
      "details": [
        "Bruteforce protection: 5 попыток → 15 мин lockout (см. конфиг выше)",
        "Backup: рекомендовать сохранение initial secret в password manager (1Password / Bitwarden)",
        "Не использовать SMS — проще phishing’у, требует phone porting attack для bypass",
        "Compliance: NIST AAL2 — приемлемо для большинства Tier-3 систем"
      ]
    },
    "leaf-totp-with-warning": {
      "type": "leaf",
      "tone": "err",
      "title": "TOTP для prod — НЕ рекомендуется",
      "summary": "Phishing-resistance критична для production-доступа. TOTP можно «отдать» атакующему через AiTM (adversary-in-the-middle, например, evilginx). Hardware token — must.",
      "details": [
        "AiTM атаки на TOTP — задокументированы и регулярны (см. отчёты CrowdStrike, Mandiant 2023–2024)",
        "Если YubiKey невозможен — минимум passkey (TPM-bound)",
        "Если совсем нечего — TOTP + IP-restriction + step-up на каждое prod-action + анализ behavior anomalies",
        "Регуляторика: 716-П / NIST AAL3 не пройдут TOTP-only для production"
      ]
    }
  }
}

3.7. Ротация секретов: автоматизация и мониторинг

Приоритет: ВЫСОКИЙ

Матрица ротации:

Тип секрета Механизм ротации Периодичность Ответственный
БД credentials (dynamic) Vault Database Engine, TTL Автоматически при каждом запросе (TTL=1h) Vault
SSH-ключи (OTP) Vault SSH Engine Одноразовые (TTL=30m) Vault
API-токены MLflow Скрипт ротации + Vault KV Еженедельно CI/CD job
S3/MinIO access keys Скрипт ротации + Vault KV Еженедельно CI/CD job
TLS-сертификаты Vault PKI / cert-manager Автоматически (TTL=30d) Vault / cert-manager
Keycloak client secrets Keycloak Admin API Ежемесячно CI/CD job
Vault root token Ручная ротация После каждого использования Security team
SOPS encryption keys Vault Transit key rotation Ежеквартально Security team

Скрипт автоматической ротации статических секретов:

#!/bin/bash
# rotate_static_secrets.sh
# Вызывается по cron или из CI/CD scheduled pipeline

set -euo pipefail

VAULT_ADDR="https://vault.company.internal"
LOG_FILE="/var/log/ml-secops/rotation.log"

rotate_mlflow_token() {
  NEW_TOKEN=$(python3 /opt/scripts/generate_mlflow_token.py)
  vault kv put secret/ml/cicd/mlflow_token \
    value="$NEW_TOKEN" \
    rotated_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
    rotated_by="rotation-service"
  echo "$(date -u): MLflow token rotated successfully" >> "$LOG_FILE"
}

rotate_s3_keys() {
  NEW_KEYS=$(python3 /opt/scripts/rotate_minio_keys.py)
  ACCESS_KEY=$(echo "$NEW_KEYS" | jq -r '.access_key')
  SECRET_KEY=$(echo "$NEW_KEYS" | jq -r '.secret_key')
  vault kv put secret/ml/cicd/s3_credentials \
    access_key="$ACCESS_KEY" \
    secret_key="$SECRET_KEY" \
    rotated_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
  echo "$(date -u): S3 credentials rotated successfully" >> "$LOG_FILE"
}

rotate_mlflow_token
rotate_s3_keys

# Уведомление в Slack/Mattermost
curl -X POST "$WEBHOOK_URL" \
  -H 'Content-Type: application/json' \
  -d '{"text":"Secrets rotation completed successfully"}'

Мониторинг ротации:

  • Alerting при неудачной ротации (Prometheus + Alertmanager).
  • Дашборд со статусом всех секретов и датой последней ротации (Grafana).
  • Alert при приближении TTL статического секрета к порогу (7 дней до плановой ротации).

Калькулятор: TTL динамических секретов vs operational overhead

{
  "id": "ch08-secret-ttl",
  "title": "TTL Vault dynamic secret: trade-off compromise window vs Vault load",
  "description": "Покрутите ползунок TTL — увидите, как меняется compromise window (сколько у атакующего времени), нагрузка на Vault, и UX-разрывы пользователей. Цифры — для типичного банка с 50–100 ML-инженерами и 200–500 CI-job/день.",
  "min": 1,
  "max": 1440,
  "step": 1,
  "default": 60,
  "unit": " мин",
  "axisLabel": "TTL динамического credentials",
  "tracks": [
    { "label": "Compromise window (мин)", "compute": "x", "format": "fixed1", "tone": "err", "hint": "Если creds украли через 1 минуту после issue — атакующий имеет (TTL - 1) мин до автоматической revoke. Прямое прокси для blast radius." },
    { "label": "Vault renew RPS", "compute": "1500 / x", "format": "fixed1", "tone": "info", "hint": "Каждый клиент рено TTL/2. На 60 мин ≈ 25 RPS на 1500 одновременных клиентов. На 5 мин — 300 RPS, требует HA Vault кластер." },
    { "label": "% job-разрывов из-за expiration mid-job", "compute": "x < 30 ? 100 / x : 0.5 + 50 / x", "format": "%", "tone": "warn", "hint": "Long-running ML-jobs (training) могут пережить expiration — нужен renew loop в коде. На <15 мин типично теряется ~7% jobs без правильного renew." },
    { "label": "Compliance score (716-П / NIST)", "compute": "x <= 60 ? 100 : (x <= 240 ? 70 : (x <= 720 ? 30 : 10))", "format": "%", "tone": "ok", "hint": "716-П / NIST AI 600 для Tier-1 ожидает TTL ≤ 1 ч для credentials с прямым доступом к ПДн. Tier-3 — до 4 ч приемлемо." }
  ],
  "regions": [
    { "from": 1, "to": 15, "label": "Параноидальный — для Tier-1 / production-deploy жобов; требует HA Vault", "tone": "ok" },
    { "from": 15, "to": 60, "label": "Industry sweet spot — баланс security / operational, для CI-job и Tier-2", "tone": "info" },
    { "from": 60, "to": 240, "label": "Расслабленный — для долгих training jobs или dev environments", "tone": "warn" },
    { "from": 240, "to": 1440, "label": "Слишком долго для prod — фактически long-lived token, не подходит для Tier-1/2", "tone": "err" }
  ]
}

3.8. Аудит доступа: централизация в SIEM

Приоритет: ВЫСОКИЙ

Все логи доступа агрегируются в централизованной SIEM-системе (ELK/OpenSearch, Splunk или аналог) для корреляции событий и обнаружения аномалий.

Включение аудита Vault:

# Файловый аудит-лог (основной)
vault audit enable file file_path=/var/log/vault/audit.log

# Syslog-аудит (резервный, для интеграции с SIEM)
vault audit enable syslog tag="vault" facility="AUTH"

Формат записи аудита Vault (пример):

{
  "time": "2025-01-15T10:23:45Z",
  "type": "request",
  "auth": {
    "token_type": "service",
    "policies": ["ml-engineer"],
    "metadata": {"role": "ml-engineer", "username": "ivanov"}
  },
  "request": {
    "operation": "read",
    "path": "secret/data/ml/training/config",
    "remote_address": "10.0.1.42"
  }
}

Ключевые алерты для SIEM:

Событие Severity Действие
Доступ к restricted данным HIGH Немедленное уведомление Security team
Неудачная аутентификация (>5 за 10 мин) HIGH Блокировка + расследование
Доступ к секретам вне рабочего времени MEDIUM Уведомление + логирование
Чтение production-секретов из non-protected branch CRITICAL Блокировка + немедленное расследование
Массовое чтение секретов (>20 путей за 1 мин) HIGH Ревокация токена + расследование
Отказ Vault audit backend CRITICAL Vault прекращает обслуживание запросов (by design)
Изменение Vault-политики MEDIUM Уведомление Security team
Новый сервисный аккаунт в Keycloak MEDIUM Верификация Security team

Экспорт событий GitLab в SIEM:

# GitLab CI job для экспорта аудит-событий
export_audit_events:
  stage: audit
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
  script:
    - |
      curl --header "PRIVATE-TOKEN: $GITLAB_AUDIT_TOKEN" \
        "https://gitlab.company.com/api/v4/audit_events?\
        entity_type=Group&entity_id=$CI_GROUP_ID&\
        created_after=$(date -u -d '1 hour ago' +%Y-%m-%dT%H:%M:%SZ)" \
        | jq '.[] | select(.details.action |
          test("deploy|secret|variable|member|permission"))' \
        >> /var/log/gitlab/ml_audit.json
    - filebeat -e -c /etc/filebeat/gitlab-audit.yml

Настройка экспорта событий Keycloak:

# keycloak.conf
spi-events-listener-jboss-logging-success-level=info
spi-events-listener-jboss-logging-error-level=warn

# Типы событий для мониторинга ML SecOps:
# LOGIN, LOGIN_ERROR          -- интерактивная аутентификация
# CLIENT_LOGIN                -- межсервисная аутентификация
# TOKEN_EXCHANGE              -- обмен токенов (delegation)
# PERMISSION_TOKEN            -- запрос permission ticket
# REGISTER, UPDATE_PROFILE    -- изменение учётных данных
# GRANT_CONSENT, REVOKE_GRANT -- управление согласиями

Песочница: detection-rule для аномального доступа в Vault

Vault audit log пишет каждое чтение секрета как JSON-строку. Detection-правило в SIEM ищет паттерны: чтение prod-секрета не-CI-аккаунтом, доступ из необычных источников, всплески запросов. Правьте regex и смотрите, какие тестовые строки правильно классифицируются.

{
  "id": "ch08-vault-anomaly",
  "title": "Detection-rule: чтение production-секрета интерактивным пользователем",
  "description": "Цель — поймать события, когда не-CI пользователь читает prod-секреты (потенциально компрометация). Не блокировать легитимные CI-jobs или admin-действия из maintenance-окна.",
  "defaultPattern": "(?=.*\"display_name\":\"(?!ci-)[^\"]+\").*\"path\":\"secret/data/(?:ml/production|datasets/restricted)/",
  "flags": "g",
  "tests": [
    { "id": "alert1", "shouldMatch": true, "label": "Подозрительно: интерактивный пользователь читает prod-секрет", "text": "{\"time\":\"2026-05-03T10:23:14Z\",\"type\":\"response\",\"auth\":{\"display_name\":\"alice@acme.com\"},\"request\":{\"path\":\"secret/data/ml/production/openai-api-key\",\"operation\":\"read\"}}" },
    { "id": "alert2", "shouldMatch": true, "label": "Подозрительно: чтение restricted dataset metadata", "text": "{\"time\":\"2026-05-03T11:05:22Z\",\"type\":\"response\",\"auth\":{\"display_name\":\"bob@acme.com\"},\"request\":{\"path\":\"secret/data/datasets/restricted/customer-pii-2026\",\"operation\":\"read\"}}" },
    { "id": "ok1", "shouldMatch": false, "label": "Легитимно: CI job читает prod (имя ci-*)", "text": "{\"time\":\"2026-05-03T10:00:00Z\",\"type\":\"response\",\"auth\":{\"display_name\":\"ci-deploy-12345\"},\"request\":{\"path\":\"secret/data/ml/production/db-creds\",\"operation\":\"read\"}}" },
    { "id": "ok2", "shouldMatch": false, "label": "Легитимно: alice читает свой staging-секрет", "text": "{\"time\":\"2026-05-03T09:15:00Z\",\"type\":\"response\",\"auth\":{\"display_name\":\"alice@acme.com\"},\"request\":{\"path\":\"secret/data/ml/staging/db-creds\",\"operation\":\"read\"}}" },
    { "id": "ok3", "shouldMatch": false, "label": "Легитимно: общедоступные данные", "text": "{\"time\":\"2026-05-03T08:00:00Z\",\"type\":\"response\",\"auth\":{\"display_name\":\"alice@acme.com\"},\"request\":{\"path\":\"secret/data/datasets/public/imagenet-mirror\",\"operation\":\"read\"}}" },
    { "id": "alert3", "shouldMatch": true, "label": "Подозрительно: компрометированный SA с не-ci именем", "text": "{\"time\":\"2026-05-03T03:45:00Z\",\"type\":\"response\",\"auth\":{\"display_name\":\"sa-old-test\"},\"request\":{\"path\":\"secret/data/ml/production/aws-creds\",\"operation\":\"read\"}}" }
  ]
}

Это упрощённый regex для иллюстрации. В production используется парсинг JSON в SIEM (Splunk SPL / Elastic ES|QL) с агрегацией по time window и rate-anomaly detection — single regex недостаточен.

3.9. Политики доступа как код (Policy-as-Code с OPA)

Приоритет: ВЫСОКИЙ

Все политики доступа хранятся в Git-репозитории и применяются автоматически через CI/CD. Ручное изменение политик на серверах запрещено.

Пример OPA-политики для ML-пайплайнов:

package ml_access_policy

import future.keywords.in

# Запрет: деплой в production без security approval
deny[msg] {
  input.resource.type == "model_deployment"
  input.resource.environment == "production"
  not input.metadata.security_approved == true
  msg := "Production deployment requires security team approval"
}

# Запрет: доступ к restricted данным без обоснования
deny[msg] {
  input.resource.type == "dataset"
  input.resource.classification == "restricted"
  not input.request.justification
  msg := "Access to restricted data requires written justification"
}

# Предупреждение: TTL секрета превышает 24h
warn[msg] {
  input.resource.type == "vault_secret"
  input.resource.ttl > 86400
  msg := sprintf("Secret TTL %v seconds exceeds 24h limit", [input.resource.ttl])
}

# Запрет: сервисный аккаунт с правами на запись в production
deny[msg] {
  input.resource.type == "service_account"
  input.resource.namespace == "ml-production"
  some verb in input.resource.verbs
  verb == "create"
  msg := "Service accounts in production must not have create permissions"
}

# Запрет: модель без подписи артефакта
deny[msg] {
  input.resource.type == "model_promotion"
  input.resource.target_stage == "Production"
  not input.metadata.artifact_hash
  msg := "Model artifact must be signed before promotion to production"
}

Интеграция OPA в GitLab CI:

opa_policy_check:
  stage: validate
  image: openpolicyagent/opa:latest
  script:
    - |
      VIOLATIONS=$(opa eval \
        -d policies/ \
        -i deployment_request.json \
        "data.ml_access_policy.deny" \
        --format raw)
      if [ "$VIOLATIONS" != "[]" ]; then
        echo "Policy violations detected:"
        echo "$VIOLATIONS" | jq '.'
        exit 1
      fi
    - |
      WARNINGS=$(opa eval \
        -d policies/ \
        -i deployment_request.json \
        "data.ml_access_policy.warn" \
        --format raw)
      if [ "$WARNINGS" != "[]" ]; then
        echo "Policy warnings:"
        echo "$WARNINGS" | jq '.'
      fi
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'
    - if: '$CI_MERGE_REQUEST_ID'

3.10. Offboarding-процедура

Приоритет: КРИТИЧЕСКИЙ

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

Offboarding ML-инженера: 10 обязательных шагов

{
  "id": "ch08-offboarding",
  "title": "Offboarding ML-engineer: чеклист последнего дня",
  "description": "Каждая запись — конкретный шаг с командой / API-вызовом, ответственным и SLA. Используйте как runbook при срочном offboarding (увольнение with cause, отстранение).",
  "shuffle": false,
  "cards": [
    {
      "tag": "T-0 минут · Identity",
      "front": "Disable IdP account",
      "subFront": "Keycloak / AD FS / Okta",
      "back": "Команда (Keycloak): `kcadm.sh update users/$USER_ID -s enabled=false -r mlsec`.\n\nЭффект: новые SSO-логины блокируются, но активные сессии живут до session-timeout.\n\nОтветственный: HR-system автоматический trigger; SLA — день увольнения, в час уведомления HR."
    },
    {
      "tag": "T-0 · Sessions",
      "front": "Revoke active sessions",
      "subFront": "Принудительное logout",
      "back": "Команда: `kcadm.sh delete users/$USER_ID/sessions -r mlsec` + revoke во всех downstream-сервисах через TokenExchange revocation.\n\nКритично: иначе active сессия в Jupyter / GitLab / Vault может прожить часами.\n\nSLA: <5 мин после disable account."
    },
    {
      "tag": "T-0 · Vault",
      "front": "Revoke Vault tokens & leases",
      "subFront": "Все creds выданные пользователю",
      "back": "Команда: `vault token revoke -mode=path \"auth/oidc/login/$USERNAME\"` для интерактивных + `vault lease revoke -prefix \"database/creds/ml-staging-role/$USERNAME\"` для динамических.\n\nЭффект: каскадный revoke всех дочерних tokens и leases.\n\nSLA: <5 мин."
    },
    {
      "tag": "T-0 · Cloud",
      "front": "Disable cloud IAM principals",
      "subFront": "AWS IAM / GCP IAM / Yandex IAM",
      "back": "Команды:\n• AWS: `aws iam delete-login-profile --user-name $USER` + `aws iam list-access-keys ... | aws iam delete-access-key`\n• GCP: `gcloud iam service-accounts disable $USER@$PROJECT.iam`\n\nКритично для ML: training-jobs могут использовать длинные access keys — все ротировать."
    },
    {
      "tag": "T-15 мин · Code",
      "front": "Remove from Git repos",
      "subFront": "GitLab / GitHub project-membership",
      "back": "API: `DELETE /projects/:id/members/:user_id` (GitLab) / `DELETE /repos/:org/:repo/collaborators/:user` (GitHub).\n\nСохранить commits — авторство сохраняется по email/SSH-ключу, не по членству.\n\nДополнительно: revoke Personal Access Tokens, SSH keys через API."
    },
    {
      "tag": "T-15 мин · K8s",
      "front": "Revoke Kubernetes RBAC",
      "subFront": "RoleBinding & ServiceAccount",
      "back": "Если пользователь имел RBAC через RoleBinding (subjects: User: alice@acme.com) — удалить subject из RoleBinding.\n\nЕсли использовал ServiceAccount-токен — `kubectl delete sa $SA -n $NAMESPACE` + проверить, что job’ы под этим SA уже отозваны.\n\nДля кросс-кластерной аутентификации (Keycloak + OIDC) — достаточно IdP disable."
    },
    {
      "tag": "T-30 мин · Data",
      "front": "Revoke data-plane access",
      "subFront": "MLflow / Snowflake / S3 buckets",
      "back": "Каждый data-tool требует своего API:\n• MLflow: `mlflow.tracking.MlflowClient().delete_user(...)` (если custom auth)\n• Snowflake: `DROP USER alice;`\n• S3: bucket-policy update + IAM principal revoke\n\nКоординация: chemistry между tools часто плохая; формализованный list-of-tools обязателен."
    },
    {
      "tag": "T-1 час · Devices",
      "front": "Wipe / quarantine corporate devices",
      "subFront": "MDM-команда",
      "back": "Через MDM (Jamf / Intune / Workspace ONE) — selective wipe или full wipe в зависимости от corporate policy.\n\nДля BYOD: revoke device certificate (mTLS), revoke MDM-enrollment.\n\nРиск: на устройстве могут оставаться cached secrets (Vault tokens в keychain, Kubernetes config). Wipe обязателен."
    },
    {
      "tag": "T-1 час · Audit",
      "front": "Trigger 30-day enhanced audit",
      "subFront": "SIEM rule: track ex-employee identity",
      "back": "В SIEM создать temporary correlation rule: alert на ЛЮБОЙ event с identity = ex-employee email/UID в течение 30 дней.\n\nЦель: словить cached сессии / забытые tokens / попытки атаки.\n\nSplunk / ELK / QRadar — все поддерживают временные правила.\n\nДополнительно: forensic-снимок home-directory на shared storage (legal hold если терминирован with cause)."
    },
    {
      "tag": "T-24 часа · Verification",
      "front": "Run offboarding verification",
      "subFront": "Automated check across all systems",
      "back": "Скрипт обходит все интегрированные tools и проверяет, что ex-employee действительно нигде не имеет доступа.\n\nКатегории checks:\n• IdP — account disabled / sessions revoked\n• Vault — нет активных tokens / leases\n• Cloud — нет API keys / console access\n• Git — не member ни в одном репо\n• K8s — нет subjects в RoleBindings\n• Data tools — нет user records\n\nFailure → ручное расследование + запись в audit log."
    }
  ]
}

Автоматизированный скрипт offboarding:

#!/bin/bash
# offboard_user.sh
# Использование: ./offboard_user.sh <username> <keycloak_user_id>

set -euo pipefail

USER=$1
KC_USER_ID=$2
TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)
ADMIN_USER=$(whoami)

echo "[$TIMESTAMP] Starting offboarding for user: $USER"

# 1. Отзыв всех Vault-токенов пользователя
echo "Revoking Vault tokens..."
for accessor in $(vault list -format=json auth/token/accessors | jq -r '.[]'); do
  TOKEN_META=$(vault token lookup -accessor "$accessor" -format=json 2>/dev/null || true)
  TOKEN_USER=$(echo "$TOKEN_META" | jq -r '.data.meta.username // empty')
  if [ "$TOKEN_USER" == "$USER" ]; then
    vault token revoke -accessor "$accessor"
    echo "  Revoked token accessor: $accessor"
  fi
done

# 2. Деактивация в Keycloak
echo "Disabling Keycloak account..."
curl -s -X PUT \
  "https://keycloak.company.com/admin/realms/ml-platform/users/$KC_USER_ID" \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"enabled": false}'

# 3. Закрытие всех сессий Keycloak
echo "Closing Keycloak sessions..."
curl -s -X POST \
  "https://keycloak.company.com/admin/realms/ml-platform/users/$KC_USER_ID/logout" \
  -H "Authorization: Bearer $ADMIN_TOKEN"

# 4. Ревокация GitLab Personal Access Tokens
echo "Revoking GitLab tokens..."
TOKENS=$(curl -s --header "PRIVATE-TOKEN: $GITLAB_ADMIN_TOKEN" \
  "https://gitlab.company.com/api/v4/personal_access_tokens?user_id=$GITLAB_USER_ID")
for TOKEN_ID in $(echo "$TOKENS" | jq -r '.[].id'); do
  curl -s --request DELETE \
    "https://gitlab.company.com/api/v4/personal_access_tokens/$TOKEN_ID" \
    --header "PRIVATE-TOKEN: $GITLAB_ADMIN_TOKEN"
  echo "  Revoked GitLab PAT: $TOKEN_ID"
done

# 5. Удаление SSH-ключей из GitLab
echo "Removing SSH keys..."
SSH_KEYS=$(curl -s --header "PRIVATE-TOKEN: $GITLAB_ADMIN_TOKEN" \
  "https://gitlab.company.com/api/v4/users/$GITLAB_USER_ID/keys")
for KEY_ID in $(echo "$SSH_KEYS" | jq -r '.[].id'); do
  curl -s --request DELETE \
    "https://gitlab.company.com/api/v4/users/$GITLAB_USER_ID/keys/$KEY_ID" \
    --header "PRIVATE-TOKEN: $GITLAB_ADMIN_TOKEN"
done

# 6. Удаление kubeconfig / RBAC bindings
echo "Removing Kubernetes access..."
kubectl delete rolebinding -l "user=$USER" --all-namespaces 2>/dev/null || true
kubectl delete clusterrolebinding -l "user=$USER" 2>/dev/null || true

# 7. Документирование в аудит-лог
echo "Recording audit entry..."
vault kv put "secret/audit/offboarding/$USER" \
  completed_at="$TIMESTAMP" \
  processed_by="$ADMIN_USER" \
  systems="vault,keycloak,gitlab,kubernetes"

echo "[$TIMESTAMP] Offboarding completed for $USER"
echo "$(date -u): Offboarding completed for $USER by $ADMIN_USER" \
  >> /var/log/ml-secops/offboarding.log

Контрольный список offboarding:

  • Все Vault-токены отозваны.
  • Keycloak-аккаунт деактивирован, сессии закрыты.
  • GitLab PAT отозваны, SSH-ключи удалены.
  • Kubernetes RBAC bindings удалены.
  • Доступ к VPN/SSH отозван.
  • Событие задокументировано в аудит-лог.
  • Верификация: попытка аутентификации с отозванными credentials неуспешна.

3.11. Separation of Duties в CI/CD

Приоритет: ВЫСОКИЙ

Ни один человек или сервисный аккаунт не должен иметь возможность единолично провести изменение от коммита до production-деплоя.

Реализация через GitLab CODEOWNERS:

# .gitlab/CODEOWNERS

# Данные --- только Data Engineers
/data/**                  @data-engineers-team

# Обучение --- ML Engineers + обязательный security review
/training/**              @ml-engineers-team @security-team

# Деплой --- только MLOps
/deployment/**            @mlops-team

# Политики безопасности --- только SecOps
/security-policies/**     @secops-team
/vault-policies/**        @secops-team

# Конфигурации инфраструктуры --- DevOps + SecOps
/infrastructure/**        @devops-team @secops-team

Multi-stage pipeline с обязательными gates:

stages:
  - validate        # Автоматическая валидация
  - train           # Обучение модели (ML Engineer)
  - security_gate   # Проверка безопасности (автоматическая)
  - staging         # Деплой в staging (MLOps)
  - approval        # Ручное одобрение (Security + PM)
  - deploy          # Деплой в production (MLOps)

security_gate:
  stage: security_gate
  script:
    - python run_security_tests.py --model-version $MODEL_VERSION
    - python check_adversarial_robustness.py
    - python check_data_leakage.py
    - python verify_model_signature.py
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'

production_approval:
  stage: approval
  script:
    - echo "Awaiting security team and PM approval"
  environment:
    name: production
    action: prepare
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'
      when: manual
  allow_failure: false

deploy_production:
  stage: deploy
  needs: ["security_gate", "production_approval"]
  script:
    # Верификация подписи артефакта модели
    - STORED_HASH=$(vault kv get -field=hash
        secret/ml/models/v${MODEL_VERSION})
    - CURRENT_HASH=$(sha256sum model.pkl | awk '{print $1}')
    - |
      if [ "$STORED_HASH" != "$CURRENT_HASH" ]; then
        echo "Model artifact integrity check FAILED!"
        exit 1
      fi
    - ./deploy_model.sh $MODEL_VERSION production
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'
      when: manual

3.12. Рекомендации по Keycloak hardening

Приоритет: ВЫСОКИЙ

Keycloak как центральный IdP требует дополнительного усиления безопасности.

Конфигурация realm для ML-платформы:

Параметр Значение Обоснование
Access Token Lifespan 5 минут Минимизация окна компрометации
Refresh Token Lifespan 30 минут Баланс UX и безопасности
SSO Session Idle 30 минут Автоматический logout при бездействии
SSO Session Max 8 часов Максимальная длительность рабочей сессии
Brute Force Protection Включена 5 попыток, блокировка 15 минут
Required Actions CONFIGURE_TOTP Обязательная MFA для всех пользователей
Password Policy length(12), upperCase(1), digit(1), specialChars(1), notUsername Минимальные требования
Client Authentication Confidential Все clients - confidential, без public clients

Роли Keycloak для ML-платформы:

flowchart LR
    REALM["Realm: ml-platform"]
    subgraph CLIENTS[Clients]
        direction TB
        C1["ml-training-service<br/>training-data-reader<br/>experiment-writer"]
        C2["ml-serving-service<br/>model-reader<br/>prediction-writer"]
        C3["ml-monitoring<br/>metrics-reader<br/>audit-reader"]
        C4["vault-client<br/>vault-user → Vault OIDC"]
    end
    subgraph REALMROLES[Realm Roles]
        direction TB
        R1[data-scientist]
        R2[ml-engineer]
        R3[mlops-engineer]
        R4[secops]
    end
    REALM --> CLIENTS
    REALM --> REALMROLES
    R1 -.->|"training-data-reader, experiment-writer"| C1
    R2 -.->|"training-data-reader, model-reader"| C1
    R2 -.->|model-reader| C2
    R3 -.->|"model-reader, prediction-writer"| C2
    R4 -.->|"audit-reader, metrics-reader"| C3

Дополнительные меры hardening:

  • Отключить все неиспользуемые Identity Providers.
  • Включить HTTPS-only для всех endpoints.
  • Отключить realm master для пользователей (использовать только для администрирования Keycloak).
  • Настроить Content Security Policy headers.
  • Ограничить Redirect URIs для каждого client строго по списку.
  • Включить Audit Logging для всех event types.
  • Регулярно обновлять Keycloak (отслеживать CVE).

Доступ по подписке#08-access-secrets-management

Дальше — практика и артефакты

Полная версия главы «Рекомендации ИБ: Управление доступом и секретами в MLSec» с готовыми артефактами, шаблонами, чек-листами и подробными процедурами доступна по подписке.

62% прочитано38% в подписке
Внутри:Готовые playbook'иШаблоны документовЧек-листыCI/CD конфиги
9код-блоков13таблиц65чек-пунктов3интерактивов
  • Полный доступ ко всем главам и обновлениям 30 дней
  • Готовые playbook'и, шаблоны и runbook'и под копирование
  • Поддержка автора — paywall не для заработка, а для развития справочника

Можно отменить в любой момент. Возврат средств в первые 7 дней без вопросов.