Как провернуть JWT-ключ¶
Пошаговая процедура ротации HMAC-ключа, которым Gateway подписывает
X-User-* headers, и JWT-ключа, которым user-сервис подписывает
access/refresh токены. Правила хранения ключей и формата подписи — в
../conventions/security.md; разделение
PRIMARY/PREVIOUS в конфиге — в
../conventions/configuration.md
§Key rotation.
Зачем ротировать¶
Плановая ротация — защита от медленного leak'а (например, бэкап конфиг-репо попал к третьей стороне, про это узнали позже; свежий ключ уже не тот, что утёк). Стандартная частота — раз в 90 дней. Экстренная ротация — немедленно при подозрении на компрометацию.
Архитектурное напоминание¶
- Gateway HMAC-подписывает заголовки
X-User-ID,X-User-Role,X-Request-Idи шлёт результат вX-Gateway-Signature. - Backend-сервисы проверяют HMAC на входе через middleware
GatewayAuth(см.../conventions/security.md§Аутентификация в сервисе). - User-сервис дополнительно подписывает сам JWT — этот ключ другой и живёт только там.
- Ключи в конфиге — парой:
AUTH_HMAC_PRIMARY— текущий, им подписываются новые запросы.AUTH_HMAC_PREVIOUS— предыдущий, принимается при verify до истечения TTL старых запросов/токенов.- Аналогично для user-сервиса:
JWT_SECRET_PRIMARY/JWT_SECRET_PREVIOUS.- Внутренний API токен:
INTERNAL_API_TOKEN_PRIMARY/INTERNAL_API_TOKEN_PREVIOUS.
Verify в сервисе проверяет подпись сначала primary, потом previous
(см. ../conventions/security.md
§Rotation секретов). Если primary совпал — ok. Если только previous —
тоже ok, но логируй WARN (полезно для отслеживания завершения
ротации).
Нормальная ротация (90-day cycle)¶
Шаг 1. Сгенерировать новый ключ¶
32 байта из crypto/rand — минимальная длина для HMAC-SHA256. Сохрани
вывод в secret manager (Vault / SOPS / managed secret) под новым
именем: auth-hmac-<YYYY-MM-DD>. Никогда не коммить ключ в git,
никогда не пересылать через мессенджер.
Шаг 2. Применить как PREVIOUS + новый PRIMARY во всех сервисах¶
Логика: сервисы должны уметь принять новый ключ раньше, чем Gateway им подпишет. Порядок раскатки обязательный:
- Backend-сервисы (user, review, media, notification, ...) — сначала.
- Gateway — последним.
Для каждого backend-сервиса:
# добавить новый ключ; старый уходит в PREVIOUS
kubectl set env deployment/<service> \
AUTH_HMAC_PRIMARY="<new-key>" \
AUTH_HMAC_PREVIOUS="<old-key>"
Подожди rollout (kubectl rollout status deployment/<service>) для
каждого сервиса.
Когда все backend'ы раскатились — обновляй Gateway:
Если перепутать порядок (Gateway первым) — в промежутке raw Gateway подписывает новым ключом, а backend'ы ещё не знают о нём; старые pod'ы вернут 401. Несколько минут 401-х, пока rollout не догонит.
Шаг 3. Ожидание¶
После полного rollout Gateway подожди 24 часа. Причины:
- Запросы, сформированные старыми клиентами с кэшем, должны успеть
протечь через verify по
PREVIOUS. - Любые отложенные cron'ы, батчи, worker'ы, которые формируют подпись сами, — должны увидеть новый ключ.
Шаг 4. Убрать PREVIOUS¶
Через 24 часа:
или, если сервисов много:
for svc in user review media notification gateway; do
kubectl set env deployment/$svc AUTH_HMAC_PREVIOUS=""
done
После этого только PRIMARY признаётся. Запуск мониторинга: alert на
любые 401 от auth-middleware в течение следующего часа. Если есть — в
цикле ротации что-то упущено, верни PREVIOUS обратно и разбирайся.
Экстренная ротация (подозрение на утечку)¶
Те же шаги, но без 24-часовой паузы. Плюс:
- Немедленно подменить ключи по §Шаг 1 — §Шаг 2.
- Revoke всех access-токенов через admin-endpoint user-сервиса.
Типичная реализация: endpoint
/internal/admin/tokens/invalidate-allставит в Redis ключjwt:revoked:since=<timestamp>, и middleware отказывает любому JWT сiat < timestamp. - UX-эффект: пользователи де-авторизуются массово, клиенты должны перелогиниться. Это ожидаемо в случае компрометации.
- После стабилизации — разбор логов за возможное окно компрометации (запросы к чувствительным endpoint'ам, необычные паттерны IP).
- Пропусти §Шаг 3 (24h wait):
PREVIOUSне нужен, потому что всё, что было подписано старым ключом, уже revoked. - Через несколько часов (чтобы in-flight запросы долетели) убери
PREVIOUS.
JWT-ключ user-сервиса¶
Отдельный ключ, отдельная ротация. Затрагивает только user-сервис.
- Сгенерируй новый ключ:
openssl rand -base64 32. kubectl set env deployment/user JWT_SECRET_PRIMARY="<new>" JWT_SECRET_PREVIOUS="<old>".- Ждать до окончания TTL refresh-токенов (по умолчанию ~30 дней). Access-токены короче (~15 минут), их держать не нужно.
- После 30 дней:
JWT_SECRET_PREVIOUS="".
Если ротация экстренная — дополнительно revoke'ни все refresh-токены через admin-endpoint, чтобы не ждать 30 дней.
Internal API token¶
/internal/* endpoint'ы защищены отдельным статическим токеном (см.
../conventions/security.md §Internal
endpoints). Ротация по тому же dual-key паттерну:
kubectl set env deployment/<service> \
INTERNAL_API_TOKEN_PRIMARY="<new>" \
INTERNAL_API_TOKEN_PREVIOUS="<old>"
Порядок раскатки: сначала потребители (сервисы, зовущие чужие
/internal/*), потом хосты (сервисы, обслуживающие эти endpoint'ы).
Почему в другую сторону, чем Gateway/backend? Потому что потребитель
присылает токен, хост проверяет. Сначала учи потребителя слать новый —
иначе хост его не увидит.
Частота — раз в 180 дней, если не было инцидента.
Валидация на стороне сервиса¶
Эталонная логика auth-middleware (см.
../conventions/security.md):
func GatewayAuth(cfg config.Auth) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
payload := buildPayload(r) // uid || role || request_id
sig, _ := hex.DecodeString(r.Header.Get("X-Gateway-Signature"))
if verifyHMAC(cfg.HMACPrimary, payload, sig) {
next.ServeHTTP(w, r)
return
}
if cfg.HMACPrevious != "" && verifyHMAC(cfg.HMACPrevious, payload, sig) {
slog.WarnContext(r.Context(), "auth: verified via PREVIOUS key")
next.ServeHTTP(w, r)
return
}
writeError(w, http.StatusUnauthorized, "unauthorized", "bad signature")
})
}
}
verifyHMAC использует hmac.Equal (constant-time). WARN-лог на
verify через PREVIOUS — важный: в нормальной работе после wait
таких записей быть не должно. Если они есть после §Шаг 4 — ротация
неполная.
Чеклист ротации¶
Подготовка:
- [ ] Новый ключ сгенерирован через openssl rand -base64 32 (32
байта, случайность из crypto-источника).
- [ ] Ключ сохранён в secret manager, не в git, не в мессенджере.
- [ ] Предыдущий ключ сохранён отдельно (на случай отката).
- [ ] Incident log заведён (даже при плановой ротации).
Раскатка:
- [ ] Для HMAC: сначала backend-сервисы, потом Gateway.
- [ ] Для internal API token: сначала потребители, потом хосты.
- [ ] PREVIOUS установлен во всех сервисах, не только в одном.
- [ ] kubectl rollout status зелёный для каждого Deployment.
Ожидание: - [ ] 24 часа для HMAC (плановая ротация). - [ ] 30 дней для JWT (длительность refresh TTL). - [ ] Monitoring активен: dashboards auth errors / 401 rate.
Завершение:
- [ ] PREVIOUS очищен во всех сервисах.
- [ ] Smoke-test: login → получить JWT → сделать запрос в backend →
200. Repeat для internal API: сервис A → /internal/... сервиса
B → 200.
- [ ] 401 rate не вырос после очистки PREVIOUS.
Пост-ротация: - [ ] Incident log закрыт с пометкой ok. - [ ] Следующая плановая ротация запланирована (через 90 дней — для HMAC; через 180 — для internal token). - [ ] Старый ключ удалён из secret manager через 30 дней после завершения.
Anti-patterns¶
- Ротация только Gateway без backend. Все запросы начнут валиться на 401, пока backend не подтянется. Всегда: backend → Gateway.
- Ротация без
PREVIOUS. Любой in-flight запрос с подписью от старого ключа упадёт на 401. Без 24h буфера не обойтись. - Хранение ключа в
docker-compose.ymlprod. Ключ в plain text в репо — это compromise на следующий же день. - Использование одного HMAC-ключа для Gateway и internal API. Разные доверительные границы; компрометация одного не должна ломать другое. Разные ключи, разные rotation-циклы.
- Reuse старого ключа в новом сервисе. Если поднимаете новый
сервис, не копируйте туда текущий
PRIMARY«как dev-placeholder». Dev-стенд использует dev-ключи с маркером_dev_, prod — свежие из secret manager. - Ротация вручную без incident log. Через месяц никто не вспомнит, кто, когда и почему менял ключи; при инциденте это усложняет расследование.
Связанные разделы¶
../conventions/security.md— HMAC, JWT, internal token, constant-time compare.../conventions/configuration.md—AUTH_HMAC_PRIMARY/PREVIOUSв env, структура Config, redact.../checklists/production-ready.md— проверка «rotation plan задокументирован» перед включением prod-трафика.