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

У меня есть несколько микро-сервисов, работающих в AWS, некоторые из которых взаимодействуют друг с другом, некоторые имеют внешних клиентов или являются клиентами внешних сервисов.

Для реализации моих сервисов мне нужен ряд секретов (пары ключей RSA для подписи / проверки токенов, симметричные ключи, ключи API и т. Д.). Я использую AWS SecretsManager для этого, и он работает нормально, но сейчас я нахожусь в процессе реализации надлежащей поддержки ротации клавиш, и у меня есть несколько мыслей.

  • Я использую AWS SecretsManager, периодически извлекаю секреты (~ 5 минут) и кеширую их локально.
  • Я использую функцию этапов версии AWS SecretsManager для ссылки на версии AWSCURRENT и AWSPREVIOUS по мере необходимости.

Допустим, службе A нужен ключ K для службы B:

  • Скажем, в начале, K имеет текущее значение K1 и предыдущее значение K0.
  • Служба A всегда будет использовать (и кэшировать локально) версию K AWSCURRENT для связи с B, поэтому в этом случае K1
  • Служба B будет хранить версии AWSCURRENT и AWSPREVIOUS в своем локальном кэше и принимать обе версии [K1, K0]
  • При вращении K я сначала проверяю, что секрет, используемый службой B, вращается, так что после истечения интервала обновления все экземпляры службы B принимают [K2, K1] вместо [K1, K0]. Пока интервал обновления не истек, все экземпляры A все еще используют K1.
  • Когда интервал обновления истек, а это означает, что все экземпляры B должны получить K2, я поворачиваю ключ для обслуживания, так что A будет использовать K1 или K2 до тех пор, пока не истечет интервал обновления, затем только K2.
  • Это завершает поворот ключа (но если K1 считается скомпрометированным, мы можем снова повернуть секрет B, чтобы вытолкнуть K1 и получить [K3, K2]).

Это лучший подход или есть другие, чтобы рассмотреть?

Затем в некоторых ситуациях у меня есть симметричный ключ J, который используется в той же службе, например, ключ для шифрования некоторого сеанса. Таким образом, в одном запросе к услуге C сеанс шифруется ключом J1, а затем должен быть расшифрован с помощью J1 на более позднем этапе. У меня есть несколько экземпляров службы C.

Проблема здесь заключается в том, что если один и тот же секрет используется как для шифрования, так и для дешифрования, то его вращение становится более грязным - если ключ повернут, чтобы иметь значение J2, и один экземпляр обновился, чтобы он зашифровывался с помощью J2, а другой экземпляр все еще не видит J2, расшифровка не удастся.

Я вижу несколько подходов здесь:

  1. Разделите на два секрета с отдельными схемами вращения и вращайте по одному, как описано выше. Это добавляет накладные расходы с точки зрения дополнительных секретов для обработки, с идентичными значениями (кроме того, что они вращаются с некоторым временем между ними)

  2. Пусть расшифровка заставит освежить секрет при неудаче:

    • Шифрование всегда использует AWSCURRENT (J1 или J2 в зависимости от того, обновлены)
    • Расшифровка попытается выполнить AWSCURRENT, затем AWSPREVIOUS, и если оба не удастся (поскольку шифрование другим экземпляром, используемым J2 и [J1, J0] сохранено), запросит обновление секрета вручную (теперь сохранено [J2, J1]), а затем попытается Снова и снова УДИВИТЕЛЬНЫЙ.
  3. Используйте три ключа в окне ключей и всегда шифруйте со средним, так как он всегда должен быть в окне всех остальных экземпляров (если он не был повернут несколько раз, быстрее, чем интервал обновления). Это добавляет сложности.

Какие еще есть варианты? Это похоже на такой стандартный вариант использования, но я все еще изо всех сил пытался найти лучший подход.

РЕДАКТИРОВАТЬ ------------------

Основываясь на ответе JoeB, алгоритм, который я до сих пор придумал, таков: предположим, что первоначально секрет имеет значение CURRENT K1 и значение PENDING null.

Нормальная операция

  • Все службы периодически (каждые T секунд) запрашивают SecretsManager для AWSCURRENT, AWSPENDING и пользовательский ярлык ROTATING и принять их все (если они существуют) -> Все сервисы принимают [AWSCURRENT= К1]
  • Все клиенты используют AWSCURRENT=K1

Поворот ключа

  1. Установите новое значение K2 для этапа ОЖИДАНИЯ
  2. подождите T секунд -> Все сервисы теперь принимают [AWSCURRENT= К1, AWSPENDING= К2]
  3. добавлять ROTATING к версии К1 + переезд AWSCURRENT до версии K2 + удалить AWSPENDING метка из К2 (похоже, нет атомного обмена метками). Пока не пройдет T секунд, некоторые клиенты будут использовать K2, а некоторые K1, но все сервисы принимают оба
  4. подождите T секунд -> Все сервисы все еще принимают [AWSCURRENT= К2, AWSPENDING=K1] и все клиенты используют AWSCURRENT= К2
  5. Удалить ROTATING этап из К1. Обратите внимание, что К1 все еще будет иметь AWSPREVIOUS этап.
  6. Через T секунд все службы будут принимать только [AWSCURRENT= K2], а K1 фактически мертв.

