Ключи для разных окружений: dev, staging, production

В системах, где используется jose, ключи подписания и шифрования JWT перестают быть статическим параметром и становятся частью инфраструктуры безопасности. Разделение окружений — development, staging, production — требует отдельного подхода к генерации, хранению и использованию криптографических ключей, поскольку компрометация в одном окружении не должна затрагивать остальные.

В библиотеке jose поддерживаются оба подхода: симметричные алгоритмы (например, HS256) и асимметричные (RS256, ES256, EdDSA). Выбор напрямую влияет на стратегию управления ключами в разных окружениях.

Симметричный подход (HS256) Используется единый секрет для подписи и проверки токенов. В контексте окружений он допустим только в development, где:

  • отсутствует распределённая архитектура
  • нет строгих требований к изоляции компонентов
  • допустима простота в ущерб безопасности

Пример использования:

import { SignJWT, jwtVerify } from 'jose'

const secret = new TextEncoder().encode('dev-secret-key')

const token = await new SignJWT({ role: 'admin' })
  .setProtectedHeader({ alg: 'HS256' })
  .setIssuedAt()
  .setExpirationTime('2h')
  .sign(secret)

const { payload } = await jwtVerify(token, secret)

В staging и production такой подход создаёт единый вектор компрометации: утечка секрета означает полный контроль над токенами.


Асимметричная модель как базовая стратегия

Для staging и production стандартом становится использование пары ключей:

  • private key — подписывает токены
  • public key — используется для верификации

Это позволяет безопасно распространять публичную часть в любые сервисы без риска подделки токенов.

Генерация ключевой пары:

import { generateKeyPair } from 'jose'

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

Экспорт в JWK формат для хранения:

import { exportJWK } from 'jose'

const privateJwk = await exportJWK(privateKey)
const publicJwk = await exportJWK(publicKey)

Dev-окружение: упрощённая модель ключей

Development допускает компромиссы ради скорости разработки:

  • один статический ключ или секрет
  • отсутствие ротации
  • локальное хранение в .env

Типичный вариант:

const devSecret = new TextEncoder().encode(process.env.DEV_JWT_SECRET)

Характерная особенность: ключ не меняется между сессиями, что позволяет стабильно воспроизводить поведение системы, но делает окружение непригодным для любых реальных данных.


Staging: приближение к production модели

Staging служит зоной проверки безопасности и поведения системы под нагрузкой. Здесь уже применяется асимметрия, но с упрощённой инфраструктурой:

  • RS256 или ES256
  • ограниченная ротация ключей
  • централизованное хранение (Vault, AWS Secrets Manager, переменные окружения CI/CD)

Импорт ключей:

import { importJWK, SignJWT } from 'jose'

const privateKey = await importJWK(JSON.parse(process.env.STAGING_PRIVATE_JWK))

const token = await new SignJWT({ scope: 'test' })
  .setProtectedHeader({ alg: 'RS256', kid: 'staging-key-1' })
  .setIssuedAt()
  .setExpirationTime('1h')
  .sign(privateKey)

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


Production: управление ключами как инфраструктурой

Production-среда требует строгой модели управления жизненным циклом ключей:

  • обязательное разделение private/public ключей
  • регулярная ротация
  • хранение private key только в защищённых системах
  • публикация public key через JWKS endpoint

JWKS как стандарт распространения публичных ключей

jose позволяет работать с JWK напрямую, что удобно для реализации JWKS:

import { exportJWK } from 'jose'

const publicJwk = await exportJWK(publicKey)

publicJwk.use = 'sig'
publicJwk.alg = 'RS256'
publicJwk.kid = 'prod-key-2026-01'

В production обычно формируется набор ключей:

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

Ротация ключей и поддержка нескольких активных версий

Production почти всегда работает с несколькими активными ключами одновременно. Это необходимо для плавной ротации без инвалидирования существующих токенов.

Модель:

  • новый ключ добавляется в JWKS
  • старый остаётся активным до истечения всех токенов
  • kid определяет, каким ключом подписан JWT

Подписание:

const token = await new SignJWT({ userId: 123 })
  .setProtectedHeader({ alg: 'RS256', kid: 'prod-key-2026-01' })
  .setIssuedAt()
  .setExpirationTime('15m')
  .sign(privateKey)

Проверка:

import { jwtVerify, createRemoteJWKSet } from 'jose'

const JWKS = createRemoteJWKSet(new URL('https://auth.example.com/.well-known/jwks.json'))

const { payload } = await jwtVerify(token, JWKS)

Изоляция ключей между окружениями

Ключевая ошибка архитектуры — использование одинаковых ключей для dev, staging и production. Это приводит к следующим рискам:

  • возможность подписывать production-токены из dev-среды
  • утечка тестовых данных в боевую систему
  • невозможность корректной ротации

Правильная модель:

  • отдельный namespace ключей на каждое окружение
  • различный kid префикс (dev-, staging-, prod-)
  • физически раздельные секрет-хранилища

Использование алгоритмов в зависимости от окружения

Типичная матрица выбора:

  • development: HS256 (простота)
  • staging: RS256 (проверка архитектуры)
  • production: RS256 или ES256 (оптимизация и безопасность)

Пример выбора алгоритма:

const alg = process.env.NODE_ENV === 'production' ? 'RS256' : 'HS256'

Однако более строгая модель исключает HS256 вне локальной среды полностью.


Ошибки проектирования ключей в jose

На практике часто встречаются следующие проблемы:

  • хранение private key в коде
  • отсутствие kid при ротации
  • использование одного ключа на все окружения
  • отсутствие JWKS и ручное распределение public key
  • долгоживущие JWT без стратегии отзыва

Каждая из этих ошибок приводит к снижению уровня доверия к системе аутентификации.


Хранение ключей в инфраструктуре

Production-интеграция обычно строится вокруг:

  • AWS KMS / Secrets Manager
  • HashiCorp Vault
  • Kubernetes Secrets (в ограниченных сценариях)

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


Привязка ключей к домену безопасности

Разделение окружений в jose — это не только техническая мера, но и граница доверия:

  • dev — экспериментальная зона
  • staging — зона валидации безопасности
  • production — зона строгой криптографической целостности

Каждое окружение формирует собственный контур ключей, несовместимый с другими по дизайну, а не по договорённости.