Перейти к содержимому
5 мин чтения

Как провести ротацию ключей шифрования без простоя

Ротация ключей шифрования без простоя: как разделить домены, внедрить двойное чтение, перешифровать данные и проверить бэкапы.

Как провести ротацию ключей шифрования без простоя

Ротация ключей шифрования без простоя не начинается с кнопки Rotate в KMS. Она начинается с признания неприятного факта: у LLM-сервиса данные живут не в одной базе. Промпты и ответы попадают в рабочие таблицы, технические журналы, трассировки, очереди, векторные индексы, вложения, экспорты и резервные копии. Если один ключ защищает все это, вы не построили простую схему. Вы связали разные сроки хранения, разные права доступа и разные последствия утечки в один аварийный домен.

Рабочая схема проста по идее и требовательна в деталях: новый ключ обслуживает запись, старые версии продолжают обслуживать чтение, миграционный воркер постепенно переписывает существующие объекты, а старую версию отключают только после проверок. Самая дорогая ошибка здесь не криптографическая. Команды обычно теряют данные или ломают сервис потому, что не знают, где именно лежит шифртекст, кто его читает и как долго живет его копия.

Ротация и перешифрование решают разные задачи

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

Управляемые KMS часто делают первое почти незаметно для приложения. Документация AWS KMS прямо предупреждает: смена материала KMS-ключа не меняет data keys и не перешифровывает защищенные ими данные. Google Cloud KMS говорит то же самое в другой форме: новая версия становится активной, но старые данные не перешифровываются автоматически.

Это важно для LLM-инфраструктуры. Если вы защищаете записи через envelope encryption, объект обычно содержит шифртекст полезной нагрузки и зашифрованный DEK. Ротация KEK может защитить новые DEK новой версией, но старый DEK и старый шифртекст никуда не исчезают. При компрометации конкретного DEK календарная ротация KEK не исправляет ситуацию.

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

  • Какой ключ и какая версия защитили этот объект?
  • Может ли сервис прочитать объект, созданный до переключения записи?
  • Сколько объектов, включая копии, еще зависят от старой версии?

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

Один ключ на все хранилища делает инцидент шире

Ключи для журналов, рабочих данных, бэкапов и секретов должны быть разными криптографическими доменами. Это не прихоть аудитора и не попытка набрать больше объектов в KMS. У этих классов данных разный жизненный цикл.

Журналы часто нужны для расследований, но их читают инженеры поддержки и наблюдаемости. Рабочая база хранит состояние диалогов, учетные настройки, задания и результаты обработки. Резервные копии должны пережить сбой рабочей среды и поэтому живут в другой учетной записи, другом проекте или хотя бы под отдельной политикой. Секреты, такие как токены провайдеров, пароли баз и ключи подписи, нельзя считать обычными данными приложения вообще.

Практический минимум выглядит так:

ДоменЧто защищаетКто расшифровываетЧто меняется при ротации
logs-kekструктурированные логи, трассировки, архивы журналовсервис логирования и расследованияновые сегменты и архивы
app-data-kekзаписи приложений, файлы, результаты задачAPI и фоновые воркерыновые записи, затем миграция старых
backup-kekснимки баз, экспортные файлы, архивысервис восстановления в изолированной среденовые бэкапы, затем перепаковка старых
secrets-kekзашифрованные секреты конфигурациитолько сервис секретовотдельная процедура смены секрета

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

OWASP в Secrets Management Cheat Sheet отдельно советует автоматизировать ротацию, применять минимальные привилегии и вести метаданные о назначении, владельце и жизненном цикле секретов. Я бы добавил одно требование из практики: метаданные должны объяснять не только кто владеет ключом, но и где последний шифртекст под ним может появиться.

Инвентаризация должна считать копии, а не таблицы

Перед созданием нового ключа составьте карту шифртекстов. Список таблиц базы для этого бесполезен: одна запись может уйти в индекс, аналитический экспорт, dead-letter queue и ночную копию. Ротация ломается именно на таких вторичных маршрутах.

