Выберите продукт

JWT security: JWKS, key rotation, clock skew и защита от alg=none

JWT удобны в микросервисах, но ошибки валидации быстро превращают их в дыру. Разберём JWKS и kid, ротацию ключей без даунтайма, учёт clock skew, проверки issuer/audience и защиту от alg=none — с практичными алгоритмами и runbook для продакшена.
JWT security: JWKS, key rotation, clock skew и защита от alg=none

JWT часто выбирают как «простую» схему авторизации для API и auth-microservices: подписал access token, проверил подпись на каждом сервисе — и поехали. На практике именно здесь появляются самые дорогие инциденты: токены принимаются не теми сервисами, не в тот момент времени, с не тем ключом или вообще без подписи из‑за ошибки вида alg=none.

Ниже — практический чек-лист и разбор типовых граблей вокруг JWT: JWKS, key rotation, kid, clock skew, проверки iss/aud, а также защита от alg=none. Смотрю на задачу глазами админа/DevOps: как сделать так, чтобы оно работало стабильно в проде, диагностировалось и не ломалось на ротациях.

Что именно вы должны проверять в JWT (минимум для продакшена)

JWT — это контейнер с клеймами (claims) и подписью. Ошибка мышления: «подпись валидна ⇒ всё хорошо». Подпись — лишь часть картины.

Для access token обычно критичны:

  • Подпись (и строго ожидаемый алгоритм).
  • iss (issuer) — кто выпустил токен.
  • aud (audience) — для кого токен (какой API/ресурс).
  • exp — срок жизни.
  • nbf и/или iat — временные ограничения (важно при clock skew).
  • sub — идентификатор субъекта (пользователь/сервис).
  • scope/roles/permissions — авторизация (не путать с аутентификацией).

Для refresh token набор обычно другой: их не стоит валидировать «на каждом микросервисе». Refresh живёт в auth-сервисе и должен быть либо непрозрачным, либо JWT с отдельной аудиторией/ключами и усиленным контролем (ревокация, device binding, rotation, семейство токенов).

JWKS: как работает, и где обычно ошибаются

JWKS (JSON Web Key Set) — это набор публичных ключей (чаще RSA/ECDSA), который публикует ваш issuer (IdP/auth-сервис). Микросервисы по нему получают ключи для проверки подписи JWT.

Типичный JWT header при RS256/ES256 выглядит так:

{
  "alg": "RS256",
  "typ": "JWT",
  "kid": "2026-01-rotate-01"
}

А в JWKS для соответствующего kid будет публичный ключ с параметрами kty, n, e (RSA) или crv, x, y (EC). Ключевая идея: kid — это указатель, какой именно ключ из набора использовать.

Ошибка №1: доверять kid без ограничений

kid приходит от клиента внутри токена. Он не является доверенным параметром — это только подсказка, какой ключ взять из вашего набора. Поэтому:

  • выбирайте ключ только из заранее известного JWKS конкретного issuer;
  • не делайте «динамические» запросы за ключом по kid в произвольные места;
  • не позволяйте kid превращаться в путь к файлу/SQL/шаблон (это встречается в самописных верификаторах).

Ошибка №2: JWKS кешируют «навсегда» или не кешируют вообще

В продакшене JWKS почти всегда нужно кешировать: иначе на пике запросов вы превращаете IdP в узкое место, а свои сервисы — в генераторы DDoS на собственный auth.

Но кеш «навсегда» ломает ротацию ключей. Правильная стратегия: держать кеш с TTL и уметь обновлять его по событию «не нашли kid».

Практичный алгоритм:

  1. Пытаемся найти ключ по kid в локальном кеше JWKS.
  2. Если ключ найден — проверяем подпись.
  3. Если ключ не найден — один раз принудительно обновляем JWKS (с защитой от stampede), пробуем снова.
  4. Если после обновления ключа нет — отклоняем токен.

Ошибка №3: один JWKS для всех, без проверки iss

В микросервисной среде нередко несколько issuer’ов: прод/стейдж, разные тенанты, внешние провайдеры, сервисные токены. Если сервис «просто тянет JWKS» и не проверяет iss, есть шанс принять токен «не из того мира».

