Ротация ключей: стратегии и реализация

Ротация ключей в системах, использующих JOSE (JSON Object Signing and Encryption), представляет собой процесс регулярной замены криптографических ключей, применяемых для подписи и проверки JWT (JSON Web Token). Основная цель этой практики — снижение риска компрометации ключей и ограничение времени их потенциального злоупотребления.

В экосистеме JavaScript библиотека jose реализует полный стек JOSE-операций: подпись, проверку, шифрование и работу с JWK/JWKS. При построении системы аутентификации на основе JWT ключевая проблема заключается не в самой подписи токенов, а в управлении жизненным циклом ключей.

В основе лежит разделение ролей ключей:

  • Signing key (ключ подписи) — используется для создания JWT
  • Verification key (ключ проверки) — используется для проверки подписи
  • JWK (JSON Web Key) — представление ключа в JSON-формате
  • JWKS (JSON Web Key Set) — набор ключей, публикуемый через endpoint

Каждый ключ в JWKS имеет идентификатор kid (Key ID), который позволяет однозначно сопоставить токен и ключ проверки.

{
  "keys": [
    {
      "kty": "RSA",
      "kid": "key-2026-01",
      "use": "sig",
      "alg": "RS256",
      "n": "...",
      "e": "AQAB"
    }
  ]
}

Именно kid становится фундаментом ротации: система должна уметь одновременно работать с несколькими активными ключами.

Причины необходимости ротации

Ротация ключей решает несколько критических задач:

  • ограничение времени действия компрометированного ключа
  • снижение ущерба при утечке секретного материала
  • соответствие требованиям безопасности (PCI DSS, ISO 27001)
  • возможность обновления криптографических алгоритмов
  • изоляция ключей между версиями системы

При отсутствии ротации ключ превращается в постоянную точку отказа всей системы аутентификации.

Базовая стратегия: перекрывающаяся ротация

Наиболее распространённая модель — overlapping rotation (перекрывающаяся ротация).

Её принцип:

  1. Генерируется новый ключ
  2. Он добавляется в JWKS параллельно со старым
  3. Новый ключ начинает использоваться для подписи
  4. Старый ключ сохраняется до истечения всех старых токенов
  5. После периода «жизни токенов» старый ключ удаляется

Ключевой момент — система проверки должна поддерживать несколько ключей одновременно.

Реализация в jose: базовая схема

Подпись JWT с указанием kid:

import { SignJWT } from 'jose'

const secret = new TextEncoder().encode('old-or-current-secret')

const token = await new SignJWT({ sub: '123' })
  .setProtectedHeader({ alg: 'HS256', kid: 'key-2026-01' })
  .setIssuedAt()
  .setExpirationTime('2h')
  .sign(secret)

На практике для продакшн-систем чаще используется RSA или ECDSA:

import { generateKeyPair, SignJWT, exportJWK } from 'jose'

const { privateKey, publicKey } = await generateKeyPair('RS256')

const jwk = await exportJWK(publicKey)
jwk.kid = 'key-2026-02'
jwk.use = 'sig'
jwk.alg = 'RS256'

Структура JWKS при ротации

При наличии нескольких ключей JWKS выглядит как набор:

{
  "keys": [
    {
      "kid": "key-2025-12",
      "kty": "RSA",
      "use": "sig",
      "alg": "RS256"
    },
    {
      "kid": "key-2026-01",
      "kty": "RSA",
      "use": "sig",
      "alg": "RS256"
    }
  ]
}

Система должна гарантировать:

  • только один активный signing key
  • все предыдущие ключи доступны для верификации
  • неизменяемость уже выданных токенов

Стратегия по времени (time-based rotation)

Один из наиболее предсказуемых подходов — ротация по расписанию.

Характерные интервалы:

  • каждые 24 часа (высокая безопасность)
  • каждые 7–30 дней (баланс производительности)
  • каждые 90 дней (enterprise-уровень)

Пример логики:

function getCurrentKey() {
  const now = new Date()

  if (now.getUTCDate() % 2 === 0) {
    return 'key-even'
  }

  return 'key-odd'
}

В реальных системах вместо условных проверок используется хранилище конфигурации или KMS.