Для каждого класса данных зафиксируйте следующие поля в реестре:

  • владелец сервиса и владелец данных;
  • физическое место хранения и все пути репликации;
  • формат шифрования, алгоритм и идентификатор версии;
  • читатели, включая batch-задачи, CLI и процедуру восстановления;
  • максимальный срок, когда объект или его копия остается доступной.

Не путайте data residency с расположением первичной базы. Если промпты из Казахстана хранятся локально, а отладочный экспорт или резервная копия уходит в другую среду, требования к хранению уже не выполняются для всей цепочки. Для LLM-команд это особенно неприятно: в журналах могут остаться фрагменты промпта, ответа, идентификаторы пользователей и заголовки запроса, хотя основная таблица хранит только минимальный набор.

Проверьте также, какие данные вообще не должны доживать до ротации. Полный промпт в error log часто появляется потому, что разработчик хотел один раз увидеть сбой провайдера. После этого он годами лежит в архиве. Маскирование PII до логирования уменьшает объем перешифрования и делает утечку менее разрушительной, но не заменяет шифрование.

Полезный результат инвентаризации выглядит не как диаграмма для презентации, а как проверяемая строка: conversation_events -> PostgreSQL primary + CDC topic + nightly backup -> app-data-kek v4 -> API, summarizer, restore-job -> retention 30 days / 180 days backup. По этой строке видно, какие системы должны пережить двойное чтение и когда v4 еще нельзя трогать.

Формат шифртекста обязан хранить версию

Двойное чтение работает надежно, когда версия указана в самом объекте или в неразрывно связанной записи. Выбор ключа по дате создания, имени файла или глобальной переменной почти всегда разваливается при повторном запуске миграции, восстановлении старого бэкапа или доставке сообщения с задержкой.

Для прикладного шифрования метаданные могут выглядеть так:

{
  "cipher": "AES-256-GCM",
  "key_domain": "app-data",
  "key_version": "2026-07",
  "wrapped_dek": "base64url(...) ",
  "nonce": "base64url(...) ",
  "aad": {
    "tenant_id": "t_4821",
    "record_type": "conversation_event",
    "record_id": "ev_01J..."
  },
  "ciphertext": "base64url(...)"
}

key_version выбирает версию ключа, wrapped_dek позволяет расшифровать DEK, а AAD связывает шифртекст с контекстом. Если атакующий скопирует шифртекст одного tenant в запись другого, расшифрование должно завершиться ошибкой аутентификации. Не кладите в AAD поля, которые приложение ожидает менять без перешифрования, например статус задачи или время последнего обновления.

В формате есть тонкость, которую часто пропускают. Версия ключа не должна быть секретом. Она нужна для маршрутизации расшифрования, журналирования и проверки миграции. Секретны сам ключ и права на операцию расшифрования.

Для небольших значений можно использовать прямое шифрование через KMS. Для журналов, файлов, крупных ответов модели и бэкапов обычно разумнее envelope encryption: сервис генерирует случайный DEK, шифрует им данные и хранит DEK в завернутом виде. Это уменьшает число дорогих обращений к KMS и позволяет мигрировать объект по частям. Но DEK нельзя писать в лог, кэшировать без срока жизни или передавать в очередь как обычное поле JSON.

Двойное чтение сохраняет сервис во время миграции

Учтите требования к данным
Метки контента, маскирование PII и аудит-логи помогают выполнить требования законодательства Казахстана.

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

Последовательность выглядит так:

  1. Создайте новую версию или новый ключ, но не переключайте запись.
  2. Разверните код, который читает старую и новую версии по key_version, а в метриках считает их отдельно.
  3. Переключите новых писателей на новую версию через конфигурацию, которую можно быстро откатить.
  4. Запустите воркер, который читает старые объекты, проверяет их целостность и записывает новую криптографическую оболочку.
  5. После подтверждения охвата запретите новые операции под старой версией, оставив только расшифрование на срок наблюдения.

