Положите рядом два ключа. Первый знают приложения: бот поддержки, ночной обработчик документов, скрипт аналитика, ноутбук разработчика с отладочным окружением. Второй знает только шлюз: он предъявляет его поставщику модели, когда пересылает запрос дальше. Выглядят они похоже, оба начинаются с какого-нибудь префикса и состоят из длинной случайной строки. Но живут по разным законам, и вся ротация начинается с понимания этой разницы.
Если ключ у вас один и вшит в каждое приложение, вопрос «какой доступ отзывать» не возникает: отзывать приходится единственный, а потом бегать по всем местам, где он был записан. Шлюз между приложениями и поставщиком этот узел распутывает. Внутренний ключ можно заменить, не трогая поставщика. Ключ поставщика можно заменить, не трогая ни одно приложение. А при компрометации можно отключить только то, что действительно утекло. Ниже разберём, как это устроено на практике, на примере прокси LiteLLM и его виртуальных ключей.
Два уровня ключей и круг тех, кто их знает
В схеме со шлюзом обычно живут три разных вида секретов, и их полезно не путать.
- Секрет поставщика. Ключ, который выдан в кабинете провайдера модели. Он лежит в конфигурации или переменных окружения шлюза. Знают его администратор шлюза и сам шлюз, больше никто.
- Виртуальный ключ. Ключ, который шлюз выдаёт своим клиентам. По документации LiteLLM, виртуальные ключи управляют доступом к моделям, а для их хранения нужен PostgreSQL (LiteLLM: Virtual Keys). Его знают приложение и те люди, которые его настраивали.
- Сервисный ключ проекта. Ключ, привязанный не к человеку, а к проекту. Он нужен для приложений, которые должны пережить уход любого сотрудника.
Теперь важный вопрос: кто знает каждый из них? Для секрета поставщика круг узкий, и его легко описать списком из двух-трёх имён. Для виртуального ключа круг шире: разработчик, который вставил его в конфиг, коллега, которому он переслал в мессенджере, CI-система, где ключ лежит в переменных, и, возможно, чей-то локальный файл с окружением. Поэтому у ключей разные сценарии замены. Секрет поставщика меняют редко и в спокойной обстановке. Виртуальные ключи могут меняться чаще, но каждая замена требует, чтобы клиент подхватил новое значение.
Отсюда и первое практическое правило: прежде чем что-то менять, выпишите для каждого ключа два списка. Первый — люди, которые видели значение. Второй — сервисы, которые его сейчас предъявляют. Эти списки редко совпадают. Человек мог увидеть ключ полгода назад и давно не пользоваться им, но значение у него остаётся. Сервис, наоборот, может использовать ключ каждую секунду, не имея ни одного «знающего» живого человека рядом. Первый список определяет, насколько опасна утечка. Второй определяет, что сломается при отзыве.
Плановая замена секрета поставщика: почему нужен период двух ключей
Предположим, никакой аварии нет. Политика требует менять ключ провайдера раз в квартал, или сменился сотрудник с доступом к кабинету, или вы просто наводите порядок. Здесь спешить некуда, и именно поэтому торопиться не стоит: у плановой замены есть удобное свойство, которое экстренная лишена. Можно на какое-то время держать оба секрета живыми.
Последовательность такая.
- Создать новый ключ в кабинете поставщика, не трогая старый. На этом шаге у вас два действующих секрета у провайдера. Приложения о происходящем не подозревают: они ходят в шлюз со своими виртуальными ключами.
- Добавить новый секрет в конфигурацию шлюза и применить её так, как принято в вашей установке: перезапуск контейнера, выкатка новой версии конфига или иной штатный способ. Пока вы не подтвердили, что шлюз с новым секретом работает, не удаляйте старый ни из кабинета, ни из резервного места хранения.
- Проверить новый секрет. Отправить через шлюз тестовый запрос к каждой модели, которая использует этого поставщика, и убедиться, что ответ пришёл. Если у вас несколько моделей или несколько развёртываний, проверка должна покрыть каждое, а не одно «репрезентативное».
- Понаблюдать за реальным трафиком. Ошибки авторизации у поставщика после замены видны сразу, как всплеск ответов с кодами отказа. Если их нет, а нормальные запросы идут, шлюз действительно работает с новым значением.
- Отозвать старый ключ в кабинете поставщика. Только теперь.
Обратите внимание, что приложения ни в одном шаге не участвовали. Это и есть главная выгода шлюза: клиентский круг знает виртуальные ключи, а ключ поставщика ему не принадлежит. Замена внешнего секрета превращается в операцию одного узла, а не в рассылку «обновите переменные окружения до пятницы».
Почему не удалить старый сразу
Соблазн понятен: зачем держать лишний рабочий ключ, если вы собираетесь его убрать? Ответ в том самом оставшемся доступе. Пока старый секрет не отозван, он остаётся пригодным для всех, кто его помнит. Но тот же период решает и другую задачу: он оставляет вам путь назад. Если новое значение окажется с опечаткой, привязано не к тому проекту у поставщика или у него другие лимиты на модель, шлюз можно вернуть на прежнее значение за минуту. Если старый уже удалён, откат превращается в срочную выдачу третьего ключа.
Так что двойной период — не перестраховка, а рабочий инструмент. Но у него есть цена: всё время, пока он длится, старый ключ продолжает быть уязвимым, а значит, его нужно сделать как можно короче. Часы или сутки с мониторингом обычно честнее, чем недели «на всякий случай».
Проверка нового и прекращение старого — разные события
Здесь легко совершить ошибку мысли. Когда новый секрет заработал, возникает ощущение, что ротация завершена. Это не так. Работает новый ключ, но старый всё ещё пригоден. Если кто-то из тех, кто видел его, сохранил копию, она продолжает открывать доступ и ваш счёт у провайдера.
Поэтому ротация состоит из двух отдельных событий, и между ними полезно поставить явную границу:
- Новый секрет подтверждён. Шлюз с ним получает ответы от всех нужных моделей.
- Старый секрет прекращён. Отозван у поставщика, и пробный запрос с ним возвращает отказ.
Второе событие тоже стоит проверить, а не предполагать. После отзыва сделайте один запрос напрямую к поставщику со старым значением, не через шлюз, и убедитесь, что он отклонён. Только такой отказ и есть доказательство того, что старый доступ закончился. Запись в журнале «ключ удалён» говорит о вашем действии, а отказ — о результате.
Полезна и привычка хранить для каждой ротации короткую запись из четырёх полей: когда создан новый, когда подтверждён, когда отозван старый, кто видел старый. Последнее поле особенно важно. Оно превращает абстрактное «где-то мог остаться» в конкретный список людей, которым можно написать и спросить, не лежит ли у них копия.
Когда ждать нельзя: ключ украден
Теперь изменим условия. Ключ не просто пора менять: он попал туда, куда не должен был. Закоммичен в публичный репозиторий, вставлен в скриншот в чате, найден в логах, выложенных подрядчику. Двойной период из предыдущего раздела здесь превращается из удобства в уязвимость: каждая минута, пока старый ключ жив, — это минута, когда им может воспользоваться кто угодно.
Логика меняется на противоположную. Если плановую замену обосновывает оставшийся доступ, то экстренный отзыв определяется другим моментом: временем, когда запросы со старым ключом перестают проходить. Именно этот момент вы сокращаете, а удобство отката отходит на второй план.
Посмотрим, как выглядит последовательность.
- Определить, какой именно ключ утёк. Виртуальный ключ приложения или секрет поставщика? От этого зависит всё остальное. Если утёк виртуальный ключ, достаточно закрыть его на шлюзе, а провайдера не трогать вовсе. Если утёк секрет поставщика, ситуация серьёзнее, потому что с ним можно обращаться к провайдеру в обход вашего шлюза: без ваших лимитов, учёта и ограничений.
- Отозвать скомпрометированный ключ сразу. Для виртуального ключа документация LiteLLM описывает операции создания, изменения, удаления и регенерации (LiteLLM: Virtual Keys). В экстренной ситуации важны удаление и регенерация: оба действия лишают прежнее значение силы. Регенерация удобна, когда приложение должно продолжить работу с тем же набором прав: вы получаете новое значение и обновляете клиент, а старое перестаёт быть действительным.
- Выдать замену и доставить её клиенту. Здесь неизбежна пауза: приложение, чей ключ отозван, не работает, пока не получит новый. Это осознанная цена. В экстренном режиме лучше недолгий простой одного клиента, чем открытая дверь для всех.
- Если утёк секрет поставщика, действовать в обратном порядке сравнительно с плановой заменой: сначала создать новый ключ у поставщика и поставить его на шлюз, затем немедленно отозвать старый у поставщика, не дожидаясь длительного наблюдения. Окно между двумя шагами должно быть таким, чтобы успеть применить конфигурацию и сделать пробный запрос, но не более.
- Просмотреть расход за период, когда ключ мог быть известен посторонним. Аномалии в кабинете поставщика покажут, пользовался ли им кто-то помимо вас.
Две последовательности рядом
| Шаг | Плановая замена секрета поставщика | Компрометация |
|---|---|---|
| 1 | Создать новый ключ у поставщика, старый оставить | Определить, какой ключ утёк: виртуальный или поставщика |
| 2 | Поставить новый секрет на шлюз | Отозвать утёкший ключ немедленно |
| 3 | Проверить каждую модель тестовым запросом | Выдать замену и обновить клиента или шлюз |
| 4 | Понаблюдать за реальным трафиком | Проверить, что старое значение отклоняется |
| 5 | Отозвать старый ключ, проверить отказ | Разобрать расход за период возможной утечки |
Различие в том, что в левом столбце старый и новый ключи какое-то время работают одновременно, а в правом такого периода нет по замыслу. В плановом режиме вы защищаете непрерывность работы, в экстренном — прекращаете чужой доступ.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации Россия, США, Франция и Великобритания. Оплата картой РФ и по СБП.
Регенерация виртуального ключа — не ротация секрета поставщика
Эти два действия легко спутать, поскольку оба называются «заменой ключа». Но они затрагивают разные уровни. Регенерация виртуального ключа меняет значение, которое клиент предъявляет шлюзу. Секрет у поставщика при этом остаётся прежним. Если ваша проблема в том, что ключ поставщика был виден посторонним, регенерация виртуальных ключей её не решит: шлюз по-прежнему будет предъявлять утёкшее значение.
Верно и обратное. Если вы сменили секрет у поставщика, но приложения продолжают ходить в шлюз со старым виртуальным ключом, который был скомпрометирован, вы закрыли не ту дверь. Поэтому первый вопрос при любом инциденте — не «как быстро заменить», а «какое значение видели те, кто не должен был его видеть». Ответ определяет, какой из двух уровней нужно менять.
Есть и ещё одно следствие: безостановочность замены — это цель, которую вы проверяете, а не свойство, которое получаете даром. Шлюз позволяет сделать замену секрета поставщика прозрачной для приложений, но только если вы действительно подтвердили работу нового значения до удаления старого. Для виртуальных ключей прозрачности нет вовсе: клиент должен получить новое значение, и между отзывом и обновлением всегда есть интервал, который вы либо сокращаете двойным периодом, либо принимаете как неизбежный.
Ключ проекта, который переживает автора
Есть третья категория, о которой часто забывают, пока она не сломается. Разработчик создал виртуальный ключ от своего имени, настроил с ним приложение и ушёл из компании. Аккаунт закрыли, и вместе с ним, в зависимости от настроек, может исчезнуть и ключ. Приложение, которое работало месяцами, внезапно получает отказ, и никто не может сразу понять почему.
Для таких случаев в LiteLLM описаны сервисные ключи. Согласно документации, такой ключ предназначен для проекта, а не для отдельного пользователя, сохраняется после удаления пользователя и использует командные ограничения (LiteLLM: Service Accounts). Практический смысл в том, что срок жизни ключа привязан к проекту, а не к трудовой биографии конкретного человека.
Для ротации это даёт два последствия.
Во-первых, исчезает целый класс неожиданных отказов. Когда уходит сотрудник, вы удаляете его пользователя, и работающие сервисы этого не замечают, потому что их ключи ему не принадлежали. При этом нужно чётко различать два события: удаление пользователя не отзывает значение ключа в том смысле, что человек, который его видел, продолжает его помнить. Если сотрудник уходит при сложных обстоятельствах и видел сервисный ключ, этот ключ нужно заменить отдельным действием. Список «люди, видевшие значение» из начала статьи именно для этого и нужен: уход автора не отменяет его знания.
Во-вторых, командные ограничения работают как граница ущерба. Сервисный ключ получает лимиты команды, к которой привязан проект, а значит, даже утёкший, он не открывает всё, что доступно шлюзу, а только то, что разрешено этой команде. Поэтому при разборе инцидента с сервисным ключом первое, что стоит посмотреть, — не только когда он утёк, но и какие ограничения на нём действовали. От этого зависит оценка возможного расхода.
Как это выглядит в практике замены. Допустим, у вас есть проект обработки обращений, и его сервис ходит в шлюз по сервисному ключу. Плановая смена идёт по схеме двойного периода: выдаёте второй ключ того же проекта, переключаете сервис, проверяете, потом удаляете первый. Здесь условия лучше, чем у личных ключей, потому что ключ не завязан на человека, и вам не нужно думать, на чьём аккаунте он висит. Но оговорка про знавших людей остаётся в силе: если эти люди ещё работают, спросите, где они хранили копии, до того как удалять старое значение.
Что происходит с запросами, которые уже в пути
Остаётся вопрос, который обычно задают в последнюю очередь: что будет с запросами, которые начались до отзыва? Языковые модели отвечают не мгновенно. Запрос на длинную генерацию может идти десятки секунд, а потоковый ответ тянется всё это время. Если в середине такого запроса вы отозвали ключ, судьба соединения зависит от того, где проверяется ключ.
Разумно рассуждать так. Проверка доступа обычно выполняется в момент приёма запроса. Всё, что уже принято и передано дальше, вероятнее всего, доработает, а новые запросы с отозванным ключом будут отклонены. Но «вероятнее всего» — не то слово, на которое стоит опираться при инциденте, и точное поведение вашей версии нужно проверить на тестовой установке: начать долгий потоковый запрос, отозвать ключ посреди него и посмотреть, оборвётся ли ответ.
Для плановой замены вопрос почти не важен: вы отзываете старое значение после того, как трафик ушёл на новое, и начатые запросы успевают завершиться. Для экстренного отзыва он значим в одном аспекте: момент, когда запросы со старым ключом перестают проходить, — это не момент нажатия кнопки, а момент, когда шлюз начал отклонять новые обращения. Разница измеряется секундами, но в журнале её видно, и именно по журналу вы подтверждаете, что доступ закрыт.
Полезно заранее решить, что делать с запросами, оборванными при отзыве. Клиент получит ошибку, и если он умеет повторять запросы, повтор уйдёт уже с тем ключом, который у него в конфигурации. Если это отозванное значение, повтор будет отклонён, и приложение не зациклится на бесконечных попытках только при условии, что отказ авторизации не считается временной ошибкой. Проверьте это в своих клиентах: повторять запрос после отказа по ключу бессмысленно, а повторять после ошибки сервера уместно. Приложение, которое путает эти два случая, во время ротации будет нагружать шлюз пачками обречённых запросов.
Наблюдение за старым доступом после замены
Отзыв закрывает вопрос, но не снимает необходимость посмотреть, не стучится ли кто-нибудь старым ключом. Такие обращения интересны по двум причинам: они выдают забытых клиентов и они выдают посторонних.
Различить их помогает простой признак. Забытый клиент — это ваш сервис, который вы не обновили: запросы идут с привычного адреса, в привычном ритме, с типичными моделями и размерами. Посторонний ведёт себя иначе: незнакомые адреса, нетипичные модели, всплеск обращений, не связанный с вашей нагрузкой. В первом случае нужно найти и обновить сервис. Во втором — убедиться, что отзыв сработал полностью, и пересмотреть, откуда ключ мог утечь.
На практике достаточно настроить две вещи.
- Счётчик отказов авторизации на шлюзе и у поставщика. Рост после отзыва ожидаем: кто-то продолжает использовать старое значение. Важно, чтобы рост был коротким и затухал по мере обновления клиентов.
- Пробный запрос со старым значением по расписанию в течение нескольких дней после отзыва. Он должен стабильно возвращать отказ. Если однажды он вдруг проходит, значит, конфигурация откатилась или старый ключ снова появился где-то в цепочке.
Наблюдение стоит держать в течение срока, который соответствует вашему циклу работы. Если у вас есть квартальная отчётность, которую запускает сервис раз в три месяца, он может проснуться только тогда и обнаружить, что его ключ больше недействителен. Поэтому список сервисов, использующих ключ, из начала статьи важнее любого срока наблюдения: он показывает, кого вы можете не увидеть в журналах неделю.
Где это всё запускать
Шлюз с хранилищем на PostgreSQL — это постоянно работающий сервис с базой, которая хранит виртуальные ключи. Ему нужен выделенный сервер, на котором вы управляете конфигурацией и переменными окружения, а не общая платформа, где секреты поставщика пришлось бы хранить в чужих настройках. Для такой задачи подходит VPS с root-доступом: вы сами решаете, как хранить секреты шлюза, кто имеет к ним доступ и как применять новую конфигурацию.
По ресурсам шлюз сам по себе нетребователен, поскольку основную работу делает поставщик модели, а шлюз пересылает запросы и ведёт учёт. На VPS MAATRIX в публичном предложении указаны NVMe-диски, процессоры AMD EPYC и root-доступ, а тарифы начинаются с Ba: 1 vCPU, 2 ГБ RAM и 40 ГБ NVMe. Для шлюза с PostgreSQL на одной машине имеет смысл смотреть на Uab с 2 vCPU, 4 ГБ и 80 ГБ или выше, но подходящий размер определяется замером вашей нагрузки: число одновременных потоковых ответов, объём журналов и размер базы. Ресурсы тарифа не равны обещанию ёмкости, а доступность внешних API с выбранной локации нужно проверять отдельно, до того как вы переведёте на шлюз рабочий трафик. Среди локаций в публичном предложении перечислены UK, US, RU и FR, а оплата возможна картой РФ и через СБП. Заказать сервер можно на странице аренды VPS MAATRIX.
Ротация ключей по сути та же задача, что и смена ключей в других сервисах: узкий круг знающих, период двойного доступа и проверенное прекращение старого. Если вы уже настраивали подобное для туннелей, схема вам знакома: похожий порядок разобран в материале про ротацию ключей WireGuard.
Момент, когда старое значение перестало работать
Вернёмся к двум ключам на столе. После ротации секрет поставщика у шлюза новый, а виртуальные ключи приложений либо остались прежними, потому что утечки не было, либо заменены там, где она была. Всё это можно описать одной проверяемой фразой: пробный запрос со старым значением возвращает отказ. До этого момента ротация не закончена, как бы аккуратно ни выглядели записи в журнале изменений.
Бесшовность здесь — не обещание, а результат, который вы подтверждаете по каждому шагу: новое значение работает, клиенты не просят старое, а старое проверено и отклонено. Если на каком-то шаге вы не можете это показать, значит, там осталось место, где ключ ещё жив. Именно это место и нужно найти раньше, чем найдёт кто-то другой.