Нормальная модель:

  • сначала валидируете iss на уровне конфигурации сервиса (разрешённый список);
  • для каждого iss — свой JWKS cache;
  • далее подбираете ключ по kid в рамках JWKS конкретного issuer.

Если вы строите микросервисную авторизацию вокруг JWT, воспринимайте iss как «корень доверия», а JWKS — как «публичный каталог ключей» для этого корня. Смешивание issuer’ов — классическая причина неожиданных обходов.

Если JWT-валидация крутится на периметре (API gateway/ingress) или вам нужно единообразие политик между сервисами, удобнее держать это на отдельной инфраструктуре. Для таких задач часто выбирают VDS, чтобы жёстко контролировать конфигурацию, кеши JWKS и наблюдаемость.

Перед тем как раскатывать валидацию JWT на десятки сервисов, проверьте, что у вас есть где нормально жить кешу JWKS, метрикам и логике антишторма — иначе ротация ключей превращается в лотерею.

FastFox VDS
Облачный VDS-сервер в России
Аренда виртуальных серверов с моментальным развертыванием инфраструктуры от 195₽ / мес

Схема кеширования JWKS: поиск ключа по kid и принудительное обновление при промахе

Key rotation без простоев: что предусмотреть заранее

Key rotation неизбежна: ключи утекают, криптополитики меняются, комплаенс требует регулярной смены. Ошибка — думать, что ротация это «заменили ключ и всё». Для JWT важно окно совместимости.

Две фазы ротации (практический шаблон)

Самый устойчивый вариант — держать в JWKS минимум два ключа: активный для подписания и предыдущий для валидации.

  1. Подготовка: публикуете новый публичный ключ в JWKS (новый kid), но продолжаете подписывать старым.
  2. Переключение: начинаете подписывать новым ключом. Старый ключ всё ещё остаётся в JWKS, пока живут ранее выданные access token.
  3. Вывод: удаляете старый ключ из JWKS только после того, как гарантированно истекли все токены, подписанные старым ключом, плюс запас на clock skew и кеши.

Сколько держать старые ключи

Простая формула для минимального удержания старого ключа:

  • TTL access token (например, 10–15 минут);
  • плюс максимально допустимый clock skew (например, 1–2 минуты);
  • плюс время жизни кеша JWKS на сервисах (например, 5–10 минут);
  • плюс запас на задержки деплоя/катастрофы (по ситуации).

Если ваш кеш JWKS живет 1 час, а access token — 5 минут, то именно кеш будет диктовать, как долго старый ключ обязан оставаться доступным.

Как пережить «внезапный» kid

После переключения подписи на новый ключ микросервисы должны уметь обработать токен с неизвестным kid без лавины запросов к JWKS endpoint.

Антишторм (stampede protection) идеи:

  • локальный mutex/lock: только один поток обновляет JWKS, остальные ждут или отвечают 401;
  • ограничение частоты обновлений JWKS: например, не чаще раза в N секунд;
  • отрицательное кеширование: «этого kid нет» на короткое время, чтобы не дергать IdP на каждом запросе с мусорным токеном.

Если такие проверки вы делаете на шлюзе/ingress, храните кеш и логику обновления там же. А сами микросервисы держите максимально «тонкими» — так проще поддерживать единый стандарт безопасности.

Clock skew: почему токены «еще не действуют» или «уже просрочены»

Clock skew — расхождение времени между системами. JWT очень чувствителен к времени из-за exp, nbf, iat. В микросервисах время может расходиться на секунды/минуты из-за проблем с NTP/chrony, виртуализации, перегрузки, ручных правок часов.

Симптомы в логах

  • Валидация падает с «token is expired», хотя токен только что выдан.
  • Ошибка «token is not active yet» (проверка nbf).
  • В разных сервисах один и тот же токен то принимается, то нет.

Правильная стратегия: маленький допуск + синхронизация времени

Практика: задавайте небольшой допуск времени при проверке exp/nbf (например 30–120 секунд), но не превращайте это в «можно всё».

Что важно:

  • допуск должен быть одинаковым везде, иначе отладка превращается в ад;
  • допуск не заменяет синхронизацию времени: если часы «плывут» на минуты и часы — это инцидент инфраструктуры;
  • если вы используете iat как «не раньше чем», не делайте агрессивных проверок без допуска.

Clock skew почти всегда проявляется «волнами»: после рестарта, миграции VM, просадки NTP или при высокой нагрузке. Если JWT стал нестабилен внезапно — первым делом проверьте время на узлах и логи NTP/chrony.

Issuer и Audience: два параметра, которые чаще всего забывают

Проверка iss и aud защищает от принятия «чужих» токенов. Даже если подпись валидна, токен может быть выпущен для другого API.

Проверка iss

iss должен точно совпадать с ожидаемым значением (или входить в allowlist). Не делайте «частичное совпадение», «contains», «startsWith» — это прямая дорожка к обходам.

Проверка aud

Варианты:

  • если aud — строка, сравнивайте со строгим ожидаемым значением;
  • если aud — массив, проверяйте, что идентификатор вашего сервиса/ресурса присутствует.

Договоритесь о модели audience в организации: «audience = API name», «audience = resource server id» и т.п. Главное — стабильность и однозначность.

Если вы внедряете JWT для внешних клиентов, не забывайте про транспорт и доверие к домену: сертификат — это не «галочка», а базовая защита от MITM и подмены endpoint’ов. По смыслу здесь уместны SSL-сертификаты для продовых доменов API.

Перед выпуском токенов во внешний мир проверьте, что у клиентов нет возможности «случайно» уйти на тестовый домен или принять подменённый endpoint без TLS.

FastFox SSL
Надежные SSL-сертификаты
Мы предлагаем широкий спектр SSL-сертификатов от GlobalSign по самым низким ценам. Поможем с покупкой и установкой SSL бесплатно!

Уязвимость alg=none: почему она появляется и как закрыть навсегда

alg=none — классическая ошибка, когда верификатор принимает JWT с заголовком "alg":"none" и считает, что подпись не нужна. Исторически некоторые библиотеки допускали такое поведение, а иногда разработчики включали «для тестов», а потом забывали.

Как выглядит поддельный заголовок:

{
  "alg": "none",
  "typ": "JWT"
}

Правило №1: алгоритм задается конфигом сервиса, а не токеном

Сервис должен заранее знать, какие алгоритмы он принимает. Это должен быть короткий allowlist, например только RS256 или только ES256. Не делайте «принимаем всё, что умеет библиотека».

Правило №2: запретить fallback и автоопределение ключей

Нельзя делать логику вида: «если есть публичный ключ — проверяем асимметрично, иначе пробуем симметрично». Это приводит к путанице HS256/RS256 (когда публичный ключ ошибочно используют как HMAC-секрет) и к обходам политики.

Минимальный чек-лист защиты

  • Отклонять alg=none всегда.
  • Allowlist алгоритмов на стороне верификатора.
  • Жестко разделять токены по типам: access/refresh/service-to-service.
  • Логировать причину отклонения (но не сам токен целиком в прод-логах).

Если вы хостите свои микросервисы на VDS, держите правило простым: «проверка токена — часть baseline security», как SSH-доступ и firewall. В тему пригодится практический материал: чек-лист защиты VDS: SSH-доступ и firewall.

Шлюз микросервисов отклоняет JWT с заголовком alg=none по allowlist алгоритмов

Access token vs Refresh token: где хранить, кто проверяет, какие TTL

В микросервисах есть соблазн сделать всё JWT и везде. Практичный подход такой:

  • Access token короткий (минуты), проверяется на каждом сервисе локально по JWKS.
  • Refresh token живет дольше (дни/недели), используется только в auth/identity сервисе для выдачи нового access token.

Если refresh token украдут, атакующий получит «длинную» сессию. Поэтому refresh требует дополнительной защиты: rotation (каждое обновление выдает новый refresh и инвалидирует старый), привязка к устройству/сессии, обнаружение reuse, ограничение по рискам, серверное хранилище состояния.