Смысл второго шага не в запасной ветке try old key. Такой код скрывает ошибки формата и заставляет сервис перебором проверять ключи. Чтение должно быть детерминированным: объект v4 открывается v4, объект v5 открывается v5. Если версия неизвестна, API возвращает контролируемую ошибку, поднимает тревогу и не делает пять вызовов KMS на удачу.

Ниже упрощенный псевдокод. Он намеренно не содержит «если расшифрование упало, попробуй предыдущий ключ».

def decrypt_record(envelope):
    policy = key_registry.get(
        domain=envelope["key_domain"],
        version=envelope["key_version"]
    )
    if policy is None or policy.read_status != "enabled":
        raise UnsupportedCiphertextVersion(envelope["key_version"])

    dek = unwrap(policy.kek_ref, envelope["wrapped_dek"], envelope["aad"])
    return aes_gcm_decrypt(dek, envelope["nonce"], envelope["ciphertext"], envelope["aad"])


def encrypt_record(plaintext, context):
    policy = key_registry.current_writer(domain="app-data")
    return envelope_encrypt(policy.kek_ref, plaintext, context)

Код должен журналировать домен, версию и результат операции, но не полезную нагрузку, DEK, nonce или завернутый ключ. Метрика decrypt_success_total{domain,version} покажет живое чтение старой версии точнее, чем надежда на завершенный batch.

Миграционный воркер должен быть идемпотентным

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

Представьте запись conversation_event. Воркер читает ее с row_version=18 и key_version=2026-01, расшифровывает, шифрует тем же AAD под 2026-07 и обновляет запись только при совпадении row_version=18. Если пользовательский запрос изменил запись раньше, условное обновление не сработает. Воркер перечитает актуальную версию и повторит работу. Он не имеет права перезаписывать новое состояние старым снимком.

Проверяйте четыре условия перед записью:

  • объект все еще использует старую версию;
  • AAD соответствует текущему владельцу и типу объекта;
  • расшифрование прошло проверку аутентичности;
  • условное обновление подтвердило, что объект не изменился между чтением и записью.

Не выполняйте миграцию одним SQL-скриптом, который расшифровывает данные в клиенте и обновляет миллионы строк в одной транзакции. Такая операция заблокирует таблицы, забьет KMS и оставит вас с плохим вариантом отката. Работайте маленькими порциями, ограничивайте параллелизм и делайте паузы по ошибкам KMS, росту задержки API и насыщению базы.

Хороший воркер хранит checkpoint не как «последний id», а как устойчивое правило обхода. Последний id ломается, когда идентификаторы не упорядочены, записи восстанавливают из бэкапа или часть объектов создается со старой версией из задержанной очереди. Используйте запрос вида «все объекты домена X с key_version=old, упорядоченные по первичному ключу» и возвращайтесь к нему, пока счетчик не станет нулевым. Затем повторите проход после периода, который покрывает максимальную задержку очередей.

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

Маршрутизируйте через один API
Единый шлюз маршрутизирует запросы к 500+ моделям через совместимый API.

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

Разделите два действия. Первое: новые бэкапы после переключения должны использовать новый backup-kek. Второе: старые бэкапы нужно либо перепаковать, либо оставить до естественного истечения срока хранения вместе с доступной старой версией. Второй вариант часто дешевле и безопаснее, если политики позволяют хранить старый ключ только для расшифрования в изолированной процедуре восстановления.

Проверка восстановления должна быть частью ротации, а не упражнением раз в год. Возьмите один старый бэкап, поднимите его в отдельной среде без доступа к боевым endpoint, выполните восстановление и прочитайте несколько зашифрованных объектов. Затем повторите проверку с новым бэкапом. Зафиксируйте, какой домен ключа, версия, роль и процедура потребовались.

