Модуль 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, готовая база для фишинга.
Три дыры:
- Replay-атака — HMAC-подпись с каноническим JSON и
X-Timestamp(окно ±2 мин). Изменишь символ или отправишь через 10 минут — подпись не совпадёт. - «Кто может что» — JWT на Edge даёт «кто ты», но не «что тебе можно». RBAC в бэкенде:
guestне делает переводы. - Секреты везде —
.envв образе = бомба. Yandex Lockbox с IAMcondition: 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 в Prometheus | OOM 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 + condition | least-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-переводов. Требуется защитить эндпоинт списания денег от подделки и обеспечить безопасный доступ к БД.
Примерный план работы:
- Replay-защита (HMAC + канонический JSON) →
X-Timestamp± 2 мин,X-Signatureпроверка. - Keycloak + RBAC → ALB валидирует JWT, бэкенд проверяет
role/scopeдля действия. - Yandex Lockbox → секреты убраны из
.env, SA сcondition(только нужные ключи). - 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") }
Вопросы студенту:
- Чем опасно логирование полного
payload? - Что произойдёт, если злоумышленник перехватит этот запрос и отправит повторно?
- Почему хранение
DB_PASSWORDв.envнедопустимо в K8s/Serverless?
📐 Задание по визуализации (Mermaid)
Студенту необходимо нарисовать схему аутентификации, авторизации и защиты от Replay.
Шаблон для ориентира:
🧩 Проверь себя
🤖 Инструкция для 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.