MAATRIX / Блог / Ключ API утёк к чужому боту: как найти по логам, кто его жжёт

Ключ API утёк к чужому боту: как найти по логам, кто его жжёт

MAATRIX

Приходит письмо от провайдера: лимит по API-ключу почти исчерпан, хотя вы его не трогали три дня. Или счёт за месяц вырос втрое без единого изменения в коде. Первая мысль — «где-то баг, шлём лишние запросы». Вторая, более неприятная — ключ мог утечь и его использует кто-то посторонний. Разница между версиями решается не гаданием, а логами: если вы заранее пишете, кто, когда и как дёргает ваш ключ, ответ находится за 15–20 минут. Если логов нет — картину придётся реконструировать по огрызкам, которые остались у провайдера, и это куда дольше и менее надёжно.

Как понять, что дело не в баге, а в чужом трафике

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

  • Расход не совпадает с деплоями. Если рост потребления начался не в момент релиза и не совпадает с ростом трафика на сайте или в приложении — это подозрительно. Баг в коде обычно коррелирует с конкретным коммитом.
  • Активность есть там, где её быть не должно. Ночью по вашему времени, когда все фоновые джобы спят, а счётчик запросов всё равно растёт.
  • Используются дорогие эндпоинты, которые вы почти не вызываете. Например, генерация изображений или самые длинные модели — если в вашем продукте это редкая функция, а в логах провайдера она внезапно доминирует, это чужой трафик.
  • География не сходится. У большинства сервисов есть дашборд с гео или хотя бы с ASN источника запроса — если ваш продакшн живёт в одном регионе, а всплеск пришёл из совершенно другого, это тревожный звонок.

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

Отдельно стоит проверить самое банальное: не тестировал ли кто-то из команды тот же ключ с ноутбука, не завёлся ли скрипт с ретраями без бэкоффа, который сам себя зацикливает (мы разбирали похожий случай — цикл в коде исчерпал лимит API за двадцать минут). Иногда «утечка» оказывается собственным багом, и это стоит исключить в первую очередь.

Что даёт дашборд провайдера — и где его недостаточно

У большинства платных API есть раздел usage/billing с разбивкой по дням, иногда по часам. Это первая точка проверки, потому что она не требует доступа к вашим серверам:

  • график расхода квоты по времени — здесь видно сам факт всплеска и его форму (плавный рост или резкий скачок);
  • разбивка по эндпоинтам, если провайдер её показывает;
  • иногда — IP или страна последнего запроса (не у всех сервисов, но у части платёжных и SMS-шлюзов это есть в консоли);
  • список активных ключей с датой последнего использования — полезно, если у вас их несколько и непонятно, какой именно утёк.

Проблема в том, что детализация там почти всегда усреднённая и с задержкой в несколько часов, а иногда и в сутки. Дашборд скажет «было потрачено на 40% больше токенов вчера между 14:00 и 18:00», но не покажет ни конкретных IP, ни User-Agent, ни тела запросов. Для настоящего расследования этого мало — нужны собственные логи на границе, через которую проходит трафик к API.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Почему разбирать нужно свои логи, а не только провайдера

Если вы дёргаете сторонний API напрямую из клиентских приложений — расследовать нечем, кроме дашборда провайдера, и это одна из причин, почему прямой доступ к платному ключу с клиента — плохая идея в принципе. Правильная схема — ключ живёт только на сервере, а клиенты и внутренние сервисы ходят через собственный reverse-proxy (мы разбирали эту схему подробно в статье про прокси к OpenAI). У такой схемы, кроме прочего, есть побочный эффект, который окупается именно в подобных ситуациях: весь трафик к API проходит через точку, которую вы полностью контролируете и логируете.

Если у вас уже есть такой proxy на nginx, добавьте в конфиг расширенный access log, который фиксирует не только факт запроса, но и то, что нужно для разбора инцидентов:

log_format api_audit '$time_iso8601 $remote_addr "$http_x_forwarded_for" '
                      '"$request_method $uri" status=$status '
                      'bytes_in=$request_length bytes_out=$body_bytes_sent '
                      'rt=$request_time ua="$http_user_agent" '
                      'key_tail=$sent_http_x_key_tail';

server {
    location /v1/ {
        access_log /var/log/nginx/api_audit.log api_audit;
        proxy_pass https://api.upstream.example/v1/;
    }
}

Поле key_tail — не сам ключ (его в логах быть не должно никогда), а последние 4 символа, которые помогают отличить один ключ от другого, если у вас их несколько. Если проксируете через APISIX — то же самое делается плагином http-logger с кастомным форматом, включающим route_id и consumer.

Если API дергается не через ваш прокси, а прямо из бэкенд-кода — логируйте на уровне приложения: время, инициировавший модуль, эндпоинт, размер payload, ответ провайдера. Даже простая строка в syslog вида api_call service=openai endpoint=/chat user_id=4821 tokens_est=1200 status=200 через полгода спасёт часы разбора.

