Безопасность AI-агентов

Где хранить ключи и токены AI-агента

Уровень: средний Чтение: до 8 минут Результат: переменные окружения, права и ротация ключей

Введение

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

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

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

Базовая схема хранения

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

Среда Где хранить значение Чего избегать
Локальная разработка Переменная окружения или локальный файл, исключенный из Git Ключ в исходном коде, истории shell или общем чате
CI/CD Защищенные секреты системы сборки Значение в YAML, аргументах команд и выводе шагов
Рабочая среда Хранилище секретов платформы с журналом доступа Постоянный ключ в образе контейнера или репозитории

Воспроизводимые шаги

1. Уберите значение из кода

В коде оставьте только чтение переменной и явную проверку ее наличия. Ниже приведен пример на Python; имя и значение демонстрационные.

import os

api_token = os.getenv("AGENT_API_TOKEN")
if not api_token:
    raise RuntimeError("Переменная AGENT_API_TOKEN не задана")

# Передавайте api_token только клиенту нужного API.
# Не включайте значение в промпты, исключения и журналы.

Безопасная проверка должна сообщать о наличии настройки, не показывая ее содержимое:

import os

print("AGENT_API_TOKEN configured:", bool(os.getenv("AGENT_API_TOKEN")))

2. Задайте переменную для текущего процесса

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

read -r -s -p "AGENT_API_TOKEN: " AGENT_API_TOKEN
printf '\n'
export AGENT_API_TOKEN
python app.py
unset AGENT_API_TOKEN

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

3. Если нужен локальный файл, ограничьте доступ

Некоторые инструменты разработки автоматически читают файл .env. Такой файл допустим только как локальный компромисс: не добавляйте его в репозиторий и ограничьте права владельцем.

umask 077
touch .env
chmod 600 .env
printf '%s\n' '.env' '.env.*' >> .gitignore
printf '%s\n' '!.env.example' >> .gitignore

Добавьте в репозиторий безопасный шаблон без значения:

# .env.example
AGENT_API_TOKEN=

Перед коммитом убедитесь, что рабочий файл игнорируется:

git check-ignore -v .env
git status --short

Если .env уже отслеживается Git, одного добавления в .gitignore недостаточно. Сначала отзовите попавший туда ключ, затем удалите файл из индекса, сохранив локальную копию:

git rm --cached .env
git status --short

4. Разделите ключи по агентам и средам

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

Выдавайте минимальный набор разрешений:

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

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

5. Не допускайте секреты в логи

Не журналируйте полный объект окружения, заголовки HTTP-запроса, параметры клиента или тело запроса без фильтрации. Маскируйте чувствительные поля до передачи логгеру.

SAFE_FIELDS = {"model", "request_id", "status"}

def safe_log_context(context: dict) -> dict:
    return {
        key: value
        for key, value in context.items()
        if key in SAFE_FIELDS
    }

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

6. Не передавайте ключ через командную строку или промпт

Аргументы запущенного процесса могут быть видны другим диагностическим инструментам. Поэтому конструкция вида app --token REAL_VALUE хуже переменной окружения или файлового дескриптора.

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

Ротация ключей без лишнего риска

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

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

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

Проверка результата

Проверяйте не значение, а соблюдение границ:

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

Для локальной проверки на Unix-подобной системе:

test -f .env && stat -c '%a %n' .env
git check-ignore -v .env
git diff --cached --name-only

Ожидаемый принцип результата: .env игнорируется, не числится среди подготовленных к коммиту файлов, а его содержимое доступно только владельцу. Формат вывода stat может отличаться в разных операционных системах.

Для поиска подозрительных имен в отслеживаемых файлах можно использовать проверку без вывода значений:

git grep -IlE '(API_KEY|ACCESS_TOKEN|AUTH_TOKEN|SECRET)' -- \
  ':(exclude).env.example'

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

Типовые ошибки

«Закоммитим сейчас, удалим потом»
Секрет остается в истории Git, форках, кэшах и сборочных артефактах. После публикации его нужно ротировать.
Вывод всех переменных для диагностики
Команда помогает отладке, но отправляет секреты в терминал или CI-лог. Выводите только наличие конкретной настройки.
Один бессрочный ключ на всех
Невозможно понять источник обращения и отозвать доступ одного агента без остановки остальных.
Секрет в Dockerfile
Значение может сохраниться в слоях образа и кэше сборки. Передавайте секрет во время запуска или используйте специальный механизм секретов сборочной системы.
Маскирование только по точному значению
Ключ может попасть в лог в закодированном, частичном или составном виде. Не допускайте чувствительные поля до логгера.
Ротация без отзыва старого ключа
Замена конфигурации не делает прежнее значение недействительным. Отзыв — обязательный отдельный шаг.
Секрет в переписке с моделью
Токен может попасть в историю, трассировку или контекст следующего шага. Передавайте учетные данные напрямую исполняемому инструменту, минуя текстовый промпт.

Ограничения подхода

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

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

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

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

Краткий контрольный список

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