Ротация ключей: один пишет, остальные читают
Практическая схема ротации ключей: один активный ключ для новых данных, старые ключи для чтения и безопасный вывод ключей из эксплуатации.
При ротации криптографического ключа недостаточно просто заменить секрет в конфигурации. Данные, созданные раньше, всё ещё зависят от старого ключа и должны оставаться доступными. Новые значения создаются активным ключом, а старые читаются тем ключом, которым они были созданы.
- Активный ключ всегда один. В конфигурации его ID положительный. Старые ключи, оставленные только для чтения, обозначаются отрицательными ID.
- Знак определяет статус ключа, а не его идентификатор. Например,
-1означает: «ключ № 1 больше не активен, но всё ещё доступен для чтения». В памяти приложения и в метаданных вроде*_key_idиспользуется положительный ID1. - При ротации старый ключ не перезаписывается. Создаётся новый секрет с новым 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 > 0if active && ring.ActiveID != 0 { return Keyring{}, newError("contains multiple active keys")}
normalizedID := rawIDif 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, а его активность определяется отдельно.
Порядок ключей в конфигурации значения не имеет.