При ротации криптографического ключа недостаточно просто заменить секрет в конфигурации. Данные, созданные раньше, всё ещё зависят от старого ключа и должны оставаться доступными. Новые значения создаются активным ключом, а старые читаются тем ключом, которым они были созданы.

  • Активный ключ всегда один. В конфигурации его ID положительный. Старые ключи, оставленные только для чтения, обозначаются отрицательными ID.
  • Знак определяет статус ключа, а не его идентификатор. Например, -1 означает: «ключ № 1 больше не активен, но всё ещё доступен для чтения». В памяти приложения и в метаданных вроде *_key_id используется положительный ID 1.
  • При ротации старый ключ не перезаписывается. Создаётся новый секрет с новым ID, а прежний активный ключ переводится в неактивное состояние.
  • Старый ключ можно удалить только тогда, когда он гарантированно больше не нужен.
  • Перезапуск приложения не является ротацией. Ключи меняются при компрометации, изменении требований безопасности или в соответствии с принятой политикой срока действия.

Допустим, сейчас используется ключ № 1:

1:<старый секрет>

После ротации конфигурация будет выглядеть так:

-1:<старый секрет>,2:<новый секрет>

Теперь ключ № 2 используется для всех новых операций, а ключ № 1 остаётся доступен для чтения ранее созданных данных.

В services/userhub/internal/config/keyring.go состояние keyring хранится без отдельного флага active у каждого ключа:

type Keyring struct {
ActiveID int16
Keys map[int16][KeyringKeySize]byte
}

ActiveID хранит ID текущего активного ключа, а Keys содержит все известные ключи — как активный, так и старые.

При разборе конфигурации знак ID используется только для определения статуса:

active := rawID > 0
if active && ring.ActiveID != 0 {
return Keyring{}, newError("contains multiple active keys")
}
normalizedID := rawID
if normalizedID < 0 {
normalizedID = -normalizedID
}
id := int16(normalizedID)
if _, duplicate := ring.Keys[id]; duplicate {
return Keyring{}, newError("contains a duplicate key ID")
}

После нормализации ключ сохраняется в Keys[id]. Если исходный ID был положительным, этот же ID записывается в ActiveID.

Благодаря этому внутри приложения нет двух разных идентификаторов 1 и -1: существует один ключ с ID 1, а его активность определяется отдельно.

Порядок ключей в конфигурации значения не имеет.