Управляемые сервисы ведут себя по-разному после ротации клиентского ключа. Документация Google Cloud KMS описывает три варианта: сервис может автоматически перезавернуть DEK, применять новую версию только к будущим данным или продолжать использовать исходную версию. Никогда не переносите поведение одного хранилища на другое по аналогии. Проверьте конкретный сервис и подтвердите его результат собственным восстановлением.

Секреты требуют отдельной процедуры, а не миграции данных

Не переписывайте интеграцию
Смените base_url и продолжайте использовать текущие SDK, код и промпты.

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

Поэтому у секретов две независимые операции. Сначала вы меняете сам секрет у поставщика или в целевой системе и разрешаете короткое перекрытие, если протокол это допускает. Затем обновляете хранилище секрета и раскатываете потребителей. Смена KMS-ключа, которым секрет хранится в покое, идет отдельно. Она защищает сохраненное значение, но не отзывает украденный токен.

Плохой план звучит так: «Повернем master key, значит токены безопасны». Нет. Если токен уже выдан, он продолжает работать до отзыва, истечения срока или смены на стороне поставщика. Для критичных интеграций проверяйте, поддерживает ли провайдер две действующие учетные данные, идентификаторы версий и наблюдение за использованием старой.

AI Router помогает держать доступ к моделям за единым совместимым API-эндпоинтом, но ключ доступа к самому шлюзу все равно является секретом с собственным жизненным циклом. Не включайте его смену в пакет перешифрования пользовательских данных без отдельного плана перекрытия и отката.

Уничтожение старого ключа требует доказательств

Старый ключ нельзя удалять по дате в календаре. Сначала закройте зависимость данными и наблюдением. В противном случае вы превратите ошибки чтения в позднюю аварию, которая проявится только при редком запросе или настоящем восстановлении.

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

  1. Инвентаризация показывает, что все известные хранилища либо мигрированы, либо сознательно остаются в режиме архивного чтения.
  2. Сканер метаданных не находит объектов со старой версией в рабочих хранилищах.
  3. Метрики чтения старой версии остаются нулевыми весь выбранный период, включая выполнение расписанных задач.
  4. Команда успешно восстановила старый и новый бэкап по документированной процедуре.

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

NIST SP 800-57 рассматривает управление ключами как часть проектирования всей криптографической системы, а не как выбор алгоритма. Это точная формулировка: AES-GCM не спасает команду, которая не может сказать, какие данные еще зависят от v3 и кто способен ее прочитать.

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

Часто задаваемые вопросы

Чем ротация ключа отличается от перешифрования данных?

Смена активной версии ключа меняет ключ для новых операций шифрования. Перешифрование меняет защиту уже существующих объектов: записей, файлов, бэкапов и секретов. Первое можно сделать быстро, второе требует отдельной очереди, контроля и доказательства завершения.

Можно ли ротировать ключи без остановки LLM API?

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

Нужен ли отдельный ключ для логов и базы данных?

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

Когда можно уничтожить старую версию ключа?

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

Зачем хранить идентификатор версии ключа рядом с данными?

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

Что делать, если ключ шифрования мог утечь?

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

Нужно ли тестировать восстановление после ротации?

Нет, один успех не доказывает ничего. Тест должен включать хотя бы один объект каждого класса, старую резервную копию, запись с PII и большой объект, который читает асинхронный воркер. Восстановление надо проверять в изолированной среде, а не на боевой базе.

Что означает key_version в зашифрованной записи?

Для локальной симметричной криптографии это обычно поле шифртекста. Для envelope encryption версия может относиться к ключу, которым зашифрован DEK, а не к самому объекту. Смешивать эти уровни опасно: отчет о миграции станет неверным.

Нужно ли одновременно менять API-ключи и ключи шифрования?

Ротация ключа доступа меняет способ получить секрет, а ротация шифрующего ключа меняет защиту данных. Они могут идти в одном инциденте, но у них разные зависимости, сроки и критерии готовности. Не объединяйте их в один неуправляемый запуск.

Достаточно ли включить автоматическую ротацию в KMS?

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