Как хранить секреты вне AI-агентов
API-ключ в окружении агента может утечь за один вызов инструмента. Используйте scoped short-lived tokens через credential gateway, а не реальные ключи.
Что означает хранение секретов вне AI-агентов
AI-агент — это обычный процесс Linux, который выполняет команды. Код, запущенный этим процессом, может прочитать все переменные окружения, доступные процессу. Поэтому API-ключ в окружении агента может быть отправлен агентом на любой доступный ему хост. Хранить секреты вне агента означает предоставить ему дескриптор вместо ключа: токен с ограниченным сроком действия и областью применения либо заполнитель, который другой компонент заменяет реальным значением на границе сети.
Речь не о том, что модель стала вредоносной. Механизм проще. Агент читает веб-страницу, README или комментарий к issue с инструкциями и выполняет их, потому что для языковой модели нет разницы между написанным вами текстом и полученным из сети текстом. Это prompt injection. После этого ущерб ограничивается ровно одним фактором: тем, что может прочитать процесс. Если граница еще не настроена, в разделе безопасный запуск coding agent на сервере описаны уровни изоляции, на которых основано это руководство.
Модель угроз простыми словами
Выполните это от имени пользователя, от имени которого работает ваш агент.
tr '\0' '\n' < /proc/self/environ | grep -iE 'key|token|secret|password'Каждая выводимая им строка соответствует одному HTTP-запросу к серверу постороннего лица. Теперь проверьте, какие данные находятся на диске рядом с агентом.
grep -rIl --exclude-dir=.git -e 'API_KEY' -e 'SECRET' ~/projects
find ~ -maxdepth 3 -name '.env' -o -name 'credentials' -o -name '*.pem'Агенту с доступом к shell не нужен сложный эксплойт, чтобы вывести эти данные наружу. Для этого достаточно четырех обычных способов, и в журнале все они выглядят как нормальная работа:
- Исходящий
curlилиfetchк любому узлу, где значение передается в строке запроса. git commitиgit pushв репозиторий, в который агент может выполнять запись.- Скрипт установки пакета, который выполняет произвольный код от имени пользователя агента.
- DNS-запрос имени хоста, содержащего это значение. Он выполняется даже при блокировке исходящего HTTP-трафика.
Проверка журналов не решит эту проблему. Необходимо исключить наличие ценных данных в зоне доступа агента.
Секрет в рабочем дереве становится секретом в контекстном окне
Агент читает файлы. Файл .env в репозитории, с которым он работает, будет прочитан. После этого он окажется в контекстном окне. Это означает, что он попадет в расшифровку, в любой журнал, который вы ведете, и в следующий текст, который агент создаст.
Раньше ключ находился в дереве, с которым работал агент:
cd ~/projects/billing
cat .env
# STRIPE_SECRET_KEY=sk_live_...
# DATABASE_URL=postgres://app:hunter2@db.internal:5432/billingТеперь файл перемещен туда, где агент не может его получить:
sudo install -d -m 750 -o root -g agent-review /etc/agent-review
sudo install -m 640 -o root -g agent-review ~/projects/billing/.env /etc/agent-review/billing.env
rm ~/projects/billing/.envПользователь агента больше не может открыть файл, поскольку в рабочем дереве его нет. Правила запрета в собственной конфигурации агента — это второй уровень защиты, а не первый. Claude Code считывает правила доступа из .claude/settings.json в проекте:
{
"permissions": {
"deny": ["Read(./.env)", "Read(./secrets/**)", "Read(./**/*.pem)"]
}
}Это предотвращает случайное открытие файла агентом во время исследования. Но это не препятствует выполнению внедренной инструкции с помощью base64 .env, поскольку это команда оболочки, а не чтение файла. Рассматривайте конфигурацию как защитное ограничение, а разрешения файловой системы — как физический барьер. Такое же разделение действует внутри контейнеров: в разделе файлы окружения и секреты в Docker Compose рассматривается вариант этой проблемы на уровень ниже.
Выделите каждому агенту отдельного непривилегированного пользователя
Если агент работает от вашей учетной записи, он получает доступ к вашим ключам SSH, учетным данным облачных сервисов и истории оболочки. Отдельная учетная запись требует одной команды и устраняет все эти риски.
sudo adduser --disabled-password --gecos "" agent-review
sudo chmod 700 /home/agent-review
sudo -u agent-review cat ~/.ssh/id_ed25519Последняя строка должна завершиться ошибкой с кодом cat: /home/you/.ssh/id_ed25519: Permission denied. Если вместо этого она выводит ключ, домашний каталог доступен для чтения группе или всем пользователям. Команда chmod 700 ~ исправляет эту проблему. Не добавляйте пользователя агента в sudo и не задавайте для него правило NOPASSWD шире, чем требуется для единственной команды. В статье Минимальные привилегии пользователей на VPS подробно рассматриваются группы и настройка sudoers.
На облачном VPS стоит добавить еще одну границу защиты. Сервис метаданных экземпляра отвечает по фиксированному локальному адресу канального уровня. Он часто выдает учетные данные роли любому запросившему их процессу.
sudo iptables -A OUTPUT -m owner --uid-owner agent-review -d 169.254.169.254 -j REJECTПроверьте это со стороны агента. Команда sudo -u agent-review curl -s --max-time 3 http://169.254.169.254/ должна ничего не вывести и завершиться с ненулевым кодом, поскольку пакет отклоняется до того, как покидает сервер.
Внедрение учетных данных на границе
Решение этой задачи — внедрение учетных данных. Агент никогда не получает настоящий ключ. Он отправляет запрос через локальный шлюз, а шлюз по пути заменяет заполнитель настоящим секретом. Секрет хранится в хранилище шлюза, в отдельном процессе, от имени другого пользователя.
OneCLI — одна из реализаций с открытым исходным кодом. Она распространяется по лицензии Apache-2.0 и запускается в контейнере рядом с агентом. По состоянию на июль 2026 года проект описывает следующую конфигурацию:
git clone https://github.com/onecli/onecli.git
cd onecli
docker compose -f docker/docker-compose.yml up -d --waitПанель управления прослушивает порт 10254, а шлюз — порт 10255. Вы один раз сохраняете настоящие учетные данные, а затем задаете каждому агенту вместо ключа значение-заполнитель и отдельный токен доступа с ограниченной областью действия. Агент передает этот токен в заголовке Proxy-Authorization. Шлюз сопоставляет исходящий запрос по узлу и пути, расшифровывает соответствующие учетные данные и подставляет их. В окружении агента не хранится ничего, что представляло бы ценность для злоумышленника.
Ценность этого подхода не в шифровании. Вопрос «что использовал этот агент и когда» превращается в запрос к журналу. Вы просматриваете один журнал аудита, а не пытаетесь определить, в каком из шести окружений сохранилась копия ключа.
Передавайте секрет процессу, а не в окружение
Если вы запускаете агент под управлением systemd, переменные окружения вообще не нужны. LoadCredential= помещает секрет в закрытый каталог, который может читать только эта служба. В unit-файле он доступен как %d, а внутри процесса — как $CREDENTIALS_DIRECTORY. Значение никогда не появляется в /proc/<pid>/environ, поэтому ps eww не может его показать. Каталог удаляется после остановки службы.
Сначала зашифруйте учетные данные для этой машины. Эти команды приведены в документации systemd и работают в systemd 250 или более новой версии. Это охватывает Ubuntu 24.04 и Debian 13:
echo -n 'sk-example-value' > /tmp/plain.txt
sudo systemd-creds encrypt --name=api_key /tmp/plain.txt /etc/credstore/api_key.cred
shred -u /tmp/plain.txt
sudo systemd-run -P --wait -p LoadCredentialEncrypted=api_key:/etc/credstore/api_key.cred \
systemd-creds cat api_keyПоследняя команда выводит sk-example-value. Это подтверждает, что зашифрованный файл расшифровывается на этом хосте. Затем укажите его в unit-файле:
[Service]
User=agent-review
LoadCredentialEncrypted=api_key:/etc/credstore/api_key.cred
Environment=AGENT_KEY_FILE=%d/api_key
ExecStart=/usr/local/bin/agent-workerКод агента открывает файл по пути $AGENT_KEY_FILE, когда ему требуется значение. Чтение файла занимает один момент. Переменная окружения сохраняется в течение всего времени работы процесса и доступна каждому запускаемому им дочернему процессу.
Используйте краткоживущие токены вместо долгоживущих ключей
Ключ, срок действия которого не истекает, остается действительным, если спустя несколько месяцев он появляется в журнале или стенограмме. Если сервис поддерживает токен сеанса, используйте его и задайте минимальный срок действия, допустимый для этой работы.
aws sts assume-role \
--role-arn arn:aws:iam::123456789012:role/agent-readonly \
--role-session-name agent-review \
--duration-seconds 900Пятнадцать минут — минимальный срок, который принимает AWS STS (security token service). Обычно этого достаточно для одной задачи агента. Для GitHub создайте для пользователя агента отдельную учетную запись gh с токеном с детализированными разрешениями, ограниченным одним репозиторием, с которым он работает, чтобы gh auth token внутри этого сеанса возвращал объект, который не может обращаться к другим ресурсам. Сначала ограничьте область ресурсов, затем срок действия.
Проверьте, затем продолжайте проверять
После любого изменения конфигурации агента следует выполнить 3 проверки. Запускайте их от имени пользователя агента, а не от своего имени.
sudo -u agent-review env | grep -iE 'key|token|secret'
sudo -u agent-review ls -la /home/you/ 2>&1 | head -3
sudo -u agent-review curl -s -o /dev/null -w '%{http_code}\n' --max-time 5 https://api.github.com/userПервая команда не должна выводить ничего. Вторая должна вывести ls: cannot open directory '/home/you/': Permission denied. Третья показывает, какую идентичность представляет сетевой путь агента. Именно на этот вопрос отвечает схема с gateway: значение 401 означает, что у агента нет собственных учетных данных GitHub, а 200 означает, что они есть. В последнем случае необходимо знать, какой именно token используется. Если агенты работают без контроля, в материале управление расходами AI-агентов на VPS описаны ограничения бюджета, которые дополняют эти ограничения доступа.
FAQ
Можно ли просто доверять модели и считать, что она не раскроет мои ключи?
Нет, поскольку в этой модели угроз модель не является атакующим. Агент читает текст веб-страниц, репозиториев и систем отслеживания ошибок, а этот текст может содержать инструкции. У модели нет надежного способа отличить ваши инструкции от полученного текста. Любой контроль, который зависит от правильного выбора модели, не сработает при первой убедительной внедренной инструкции. Поэтому такой контроль должен находиться в операционной системе или сети.
Действительно ли переменные окружения настолько опасны для секретов агента?
Они опасны по одной конкретной причине: они наследуются. Каждый дочерний процесс, запущенный агентом, получает их копию, включая сценарий сборки, средство запуска тестов и любой обработчик установки пакета. Те же переменные доступны для чтения через /proc/<pid>/environ тому же пользователю. Поэтому любой процесс, запущенный агентом, может прочитать их без передачи со стороны агента. Файл, читаемый непосредственно в момент использования, с помощью LoadCredential= или шлюза, ограничивает область раскрытия этим моментом.
Решает ли сама по себе передача секретов в хранилище секретов эту проблему?
Только частично. Хранилище секретов решает проблему хранения. Но оно не решает проблему последнего шага, когда некоторый компонент извлекает секрет из хранилища и передает его агенту как переменную окружения. В результате вы возвращаетесь к исходной ситуации. Важно, кто выполняет подстановку. Если секрет получает агент, секрет оказывается у агента. Если подстановку выполняет шлюз или система инициализации вне процесса агента, агент не получает секрет.
Как узнать, не раскрыл ли агент уже какие-либо данные?
Обычно постфактум это определить нельзя. Именно поэтому нужен шлюз. Без шлюза сведения разбросаны по истории оболочки, журналу действий агента и журналам исходящих соединений, которые вы, вероятно, не сохраняете. При использовании шлюза учетных данных каждое использование учетных данных представлено одной строкой с идентификатором агента и временной меткой. Если вы подозреваете утечку, сначала замените ключ, а затем проводите расследование. Замена ключа обходится недорого, а уверенность в отсутствии утечки недостижима.
Что следует сделать в первую очередь?
Переместите каждый файл .env из каталогов, в которых работают агенты, и создайте отдельного непривилегированного пользователя для каждого агента. Эти два изменения занимают около десяти минут и закрывают наиболее распространенный путь атаки: агент читает файл с учетными данными, которому не место рядом с кодом. Шлюз и короткоживущие токены — следующий шаг, а не первый. Тот же начальный подход применим к любой среде выполнения агента, включая безопасный запуск автономного агента на VPS.