Это должно работать как для отдельных секретов, так и для симметричных секретов, используемых как для шифрования, так и для дешифрования.

К сожалению, я не знаю, как использовать для этого встроенный механизм поворота, поскольку он требует нескольких шагов с задержками между ними. Одна идея состоит в том, чтобы изобрести несколько пользовательских шагов и иметь setSecret step создайте событие cron CloudWatch, которое снова вызовет функцию через T секунд, вызывая ее с шагами swapPending а также removePending, Было бы замечательно, если бы SecretsManager мог поддерживать это автоматически, например, поддерживая, что функция возвращает значение, указывающее, что следующий шаг должен быть вызван через T секунд.

1 ответ

Решение

Что касается вопроса об учетных данных, вам не нужно сохранять текущие и предыдущие учетные данные в приложении, если служба B поддерживает два активных учетных данных. Для этого вы должны убедиться, что учетные данные не помечены как AWSCURRENT, пока они не будут готовы. Тогда приложение просто всегда выбирает и использует учетные данные AWSCURRENT. Для этого в ротационной лямбде вы бы предприняли следующие шаги:

  1. Сохраните новые учетные данные в диспетчере секретов с меткой этапа AWSPENDING (если вы проходите этап создания, секрет не помечается как AWSCURRENT). Также используйте токен идемпотентности, предоставленный лямбде, когда вы создаете секрет, чтобы не создавать дубликаты при повторной попытке.
  2. Возьмите секрет, хранящийся в диспетчере секретов, на этапе AWSPENDING и добавьте его в качестве учетных данных в службу B.
  3. Убедитесь, что вы можете войти в службу B с учетными данными AWSPENDING.
  4. Измените этап мандата AWSPENDING на AWSCURRENT.

Это те же самые шаги, которые выполняет менеджер секретов, когда он создает многопользовательскую лямбда-ротацию RDS. Обязательно используйте метку AWSPENDING, потому что менеджер секретов обрабатывает это специально. Если служба B не поддерживает два активных учетных данных или совместное использование данных несколькими пользователями, возможно, нет способа сделать это. Смотрите документацию по ротации секретов на этом.

Кроме того, механизм ротации Secrets Manager является асинхронным и повторяет попытки после сбоев (поэтому каждый лямбда-шаг должен быть идемпотентным). Существует первоначальный набор повторов (порядка 5), а затем несколько ежедневных повторов. Вы можете воспользоваться этим, провалив третий шаг (проверка секрета) через исключение, пока не будут выполнены условия распространения. Кроме того, вы можете увеличить время выполнения лямбды до 15 минут и поспать соответствующее время, ожидая завершения распространения. Однако у спящего метода есть недостаток, заключающийся в ненужной привязке ресурсов.

Имейте в виду, как только вы удалите стадию ожидания или переместите AWSCURRENT на стадию ожидания, двигатель вращения остановится. Если приложение B принимает текущие и ожидающие обработки (или текущие, ожидающие и предыдущие, если вы хотите быть в большей безопасности), четыре шага, описанные выше, будут работать, если вы добавите задержку, которую вы описали. Вы также можете посмотреть в образце лямбд-менеджеров AWS Secrets Manager примеры того, как этапы манипулируются для ротации базы данных.

Что касается вашего вопроса шифрования, лучший способ, который я видел, - это сохранить идентификатор ключа шифрования вместе с зашифрованными данными. Поэтому, когда вы шифруете данные D1 ключом J1, вы либо сохраняете, либо иным образом передаете в нисходящее приложение что-то вроде секретного ARN и версии (скажем, V) в приложение. Если служба A отправляет зашифрованные данные службе B в сообщении M(...), она будет работать следующим образом:

  1. Извлекает ключ J1 для этапа AWSCURRENT (идентифицируется ARN и версией V1).
  2. A шифрует данные D1 как E1, используя ключ J1, и отправляет их в сообщении M1(ANR, V1, E1) на B.
  3. Позже J1 поворачивается к J2, а J2 помечается как AWSCURRENT.
  4. Извлекает ключ J2 для этапа AWSCURRENT (идентифицируется ARN и V2).
  5. A шифрует данные D2 как E2, используя ключ J2, и отправляет их в сообщении M2(ANR, V2, E2) на B.
  6. B получает M1 и выбирает ключ (J1), указывая ARN, V1 и дешифрует E1, чтобы получить D1.
  7. B принимает M2 и выбирает ключ (J2), указывая ARN, V2 и дешифрует E2, чтобы получить D2.

Обратите внимание, что ключи могут кэшироваться как A, так и B. Если зашифрованные данные должны храниться в течение длительного времени, вам нужно будет убедиться, что ключ не будет удален до тех пор, пока либо зашифрованные данные не исчезнут, либо они будут повторно зашифрованы с помощью текущий ключ. Вы также можете использовать несколько секретов (вместо версий), передавая различные ARN.

Другой альтернативой является использование KMS для шифрования. Служба A отправляет зашифрованный ключ данных KMS вместо идентификатора ключа вместе с зашифрованной полезной нагрузкой. Зашифрованный ключ данных KMS может быть расшифрован B, вызвав KMS, а затем использовать ключ данных для расшифровки полезной нагрузки.

Другие вопросы по тегам