Как отличить легитимные вызовы от чужих

Когда логи есть, разбор идёт по нескольким осям одновременно — ни одна сама по себе не даёт стопроцентной уверенности, но вместе они складываются в картину.

По IP-адресам. Ваш легитимный трафик к API идёт с конечного набора адресов — продакшн-сервер, staging, может быть, пара разработческих машин с известными IP. Всё, что не входит в этот набор, — подозрительно по умолчанию.

awk '{print $2}' /var/log/nginx/api_audit.log | sort | uniq -c | sort -rn | head -20

Эта команда покажет топ IP по числу запросов. Если в списке есть адрес, которого не должно быть в вашей инфраструктуре, — это первая зацепка. Дальше whois <ip> часто показывает, что адрес принадлежит облачному хостеру или дата-центру, а не обычному провайдеру домашнего интернета — это типично для скриптов, перебирающих утёкшие ключи автоматически.

По времени. Постройте распределение запросов по часам:

awk '{print substr($1,1,13)}' /var/log/nginx/api_audit.log | sort | uniq -c

Если ваш бизнес работает с 9 до 21 по своему часовому поясу, а в логе виден ровный поток запросов в 3–5 утра — это либо крон-задача (проверьте, есть ли она у вас), либо чужой трафик из другого часового пояса, который просто не в курсе, когда у вас рабочий день.

По объёму и стоимости запроса. Утечка ключа к дорогому API почти всегда выдаёт себя объёмом: чужой скрипт не станет делать один аккуратный запрос раз в минуту, он выжимает лимит максимально быстро, пока его не заблокировали. Смотрите на bytes_out и rt (request_time) — аномально большие ответы или запросы к самым тяжёлым эндпоинтам подряд, десятками в минуту, не похожи на обычного пользователя вашего приложения.

По паттерну запроса. Легитимный трафик через ваше приложение имеет структуру: определённые эндпоинты вызываются в определённой последовательности, с телами запросов характерного формата (видна ваша бизнес-логика — фиксированные системные промпты, шаблоны, предсказуемая длина). Чужой трафик обычно однообразнее по структуре, но хаотичнее по содержанию.

По User-Agent. Не надёжный сам по себе признак (подделывается в одну строку), но полезный как дополнительный сигнал: если весь ваш трафик идёт с одним-двумя известными User-Agent, а в логах внезапно появляется дефолтный python-requests/2.31 или curl/8.x — стоит присмотреться внимательнее.

Таблица ниже — рабочий чек-лист, по которому удобно быстро пройтись при разборе конкретного всплеска:

ПризнакПохоже на легитимный трафикПохоже на утечку
IP-адресаИзвестный, ограниченный набор (прод, staging)Новые адреса, часто из дата-центров/хостеров
Время сутокСовпадает с рабочими часами или расписанием джобовРовный поток вне вашего расписания
Объём за запросСтабильный, соответствует вашей бизнес-логикеМаксимизация дорогих операций
Скорость запросовОграничена вашим же кодом (rate-limit, очередь)Пачками, на пределе лимита провайдера
User-AgentОдин-два известных значенияДефолтные значения библиотек, разнобой
Структура тела запросаПредсказуемые шаблоны вашего продуктаОднообразные обёртки, случайный контент

Если совпало 3+ пункта по колонке «утечка» — считайте компрометацию подтверждённой и переходите к ротации, не дожидаясь стопроцентной уверенности. Ключ, который могли украсть, дешевле перевыпустить, чем разбираться дальше, пока он активен.

Зачем логировать использование ключа заранее, а не когда уже поздно

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

Минимальный набор, который стоит писать по каждому вызову к платному API с момента, когда ключ впервые попал в прод:

  • время запроса с точностью до секунды;
  • источник (внутренний IP или сервис-инициатор, если запрос идёт из вашей же инфраструктуры);
  • вызываемый эндпоинт и метод;
  • размер запроса и ответа (или оценка стоимости — токены, минуты, штуки — в зависимости от того, как тарифицирует провайдер);
  • код ответа и время выполнения;
  • идентификатор ключа (не сам ключ) — последние символы или алиас, если ключей несколько.

Держать это стоит не меньше 30–90 дней в горячем доступе — ровно на случай, если аномалию заметят не сразу, а через платёжный цикл. Если у вас уже есть централизованный сбор логов с нескольких серверов, лог API-прокси стоит завести туда же отдельным потоком — тогда разбор превращается в один запрос по фильтру вместо grep по файлу на конкретном сервере, который вы, возможно, уже не помните.

Отдельно стоит настроить алерт на резкий рост расхода квоты, а не полагаться на то, что вы случайно заметите письмо от провайдера. Простейший вариант — крон-скрипт, который раз в час дергает API биллинга провайдера (если он есть) и сравнивает расход с медианой за прошлые дни, отправляя уведомление при отклонении в 2-3 раза.