Набор практических проверок для микросервисов (runbook)

1) Конфигурация валидатора

  • Ожидаемые iss (allowlist).
  • Ожидаемые aud для каждого сервиса.
  • Allowlist алгоритмов (например, только RS256).
  • Clock skew допуск (например, 60 секунд).

2) JWKS кеш

  • TTL кеша разумный (часто 5–15 минут), с принудительным обновлением при неизвестном kid.
  • Защита от stampede.
  • Наблюдаемость: метрики «обновлений JWKS», «ошибок загрузки», «промахов по kid».

3) Ротация ключей

  • Минимум два ключа в JWKS: текущий и предыдущий.
  • Документированное окно удержания старого ключа.
  • План отката: если новый ключ сломал клиентов, можно временно вернуться к подписи старым, пока старый ключ ещё опубликован.

4) Логи и диагностика

Чтобы не утонуть, логируйте структурировано на уровне причины:

  • ошибка подписи;
  • неверный iss;
  • неверный/отсутствующий aud;
  • истёкший exp;
  • ранний nbf;
  • неизвестный kid (и факт обновления JWKS).

Избегайте писать в прод-логи полный токен или PII из клеймов: лучше корреляционный идентификатор запроса и «обрезанный» sub (или хеш).

Небольшой пример: политика валидации «как должно быть»

Ниже не привязанная к конкретному языку «контрактная» схема. Её удобно держать в виде конфигурации и применять одинаково во всех сервисах:

issuer_allowlist:
  - "auth-prod"
expected_audience:
  service_api:
    - "orders-api"
    - "billing-api"
alg_allowlist:
  - "RS256"
clock_skew_seconds: 60
jwks_cache_ttl_seconds: 600
jwks_refresh_min_interval_seconds: 10

Смысл: сервис не «угадывает», а строго следует политике. Это сильно снижает риск того, что один микросервис будет «чуть более либеральным» и станет дырой во всей системе.

Итоги: что проверить сегодня, чтобы не ловить инцидент завтра

Если времени мало, начните с самого «взрывоопасного»:

  1. Жёстко запретить alg=none и включить allowlist алгоритмов.
  2. Проверять iss и aud на каждом сервисе.
  3. Нормализовать JWKS кеширование: TTL + принудительное обновление при неизвестном kid + антишторм.
  4. Определить и внедрить единый допуск clock skew и параллельно привести в порядок синхронизацию времени на узлах.
  5. Описать процесс key rotation (двухфазный) и окно удержания старых ключей.

JWT хорошо работает в микросервисах, когда это именно «криптографически подписанный пропуск», а не «строка, которую где-то декодируют». Чем меньше валидация зависит от содержимого заголовка и чем больше — от вашей строгой политики, тем спокойнее живёт прод.

Поделиться статьей

Вам будет интересно

systemd hardening: DynamicUser, ProtectSystem и практичный sandboxing для сервисов OpenAI Статья написана AI (GPT 5)

systemd hardening: DynamicUser, ProtectSystem и практичный sandboxing для сервисов

Пошагово усиливаем безопасность systemd-сервисов без контейнеров: включаем DynamicUser, ограничиваем файловую систему через Protec ...
Segfault в production на Linux: coredumpctl, gdb и debuginfo — разбор падений без паники OpenAI Статья написана AI (GPT 5)

Segfault в production на Linux: coredumpctl, gdb и debuginfo — разбор падений без паники

Segfault в проде — это не «рандом», а нехватка артефактов. Показываю пошагово: включить core dump в systemd, проверить core_patter ...
Git dubious ownership и safe.directory в Linux: как чинить в CI/CD и на VDS без дыр в безопасности OpenAI Статья написана AI (GPT 5)

Git dubious ownership и safe.directory в Linux: как чинить в CI/CD и на VDS без дыр в безопасности

Git dubious ownership появляется в CI/CD и на серверах деплоя после смены пользователя, запуска через sudo, Docker volume или shar ...