Модуль 7. Безопасность и IAM (Identity & Access Management)

Цель модуля: Научить студента проектировать безопасность распределённой системы сквозным образом: от Edge (API Gateway) до межсервисного взаимодействия. Освоить защиту от Replay-атак для идемпотентных API (с канонической нормализацией JSON), интеграцию с Keycloak (RBAC) и управление секретами в облаке (Yandex Lockbox с IAM-политиками).

Стадия роста проекта: Публичный API. Проект пережил интеграции, Outbox, Saga, Resilience и Observability. Теперь система работает 24/7, метрики прозрачны. Но пришло время открыть API внешним клиентам — и первое, что ломается — безопасность. Идемпотентность без HMAC — открытая дверь для Replay-атак. Секреты в .env — бомба замедленного действия. PII в логах — готовая база для фишинга.

Перед началом: Модули 1–6 (особенно Идемпотентность — HMAC для Idempotency-Key, Observability — PII masking) Инструменты: Keycloak, Yandex Lockbox, ALB, Terraform Трудоёмкость: ~6 ч (теория 1.5 ч · практика 4 ч · ADR 0.5 ч)

🔥 Инцидент: тот самый запрос, который не должен был пройти

Система прозрачна (Модуль 6): каждый запрос в Jaeger, метрики видны, логи связаны. Но в пятницу — алерт: «3 перевода на крупные суммы от одного пользователя с одинаковым Idempotency-Key». Трейсы выглядят легитимно. Но IP нового, и между запросами — 10 секунд паузы, будто кто-то скопировал успешный запрос и отправил заново.

Злоумышленник перехватил легитимный запрос (публичный WiFi), сохранил тело, заголовки и Idempotency-Key, повторил через 10 минут. Сервис обработал — ведь ключ из Модуля 2 ловит дубли только внутри окна. Idempotency-Key без подписи ничего не стоит.

Дальше — хуже. В Docker-образе лежит .env с паролем от БД и HMAC-ключом, случайно попавший в git. Если атакующий получил доступ к образу — у него есть всё, чтобы подписывать свои запросы. А в логах и OTel-трейсах — полные номера карт и email: утечка PII, готовая база для фишинга.

Три дыры:

  1. Replay-атака — HMAC-подпись с каноническим JSON и X-Timestamp (окно ±2 мин). Изменишь символ или отправишь через 10 минут — подпись не совпадёт.
  2. «Кто может что» — JWT на Edge даёт «кто ты», но не «что тебе можно». RBAC в бэкенде: guest не делает переводы.
  3. Секреты везде — .env в образе = бомба. Yandex Lockbox с IAM condition: SA получает доступ только к нужным ключам.

Идемпотентность без HMAC — открытая дверь. Секреты вне хранилища — бомба замедленного действия. PII в трейсах — готовая база для атаки.

📖 Теория: Раскрытые термины

Глоссарий модуля:

  • JWT (JSON Web Token) — подписанный токен с claims (user_id, role, scope); валидируется на Edge (ALB).
  • HMAC (Hash-based MAC) — подпись сообщения секретным ключом (HMAC-SHA256); проверка доказывает, что отправитель знает секрет и тело не изменено.
  • OIDC / OAuth2 — OAuth2 даёт авторизацию (доступ), OIDC поверх него — аутентификацию (кто пользователь); реализуется Keycloak.
  • RBAC (Role-Based Access Control) — доступ по ролям (bank_teller, admin), проверяется в бизнес-логике, а не только на Edge.
  • mTLS (mutual TLS) — обоюдная TLS-аутентификация сервисов по сертификатам; база межсервисного Zero Trust.
  • Canonical JSON — нормализованный JSON (сортировка ключей, без пробелов) перед HMAC; иначе сериализатор меняет порядок и хеш не совпадает.
  • Replay-attack — перехват и повтор легитимного запроса; защита: HMAC + X-Timestamp (окно ±2 мин) + nonce.
  • Least Privilege — принцип: SA получает минимально необходимые права (lockbox.payloadViewer только на нужные секреты, не на всё хранилище).

7.1. Внешняя аутентификация (Keycloak / OAuth2 / OIDC)

Роль API-бэкенда — валивать JWT-токены (подпись, срок действия), выданные внешним Identity Provider (Keycloak).

Yandex API Gateway (ALB) перехватывает токен и валидирует его на Edge, пробрасывая X-User-Id в заголовках.

Авторизация (RBAC): Бэкенд доверяет X-User-Id, но обязан проверять scope / realm_role из JWT payload для конкретных бизнес-действий (например, запретить перевод, если роль не bank_teller).

Go (middleware):