Ротация ключа после подтверждённой компрометации

Как только совпадение признаков убедило вас, что ключ утёк, откладывать ротацию не нужно. Порядок действий:

  1. Создайте новый ключ, не отзывая старый. Почти все провайдеры позволяют иметь несколько активных ключей одновременно — это даёт возможность переключиться без простоя.
  2. Разверните новый ключ на всех точках использования. Если у вас единая точка (свой reverse-proxy), это один деплой конфига. Если ключ раскидан по нескольким сервисам — это самая частая причина, почему ротация превращается в квест: список мест реального использования часто расходится с тем, что помнят в команде.
  3. Проверьте, что новый ключ работает, прежде чем отзывать старый — тестовым запросом с продакшна, а не локально.
  4. Отзовите старый ключ на стороне провайдера. Это финальный и необратимый шаг, поэтому не откладывайте его после успешной проверки нового.
  5. Зафиксируйте инцидент — время обнаружения, время ротации, объём аномального расхода. Если провайдер выставит счёт с претензией на возврат средств за чужой трафик, у вас будет с чем к нему идти.

Утечку API-ключа стоит рассматривать как частный случай более общей практики ротации секретов — если у вас уже есть регулярный процесс замены ключей по расписанию, инцидент проходит быстрее, потому что процедура обкатана. Механика ротации без даунтайма подробнее разобрана в статье про ротацию секретов и ключей на практике.

Если нет полной уверенности, воспользовался ли атакующий ключом за то время, что он был доступен, — не закрывайте инцидент одной ротацией, а проверьте следы использования; методика разбора именно этого вопроса — в статье «секрет утёк — как понять, пользовались им или нет».

Ограничение ключа по IP или referrer — превентивная мера, а не расследование

Часть провайдеров позволяет ограничить область действия ключа ещё до всякой утечки — и это стоит сделать сразу при выпуске нового ключа, а не только после инцидента:

  • Привязка к IP или диапазону IP. Если ключ используется только с вашего сервера (через собственный reverse-proxy, как описано выше), укажите в настройках ключа его статический IP. Тогда даже если ключ утечёт в публичный репозиторий, запрос с чужого адреса будет отклонён на стороне провайдера — до того, как он вообще попадёт в ваши логи.
  • Ограничение по HTTP referrer. Актуально для ключей, которые используются в браузере (карты, аналитика, виджеты) — провайдер проверяет заголовок Referer и отклоняет запросы не с вашего домена. Это слабее IP-ограничения (referrer подделывается), но лучше, чем ничего, для сценариев, где ключ технически виден на клиенте.
  • Ограничение по объёму или rate-limit на уровне самого ключа, если провайдер это поддерживает отдельно от общего лимита аккаунта — так один скомпрометированный ключ не сможет выжрать весь бюджет разом, даже если ограничение по IP почему-то не сработало.

Не все провайдеры одинаково гибки: у одних ограничение по IP есть в стандартной консоли, у других — только в enterprise-тарифе, у третьих нет вовсе. В последнем случае единственный рубеж — собственный прокси-сервер, который ходит к API с фиксированного адреса, а запросы принимает уже с более гибкой авторизацией на своей стороне. Это ещё один аргумент в пользу схемы с прокси даже там, где провайдер не даёт привязку по IP напрямую — вы получаете её де-факто, потому что единственный клиент API в глазах провайдера — IP вашего сервера.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Провайдер не даёт детальных логов по каждому запросу, только суммарный расход по дням — как быть?

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

Можно ли понять, воспользовался ли кто-то ключом, если он "просто" засветился в публичном репозитории на пару часов, а расход квоты вроде не вырос?

Отсутствие всплеска — хороший знак, но не доказательство: небольшой аккуратный чужой трафик может теряться на фоне вашего собственного. Стоит явно сверить список источников запросов (IP) за период, когда ключ был публичен, а не полагаться только на график суммарного расхода.

Нужно ли ротировать ключ, если аномалия оказалась вашим же багом (например, ретраи без бэкоффа)?

Формально не обязательно — раз ключ не утекал, ротация не закрывает никакой дыры. Но если вы не можете на 100% исключить, что ключ также попадал куда-то ещё (лог сборки, публичный форк, скриншот в тикете), безопаснее ротировать в любом случае — это дешевле повторного расследования.

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

Единого стандарта нет. Шансы выше, если вы обращаетесь быстро и приводите конкретные аномальные записи (IP, эндпоинты, объём) вместо общей фразы «у нас украли ключ» — собственные логи работают не только на расследование, но и на переписку с поддержкой.

Стоит ли логировать сам API-ключ для удобства отладки?

Нет, никогда — ни полностью, ни в обратимом виде. Максимум — последние несколько символов или алиас ключа внутри вашей системы: этого достаточно, чтобы различать активные ключи в логах, не создавая ещё один канал утечки того же секрета.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →