Ваш API-ключ утечёт. Как сделать так, чтобы это ничего не стоило

от автора

По данным GitGuardian (State of Secrets Sprawl), только за 2024 год в публичный GitHub утекло около 23,7 млн секретов — ключей API, токенов, паролей. Подавляющее большинство — не результат взлома, а обычные коммиты, логи и клиентские бандлы. Свежий ключ, попавший в публичный репозиторий, боты начинают пробовать меньше чем через минуту.

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

Почему ключи утекают всегда

Один ключ OpenAI в среднем проекте живёт в пяти местах одновременно:

  • в .env на ноутбуках разработчиков;

  • в секретах CI (и иногда в логах CI — printenv в отладочном шаге);

  • в конфиге продакшена;

  • в скриптах «на минутку» (test_gpt.py с ключом в строке);

  • и с недавних пор — в контексте ИИ-агента, который «пишет код сам».

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

Мера

Что закрывает

Что не закрывает

.env + .gitignore

случайный коммит

логи, CI, агентов, «скрипт на минутку»

Сканеры (gitleaks, trufflehog)

утечку в git до пуша

все остальные каналы

Секрет-менеджеры (Vault, ASM)

хранение и доставку

ключ всё равно в runtime-окружении приложения

Скоуп-ключи провайдера

радиус поражения

есть не у всех провайдеров; отзыв всё равно ломает всех потребителей ключа

Общая черта: во всех схемах настоящий ключ в какой-то момент оказывается у потребителя.

А значит, утекает вместе с ним

Схема: credential proxy

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

приложение ──(vlt_проходка)──▶ прокси ──(настоящий ключ)──▶ провайдер                                 │                     статус / IP / лимиты → лог
  • Секрет — настоящий ключ. Вводится один раз, шифруется, наружу не возвращается никогда и никаким API.

  • Проходка (pass) — виртуальный токен, привязанный к секрету. Своя привязка к IP, свои лимиты rpm/rpd, свой срок жизни, свой лог. Именно её получают приложения, скрипты и агенты.

  • Прокси — проверяет проходку, расшифровывает секрет в памяти на время одного запроса, подставляет и стримит ответ обратно.

Следствия:

  1. Зона, где может утечь оригинал, сжимается до одного сервера.

  2. Утечка проходки — не инцидент: с чужого IP — 403, сверх лимита — 429, отзыв — один клик, оригинал не тронут.

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

Для кода переход — две строки: api.openai.com<прокси>/p/openai, sk-…vlt_…. Путь, тело, заголовки и SSE-стриминг проходят как есть.

Как это устроено внутри (наша реализация)

Мы собрали такую схему как сервис — proxykey (Node 22 + Fastify 5 + PostgreSQL + Redis). Технические решения, которые могут быть интересны независимо от продукта:

  • Конвертное шифрование: на каждый секрет — свой DEK (AES-256-GCM, AAD = id секрета), DEK завёрнут KEK’ом из окружения. Расшифровка — в памяти на время одного запроса, плейнтекст нигде не кэшируется и не логируется, DEK зануляется после использования.

  • Токены не хранятся: в базе только SHA-256-хэши проходок; горячий путь валидирует по Redis-кэшу (TTL 300 с) с фолбэком в Postgres.

  • Деградация по-разному в разные стороны: упал Postgres — работаем с кэша (fail-closed для некэшированных токенов); упал Redis — валидируем по Postgres, но рейт-лимиты открываются (fail-open), а валидация — нет.

  • SSRF-guard: кастомные base URL резолвятся и проверяются на приватные/метаданные-диапазоны, иначе прокси с пользовательскими апстримами — готовый SSRF-вектор.

  • Логи запросов: партиционированный по месяцам Postgres, метаданные без авторизационных заголовков и значений ключей.

  • Telegram-боты — отдельная боль: токен там живёт в пути URL, а aiogram/grammY валидируют его формат до первого запроса. Прокси принимает bot<digits>:vlt_… и игнорирует цифры — формат проходит валидацию клиентских библиотек, настоящий токен подставляется на сервере.

ИИ-агенты: главный новый потребитель ключей

Отдельная причина, по которой мы вообще в это ввязались. Агенты (Claude Code, Cursor и т.д.) разворачивают сервисы и настраивают ботов — им постоянно нужны ключи. Но всё, что попало в контекст модели, надо считать опубликованным: контекст логируется, трейсится и выманивается промпт-инъекцией.

Решение — дать агенту не секрет, а инструмент: MCP-сервер хранилища. Агент подключается по URL с токеном mcp_… и умеет выписывать, ротировать, отзывать проходки и читать логи. Операции «прочитать настоящий ключ» в наборе инструментов просто нет — это свойство протокола доступа, а не обещание.

Любимый сценарий — «отложенный секрет»: агент разворачивает Telegram-бота, токена которого ещё не существует. Агент создаёт pending-проходку, прописывает её в конфиг, отдаёт человеку ссылку; человек вводит настоящий токен в панели — проходка активируется, бот оживает. Агент закончил работу, ни разу не увидев секрета.

Честные trade-offs

  • Прокси — критичная точка. Если он лежит, лежат все ваши вызовы. Лечится репликами и кэшем валидации, но это надо понимать.

  • Прокси видит трафик. Вопрос не «видит ли», а «что логирует». В нашей реализации — только метаданные; превью тел — опционально и по-проходочно. Если модель угроз не допускает третью сторону — разворачивайте такую схему у себя, паттерн воспроизводим.

  • Латентность. Один хоп + проверка по кэшу — единицы миллисекунд на фоне сотен миллисекунд генерации LLM. Для низколатентных не-LLM API считайте сами.

Диклеймер

Мы — авторы proxykey (proxykey.org). Сервис бесплатный, без карты; панель, прокси и MCP-сервер описаны в статье честно, включая ограничения. Схема credential proxy воспроизводима и без нас — если соберёте свою, главное не забудьте SSRF-guard и fail-closed на валидации.

ссылка на оригинал статьи https://habr.com/ru/articles/1056070/