go
// JWT-валидация на Edge (ALB) + RBAC в бэкенде
func (h *Handler) middleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // ALB уже валидировал JWT и пробросил X-User-Id
        userID := r.Header.Get("X-User-Id")
        if userID == "" {
            http.Error(w, "Unauthorized", http.StatusUnauthorized)
            return
        }
        // RBAC: проверяем роль из JWT payload
        role := r.Header.Get("X-User-Role")
        if role != "bank_teller" && role != "admin" {
            http.Error(w, "Forbidden", http.StatusForbidden)
            return
        }
        // Передаём дальше с контекстом
        ctx := context.WithValue(r.Context(), "userID", userID)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

7.2. Защита от Replay-атак и Канонический JSON

Idempotency-Key не защищает от перехвата запроса злоумышленником. Решение: Клиент подписывает запрос HMAC-подписью и добавляет X-Timestamp.

Рассинхронизация часов: Сервер должен допускать отклонение (например, ±2 минуты). Запросы старше 5+2=7 минут отклоняются.

Канонический JSON: Перед хешированием JSON-тело должно быть нормализовано (сортировка ключей, удаление пробелов). Иначе HMAC будет невалиден при малейшем изменении порядка полей сериализатором.

Go (crypto/hmac + canonical JSON):

go
// HMAC-подпись с каноническим JSON
func (h *Handler) verifySignature(r *http.Request) error {
    timestamp := r.Header.Get("X-Timestamp")
    signature := r.Header.Get("X-Signature")
    idempotencyKey := r.Header.Get("Idempotency-Key")

    // (1) Проверяем timestamp (±2 мин)
    ts, err := strconv.ParseInt(timestamp, 10, 64)
    if err != nil || time.Now().Unix()-ts > 120 {
        return fmt.Errorf("timestamp expired or invalid")
    }

    // (2) Канонический JSON: сортировка ключей, без пробелов
    body, _ := io.ReadAll(r.Body)
    canonical := canonicalizeJSON(body) // json.Marshal с сортировкой ключей

    // (3) HMAC-SHA256 от канонического JSON + Idempotency-Key + X-Timestamp
    mac := hmac.New(sha256.New, h.secret)
    mac.Write(canonical)
    mac.Write([]byte(idempotencyKey))
    mac.Write([]byte(timestamp))
    expected := hex.EncodeToString(mac.Sum(nil))

    if !hmac.Equal([]byte(signature), []byte(expected)) {
        return fmt.Errorf("signature mismatch")
    }
    return nil
}

7.3. Межсервисное взаимодействие (mTLS)

Внутренние микросервисы не должны доверять друг другу «по умолчанию» (Zero Trust).

Синхронные вызовы защищаются Mutual TLS. Сертификаты выдаются через Certificate Authority (CA), CN проверяется на соответствие имени сервиса, ротация предусмотрена по расписанию.

7.4. Управление секретами (Yandex Lockbox + IAM)

Пароли от БД и ключи HMAC не должны лежать в .env.

Секреты хранятся в Yandex Lockbox.

IAM-политика (Security by Design): Service Account (SA) приложения должен иметь права lockbox.payloadViewer только на конкретные секреты (через condition в IAM), а не на всё хранилище.

Terraform:

hcl
# SA с минимальными правами на конкретные секреты
resource "yandex_iam_service_account" "app" {
  name = "app-sa"
}

resource "yandex_resourcemanager_folder_iam_member" "app-lockbox" {
  folder_id = var.folder_id
  role      = "lockbox.payloadViewer"
  member    = "serviceAccount:${yandex_iam_service_account.app.id}"

  # condition: доступ только к нужным secret IDs
  condition {
    title      = "specific-secrets"
    expression = "resource.id in ['secret-db-password', 'secret-hmac-key']"
  }
}

Инфраструктура: В K8s секреты подтягиваются через initContainer/sidecar, в Serverless — через нативную интеграцию Lockbox с контейнером.

7.5. Безопасность и Observability (PII Masking)

Связь с Модулем 6: логи и трейсы (атрибуты span в OpenTelemetry) не должны содержать PII (Personally Identifiable Information).

Логгер/Трейсер должен маскировать поля вроде password, card_number, cvv, оставляя только ***.

Go (slog wrapper):

go
// Маскирование PII в логах и трейсах
func (l *Logger) Info(msg string, attrs ...slog.Attr) {
    for i, attr := range attrs {
        if isPII(attr.Key) { // password, email, cvv, card_number
            attrs[i].Value = slog.StringValue("***")
        }
    }
    l.logger.Info(msg, attrs...)
}

func isPII(key string) bool {
    switch key {
    case "password", "email", "cvv", "card_number", "phone":
        return true
    }
    return false
}

7.6. Этот паттерн в архетипах

Security проявляется по-разному в четырёх архетипах проектов:

