
По данным GitGuardian ( State of Secrets Sprawl ), только за 2024 год в публичный GitHub утекло около 23,7 млн секретов — ключей API, токенов, паролей. Подавляющее большинство — не результат взлома, а обычные коммиты, логи и клиентские бандлы. Свежий ключ, попавший в публичный репозиторий, боты начинают пробовать меньше чем через минуту.
Вывод, к которому мы пришли после N-го инцидента: бороться за то, чтобы ключ не утёк — проигранная война. Выигрышная — сделать утечку бесполезной.
Почему ключи утекают всегда
Один ключ OpenAI в среднем проекте живёт в пяти местах одновременно:
в .env на ноутбуках разработчиков;
в .env на ноутбуках разработчиков;
в секретах CI (и иногда в логах CI — printenv в отладочном шаге);
в секретах CI (и иногда в логах CI — printenv в отладочном шаге);
в конфиге продакшена;
в конфиге продакшена;
в скриптах «на минутку» ( test_gpt.py с ключом в строке);
в скриптах «на минутку» ( test_gpt.py с ключом в строке);
и с недавних пор — в контексте ИИ-агента, который «пишет код сам».
и с недавних пор — в контексте ИИ-агента, который «пишет код сам».
Каждая копия — независимая точка отказа. Классические меры работают, но у каждой есть предел:
Что закрывает
Что не закрывает
.env + .gitignore
случайный коммит
логи, CI, агентов, «скрипт на минутку»
Сканеры (gitleaks, trufflehog)
утечку в git до пуша
все остальные каналы
Секрет-менеджеры (Vault, ASM)
хранение и доставку
ключ всё равно в runtime-окружении приложения
Скоуп-ключи провайдера
радиус поражения
есть не у всех провайдеров; отзыв всё равно ломает всех потребителей ключа
Общая черта: во всех схемах настоящий ключ в какой-то момент оказывается у потребителя .
А значит, утекает вместе с ним
Схема: credential proxy
Идея не новая (так работают, например, платёжные токенизаторы), но для API-ключей она почему-то почти не применяется. Разрываем связь «приложение — ключ»:
Секрет — настоящий ключ. Вводится один раз, шифруется, наружу не возвращается никогда и никаким API.
Секрет — настоящий ключ. Вводится один раз, шифруется, наружу не возвращается никогда и никаким API.
Проходка (pass) — виртуальный токен, привязанный к секрету. Своя привязка к IP, свои лимиты rpm/rpd, свой срок жизни, свой лог. Именно её получают приложения, скрипты и агенты.
Проходка (pass) — виртуальный токен, привязанный к секрету. Своя привязка к IP, свои лимиты rpm/rpd, свой срок жизни, свой лог. Именно её получают приложения, скрипты и агенты.
Прокси — проверяет проходку, расшифровывает секрет в памяти на время одного запроса, подставляет и стримит ответ обратно.
Прокси — проверяет проходку, расшифровывает секрет в памяти на время одного запроса, подставляет и стримит ответ обратно.
Следствия:
Зона, где может утечь оригинал, сжимается до одного сервера.
Зона, где может утечь оригинал, сжимается до одного сервера.
Утечка проходки — не инцидент: с чужого IP — 403, сверх лимита — 429, отзыв — один клик, оригинал не тронут.
Утечка проходки — не инцидент: с чужого IP — 403, сверх лимита — 429, отзыв — один клик, оригинал не тронут.
Побочный бонус: видно, кто и сколько ходит по каждому потребителю, потому что у каждого — своя проходка.
Побочный бонус: видно, кто и сколько ходит по каждому потребителю, потому что у каждого — своя проходка.
Для кода переход — две строки: api.openai.com → <прокси>/p/openai , sk-… → vlt_… . Путь, тело, заголовки и SSE-стриминг проходят как есть.
Как это устроено внутри (наша реализация)
Мы собрали такую схему как сервис — proxykey (Node 22 + Fastify 5 + PostgreSQL + Redis). Технические решения, которые могут быть интересны независимо от продукта:
Конвертное шифрование : на каждый секрет — свой DEK (AES-256-GCM, AAD = id секрета), DEK завёрнут KEK’ом из окружения. Расшифровка — в памяти на время одного запроса, плейнтекст нигде не кэшируется и не логируется, DEK зануляется после использования.
Конвертное шифрование : на каждый секрет — свой DEK (AES-256-GCM, AAD = id секрета), DEK завёрнут KEK’ом из окружения. Расшифровка — в памяти на время одного запроса, плейнтекст нигде не кэшируется и не логируется, DEK зануляется после использования.
Токены не хранятся : в базе только SHA-256-хэши проходок; горячий путь валидирует по Redis-кэшу (TTL 300 с) с фолбэком в Postgres.
Токены не хранятся : в базе только SHA-256-хэши проходок; горячий путь валидирует по Redis-кэшу (TTL 300 с) с фолбэком в Postgres.
Деградация по-разному в разные стороны : упал Postgres — работаем с кэша (fail-closed для некэшированных токенов); упал Redis — валидируем по Postgres, но рейт-лимиты открываются (fail-open), а валидация — нет.
Деградация по-разному в разные стороны : упал Postgres — работаем с кэша (fail-closed для некэшированных токенов); упал Redis — валидируем по Postgres, но рейт-лимиты открываются (fail-open), а валидация — нет.
SSRF-guard : кастомные base URL резолвятся и проверяются на приватные/метаданные-диапазоны, иначе прокси с пользовательскими апстримами — готовый SSRF-вектор.
SSRF-guard : кастомные base URL резолвятся и проверяются на приватные/метаданные-диапазоны, иначе прокси с пользовательскими апстримами — готовый SSRF-вектор.
Логи запросов : партиционированный по месяцам Postgres, метаданные без авторизационных заголовков и значений ключей.
Логи запросов : партиционированный по месяцам Postgres, метаданные без авторизационных заголовков и значений ключей.
Telegram-боты — отдельная боль: токен там живёт в пути URL, а aiogram/grammY валидируют его формат до первого запроса. Прокси принимает bot<digits>:vlt_… и игнорирует цифры — формат проходит валидацию клиентских библиотек, настоящий токен подставляется на сервере.
Telegram-боты — отдельная боль: токен там живёт в пути URL, а aiogram/grammY валидируют его формат до первого запроса. Прокси принимает bot<digits>:vlt_… и игнорирует цифры — формат проходит валидацию клиентских библиотек, настоящий токен подставляется на сервере.
ИИ-агенты: главный новый потребитель ключей
Отдельная причина, по которой мы вообще в это ввязались. Агенты (Claude Code, Cursor и т.д.) разворачивают сервисы и настраивают ботов — им постоянно нужны ключи. Но всё, что попало в контекст модели, надо считать опубликованным: контекст логируется, трейсится и выманивается промпт-инъекцией.
Решение — дать агенту не секрет, а инструмент : MCP-сервер хранилища. Агент подключается по URL с токеном mcp_… и умеет выписывать, ротировать, отзывать проходки и читать логи. Операции «прочитать настоящий ключ» в наборе инструментов просто нет — это свойство протокола доступа, а не обещание.
Любимый сценарий — «отложенный секрет»: агент разворачивает Telegram-бота, токена которого ещё не существует. Агент создаёт pending-проходку, прописывает её в конфиг, отдаёт человеку ссылку; человек вводит настоящий токен в панели — проходка активируется, бот оживает. Агент закончил работу, ни разу не увидев секрета.
Честные trade-offs
Прокси — критичная точка. Если он лежит, лежат все ваши вызовы. Лечится репликами и кэшем валидации, но это надо понимать.
Прокси — критичная точка. Если он лежит, лежат все ваши вызовы. Лечится репликами и кэшем валидации, но это надо понимать.
Прокси видит трафик. Вопрос не «видит ли», а «что логирует». В нашей реализации — только метаданные; превью тел — опционально и по-проходочно. Если модель угроз не допускает третью сторону — разворачивайте такую схему у себя, паттерн воспроизводим.
Прокси видит трафик. Вопрос не «видит ли», а «что логирует». В нашей реализации — только метаданные; превью тел — опционально и по-проходочно. Если модель угроз не допускает третью сторону — разворачивайте такую схему у себя, паттерн воспроизводим.
Латентность. Один хоп + проверка по кэшу — единицы миллисекунд на фоне сотен миллисекунд генерации LLM. Для низколатентных не-LLM API считайте сами.
Латентность. Один хоп + проверка по кэшу — единицы миллисекунд на фоне сотен миллисекунд генерации LLM. Для низколатентных не-LLM API считайте сами.
Мы — авторы proxykey ( proxykey.org ). Сервис бесплатный, без карты; панель, прокси и MCP-сервер описаны в статье честно, включая ограничения. Схема credential proxy воспроизводима и без нас — если соберёте свою, главное не забудьте SSRF-guard и fail-closed на валидации.