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».
Практичный алгоритм:
- Пытаемся найти ключ по
kidв локальном кеше JWKS. - Если ключ найден — проверяем подпись.
- Если ключ не найден — один раз принудительно обновляем JWKS (с защитой от stampede), пробуем снова.
- Если после обновления ключа нет — отклоняем токен.
Ошибка №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, метрикам и логике антишторма — иначе ротация ключей превращается в лотерею.

Key rotation без простоев: что предусмотреть заранее
Key rotation неизбежна: ключи утекают, криптополитики меняются, комплаенс требует регулярной смены. Ошибка — думать, что ротация это «заменили ключ и всё». Для JWT важно окно совместимости.
Две фазы ротации (практический шаблон)
Самый устойчивый вариант — держать в JWKS минимум два ключа: активный для подписания и предыдущий для валидации.
- Подготовка: публикуете новый публичный ключ в JWKS (новый
kid), но продолжаете подписывать старым. - Переключение: начинаете подписывать новым ключом. Старый ключ всё ещё остаётся в JWKS, пока живут ранее выданные access token.
- Вывод: удаляете старый ключ из 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.
Уязвимость 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.

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
Смысл: сервис не «угадывает», а строго следует политике. Это сильно снижает риск того, что один микросервис будет «чуть более либеральным» и станет дырой во всей системе.
Итоги: что проверить сегодня, чтобы не ловить инцидент завтра
Если времени мало, начните с самого «взрывоопасного»:
- Жёстко запретить
alg=noneи включить allowlist алгоритмов. - Проверять
issиaudна каждом сервисе. - Нормализовать JWKS кеширование: TTL + принудительное обновление при неизвестном
kid+ антишторм. - Определить и внедрить единый допуск clock skew и параллельно привести в порядок синхронизацию времени на узлах.
- Описать процесс key rotation (двухфазный) и окно удержания старых ключей.
JWT хорошо работает в микросервисах, когда это именно «криптографически подписанный пропуск», а не «строка, которую где-то декодируют». Чем меньше валидация зависит от содержимого заголовка и чем больше — от вашей строгой политики, тем спокойнее живёт прод.