АрхетипКлючевая угрозаЗащитаПочему критично
Финтех (P2P)Replay-атака на платёжHMAC + X-Timestamp + канонический JSONДвойное списание = деньги потеряны
Git-хостингDirectory Traversal на *.git/*Whitelist regexp + filepath.Clean + filepath.RelРаскрытие /etc/passwd через ..%2f
МессенджерПерехват сообщенийmTLS между сервисами + шифрование на EdgeУтечка переписки
Лента / чтениеКардинальность метрик (user_id)Низкая кардинальность labels в PrometheusOOM Prometheus = потеря мониторинга

Общий принцип: защита на Edge (JWT) + RBAC в бэкенде + Lockbox для секретов + HMAC для идемпотентности. Различается только ключевая угроза и приоритет защиты.

7.7. Компромиссы (Trade-offs)

ПодходПлюсМинусКогда выбирать
JWT валидация на Edge (ALB)быстрый reject до попадания в логику; меньше нагрузки на бэкендALB не знает бизнес-ролей (bank_teller); нужна дублирующая проверка scope в бэкендепубличные API, пограничный слой
JWT валидация только в бэкендеполный контроль: проверка и token, и роли/claimsкаждый запрос доходит до бэкенда (нет early-reject)внутренние сервисы без ALB
Секреты в Lockbox + conditionleast-privilege; SA получает только нужные ключисложнее Terraform; при ошибке в condition доступ пропадаетproduction, K8s, Serverless
Секреты в .envпростотапри утечке образа — компрометация; ротация вручнуюантипаттерн для production
HMAC + канонический JSONполная защита от replay и tamperingзависимость от сериализации (порядок ключей); clock-sync ±2 минпубличные API с платежами
Только Idempotency-Key (без HMAC)простотане защищает от перехвата/повторавнутренние доверенные вызовы

Вывод для курса: Edge JWT (ALB) + backend RBAC + Lockbox + HMAC = сквозная защита. См. ADR-7.


🛠 Практические задания

Сценарий: Публичное API P2P-переводов. Требуется защитить эндпоинт списания денег от подделки и обеспечить безопасный доступ к БД.

Примерный план работы:

  1. Replay-защита (HMAC + канонический JSON) → X-Timestamp ± 2 мин, X-Signature проверка.
  2. Keycloak + RBAC → ALB валидирует JWT, бэкенд проверяет role/scope для действия.
  3. Yandex Lockbox → секреты убраны из .env, SA с condition (только нужные ключи).
  4. PII Masking → password/email/cvv → *** в логах и OTel span attributes.

Задача 1: Защита от Replay-атак (HMAC + Канонический JSON)

Реализовать Middleware, проверяющее X-Timestamp, X-Signature, Idempotency-Key.

Проверить, что X-Timestamp в пределах допустимой рассинхронизации.

Вычислить HMAC-SHA256 от канонического JSON тела запроса + Idempotency-Key + X-Timestamp.

Сравнить с X-Signature. Если не совпадает — 401 Unauthorized.

✅ Готово, когда:

  • X-Timestamp проверяется на ± 2 минуты от серверного времени
  • HMAC-SHA256 от канонического JSON тела + Idempotency-Key + X-Timestamp
  • Канонический JSON: ключи отсортированы, без лишних пробелов
  • Несовпадение подписи / просроченный timestamp → 401 Unauthorized

Задача 2: Keycloak и RBAC

Настроить Yandex ALB на валидацию JWT от Keycloak.

В бэкенде (Go) написать Middleware для извлечения user_id и проверки role из JWT payload для авторизации конкретного перевода.

✅ Готово, когда:

  • Yandex ALB настроен на валидацию JWT от Keycloak (некорректный токен → 401 на Edge)
  • Бэкенд извлекает user_id из JWT payload
  • Бэкенд проверяет role / scope для авторизации конкретного действия

Задача 3: Загрузка секретов из Yandex Lockbox с IAM-политикой

Убрать все пароли из .env.

Через Terraform создать SA и назначить ему роль lockbox.payloadViewer с condition (доступ только к ключам db_password и hmac_secret).

Написать скрипт инициализации, который делает запрос в Lockbox и прокидывает секреты в окружение процесса.

✅ Готово, когда:

  • Все пароли убраны из .env и берутся из Lockbox
  • Terraform: SA с ролью lockbox.payloadViewer + condition (доступ только к нужным secret IDs)
  • Init-скрипт запрашивает секреты и прокидывает их в окружение процесса
  • ADR-7 «Lockbox vs env / JWT Edge vs backend» написан

Задача 4: Маскирование PII в логах и трейсах

Go: Написать обертку над slog и OTel-атрибутами для маскирования полей перед сериализацией.

✅ Готово, когда:

  • PII-ключи (password, email, cvv) рекурсивно заменяются на ***
  • Маскирование применяется в логах (slog wrapper)
  • Маскирование применяется в OTel span attributes

⚠️ Антипаттерн для разбора

go
// Go (Антипаттерн)
func (h *Handler) Pay(w http.ResponseWriter, r *http.Request) {
    body, _ := io.ReadAll(r.Body)
    slog.Info("payment request", slog.String("payload", string(body))) // ❌ Утечка PII
    // ❌ Подпись не проверяется, JSON не нормализуется
    key := r.Header.Get("Idempotency-Key")
    if h.cache.Has(r.Context(), key) { /* ... */ }
    // ❌ Секрет лежит в .env
    dbPassword := os.Getenv("DB_PASSWORD")
}

Вопросы студенту:

  1. Чем опасно логирование полного payload?
  2. Что произойдёт, если злоумышленник перехватит этот запрос и отправит повторно?
  3. Почему хранение DB_PASSWORD в .env недопустимо в K8s/Serverless?

📐 Задание по визуализации (Mermaid)

Студенту необходимо нарисовать схему аутентификации, авторизации и защиты от Replay.

Шаблон для ориентира:

Аутентификация, RBAC и защита от Replay (HMAC)

🧩 Проверь себя

1. Зачем каноническая нормализация JSON перед вычислением HMAC?
2. Что реально защищает от Replay-атаки перехваченного запроса?
3. Где должны храниться секреты БД/HMAC?
4. Канонический JSON перед HMAC — зачем?
5. IAM-политика с condition для Lockbox — для чего?

🤖 Инструкция для LLM (Coding Plan Assistant) — Режим Модуля 7

Привет! Я начинаю Модуль 7 курса «Архитектура распределённых систем».

Дай короткий ответ (2–3 предложения): почему Idempotency-Key без HMAC и Timestamp не защищает от Replay-атак, и как каноническая нормализация JSON влияет на проверку подписи?

Объясни разницу между аутентификацией (JWT на Edge) и авторизацией (RBAC на бэкенде).

Покажи мне код Антипаттерна (логирование PII, отсутствие HMAC, .env для секретов) и попроси меня объяснить все риски.

Дай описание задания: реализовать защиту от Replay-атак (HMAC+Timestamp с нормализацией JSON), настроить Keycloak/RBAC, маскирование PII в логах и трейсах, загрузить секреты из Yandex Lockbox с IAM-политикой.

НЕ ПИШИ за меня реализацию Middleware и Lockbox интеграции. Дождись моего ответа с архитектурным планом. После этого дай фидбек по чек-листу ментора и укажи на слепые зоны (например, «ты не учёл рассинхронизацию времени», «JSON не нормализован», «IAM слишком широкий»).


📝 Чек-лист ментора

  • Replay Protection: X-Timestamp проверяется с учётом рассинхронизации (±2 мин). Запросы старше 7 минут отклоняются.

  • Канонический JSON: Подпись HMAC вычисляется от нормализованного JSON (сортировка ключей, без пробелов).

  • Keycloak & RBAC: JWT валидируется на Edge. Бэкенд проверяет scope/role для авторизации действия.

  • Secrets Management: Секреты получены из Yandex Lockbox при старте. IAM-политика ограничивает доступ SA только нужными секретами (через condition).

  • PII Masking: Маскирование применяется к логам и атрибутам span в OpenTelemetry.

  • mTLS: Рассмотрено для межсервисного взаимодействия (CA, ротация).

  • Serverless vs K8s: Обоснован способ доставки секретов в зависимости от среды выполнения.

  • Mermaid-диаграмма: Отражает разделение ответственности между ALB (JWT), бэкендом (HMAC/RBAC) и Lockbox.

  • ADR-7: Student сформулировал ADR «JWT на Edge vs валидация в бэкенде + Lockbox vs .env» (контекст, альтернативы, решение, последствия).

🔗 Мостик к Модулю 8 (IaC + Compute)

В конце ревью скажи студенту:

  • «Отлично, мы закрыли все архитектурные дыры: данные не теряются, система не падает, мы видим все логи, а хакеры не могут украсть деньги. Но как теперь это всё развернуть в продакшене? Как автоматизировать деплой и управление инфраструктурой? В Модуле 8 мы выйдем за пределы кода и спроектируем инфраструктуру: от одной ВМ до Kubernetes-кластера в Yandex Cloud, используя Terraform как Infrastructure as Code. Мы свяжем IaC с безопасностью: как создавать SA, выдавать минимальные права и управлять секретами через Lockbox на уровне инфраструктуры.»

Если ты прошёл курс «Git Hosting: Serverless MVP», ты уже видел этот паттерн на упрощённом стеке: JWT на API Gateway, Rate Limiter, HMAC для вебхуков. Паттерн тот же — инфраструктура разная. Здесь мы разбираем его на полном стеке, с глубиной и trade-offs.