Версионная ротация ключей

Более контролируемая стратегия — versioned keys.

Каждый ключ получает:

  • version
  • createdAt
  • status (active / deprecated / revoked)

Пример структуры:

{
  "kid": "rsa-v3-2026",
  "version": 3,
  "status": "active"
}

При генерации токена всегда выбирается ключ с максимальной версией и статусом active.

Проверка токенов при ротации

При верификации JWT библиотека jose использует kid для выбора ключа:

import { jwtVerify } from 'jose'

async function verify(token, jwks) {
  return jwtVerify(token, async (header) => {
    const key = jwks.keys.find(k => k.kid === header.kid)

    if (!key) {
      throw new Error('Unknown key')
    }

    return key
  })
}

Такой подход позволяет:

  • не ломать старые токены
  • динамически обновлять ключи
  • использовать remote JWKS endpoint

JWKS endpoint и кеширование

В продакшн-системах ключи обычно публикуются через endpoint:

https://auth.example.com/.well-known/jwks.json

Клиенты:

  • кэшируют JWKS
  • обновляют его по TTL
  • используют fallback при ошибке

Проблема ротации здесь — синхронизация кешей. Если ключ удалён слишком рано, старые токены начинают падать.

Поэтому вводится правило:

ключ должен оставаться доступным дольше максимального времени жизни токена

Grace period (период совместимости)

Практика безопасной ротации включает grace period:

  • новый ключ уже используется для подписи
  • старый ключ остаётся в JWKS ещё N часов/дней
  • только после истечения периода он удаляется

Типичная формула:

grace_period ≥ max(token_lifetime)

Если токены живут 2 часа, ключи часто держат 24–72 часа.

Автоматическая ротация через KMS

В современных архитектурах ключи часто хранятся в KMS (Key Management System):

  • AWS KMS
  • Google Cloud KMS
  • Azure Key Vault

В этом случае jose используется только как криптографический слой, а управление ключами делегируется инфраструктуре.

Пример абстракции:

async function signJWT(payload) {
  const key = await kms.getActiveSigningKey()

  return new SignJWT(payload)
    .setProtectedHeader({ alg: 'RS256', kid: key.kid })
    .sign(key.privateKey)
}

Ошибки при ротации ключей

Типичные проблемы:

1. Удаление старого ключа слишком рано

Приводит к невозможности проверки активных токенов.

2. Отсутствие kid

Без идентификатора система вынуждена пробовать все ключи.

3. Несинхронизированный JWKS cache

Клиенты продолжают использовать устаревший набор ключей.

4. Несколько signing keys одновременно

Нарушает детерминированность системы.

Стратегия zero-downtime rotation

Чтобы ротация не влияла на работу системы, применяется последовательность:

  1. Генерация нового ключа
  2. Добавление в JWKS
  3. Переключение signing key
  4. Дождаться истечения TTL всех старых токенов
  5. Удаление старого ключа

Важное свойство — обратная совместимость всегда сохраняется.

Поддержка нескольких алгоритмов

В процессе ротации может происходить не только смена ключа, но и алгоритма:

  • RS256 → RS512
  • ECDSA P-256 → P-384

JWKS в этом случае содержит ключи с разными alg, а система должна учитывать это при верификации.

const key = jwks.keys.find(k =>
  k.kid === header.kid && k.alg === header.alg
)

Практическая архитектура ротации

Типичная схема включает:

  • Key Generator Service
  • JWKS Store
  • Signing Service
  • Verification Middleware
  • Cache Layer

Поток:

  1. Key Generator создаёт новый ключ
  2. JWKS обновляется
  3. Signing Service переключается
  4. Clients получают обновлённый JWKS
  5. Старые ключи выводятся из эксплуатации

Интеграция с jose

Библиотека jose обеспечивает все необходимые примитивы:

  • генерация ключей
  • экспорт/импорт JWK
  • подпись JWT
  • верификация через JWKS

Ротация реализуется не внутри библиотеки, а на уровне архитектуры, где jose выступает криптографическим движком, а не системой управления ключами.

Основная сложность всегда находится не в криптографии, а в координации жизненного цикла ключей между сервисами и клиентами.