Как защитить API-ключи от утечки через AI-агентов
Узнайте, почему передача реальных API-ключей в переменные окружения агента приводит к их утечке. Используйте короткоживущие токены и прокси-шлюзы для безопасной работы.
Что означает исключение секретов из AI-агентов
AI-агент — это обычный процесс в Linux, который выполняет команды. Любая переменная окружения, доступная этому процессу, может быть прочитана исполняемым кодом. Следовательно, API-ключ в окружении агента — это ключ, который агент может отправить на любой доступный ему хост. Исключение секретов из агента означает предоставление ему дескриптора вместо самого ключа: короткоживущего токена с ограниченными правами или заполнителя, который заменяется на реальное значение чем-то другим на сетевом уровне.
Речь не идет о том, что модель становится враждебной. Механизм гораздо прозаичнее. Агент читает веб-страницу, README или комментарий к задаче, содержащие инструкции, и выполняет их, поскольку для языковой модели нет разницы между текстом, который написали вы, и текстом, который она получила извне. Это и есть prompt injection. Как только это происходит, масштаб ущерба ограничивается ровно одним фактором: тем, что может прочитать процесс. Если вы еще не установили границы, безопасный запуск агента для написания кода на сервере описывает уровни изоляции, на которых базируется данное руководство.
Модель угроз простыми словами
Запускайте это от имени пользователя, под которым работает ваш агент.
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)"]
}
}Это предотвращает случайное открытие файла агентом во время исследования проекта. Ограничения действуют и за пределами файловых инструментов: Claude Code проверяет эти правила для команд Bash, таких как cat и head, а также для целей перенаправления потоков, например < .env. Это не остановит выполнение внедренной инструкции grep -r KEY . внутри каталога проекта или короткого скрипта на Python, который открывает .env самостоятельно. Документация по правам доступа гласит, что правила запрета не применяются к командам, которые читают файлы без явного указания их имен, или к подпроцессам, открывающим файлы напрямую. Для таких случаев в документации рекомендуется использовать песочницу Claude Code, которая блокирует доступ на уровне операционной системы. Будет ли у вас запрошено подтверждение перед выполнением такой команды, зависит от режима разрешений сессии. Автоматический режим теперь является встроенным режимом по умолчанию для интерактивных сессий Claude Code, поэтому сервер, за которым вы не наблюдаете, будет выполнять больше подобных команд без запроса. То же ограничение касается всего, что формирует привычки агента, а не его права доступа: навык, заставляющий агента вносить минимально необходимые изменения, не дает ему обращаться к файлам, которые не относятся к задаче, но это лишь рекомендация, которую модель может проигнорировать. Рассматривайте конфигурацию как ограждение, а права доступа к файловой системе — как стену. Такое же разделение актуально и внутри контейнеров: 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. Если вместо этого выводится ключ, значит, пользователь агента имеет доступ к файлу. Для открытия файла по имени требуется право на поиск (x) в каждом родительском каталоге, а не право на чтение. Поэтому домашний каталог с правами 711, который никто не может просмотреть, всё равно позволит получить ключ, если сам файл ключа доступен для чтения. chmod 700 ~ закрывает путь, а chmod 600 ~/.ssh/id_ed25519 исправляет права на ключ. Не добавляйте пользователя агента в sudo и не создавайте для него правило NOPASSWD шире, чем требуется для выполнения одной конкретной команды. В статье Пользователи с минимальными привилегиями на VPS подробно описана работа с группами и sudoers. Помните об этом разделении, если запускаете более одного сеанса на сервере, поскольку один сеанс Claude Code может отправить текст напрямую в другой, и всё, к чему имеет доступ первый сеанс, может быть передано через этот канал в одном сообщении.
На облачном VPS стоит добавить еще один рубеж защиты. Служба метаданных экземпляра отвечает по фиксированному локальному адресу и часто выдает учетные данные роли любому, кто отправит запрос. На конечной точке не проверяется, какой именно локальный пользователь делает запрос: в документации AWS для EC2 указано, что вызовы IMDSv1 не требуют токена, и для ограничения доступа пользователей к службе рекомендуется использовать правило локального брандмауэра, привязанное к ID пользователя.
sudo iptables -I OUTPUT 1 -p tcp -m owner --uid-owner agent-review -d 169.254.169.254 -j REJECTВставьте правило в начало списка с помощью -I, а не добавляйте его в конец через -A. На хосте с ufw, где по умолчанию разрешен исходящий трафик, последняя цепочка OUTPUT в ufw принимает все новые TCP-соединения, поэтому добавленное в конец правило никогда не будет достигнуто. Правила iptables также исчезают после перезагрузки, если их не сохранить. Если вы включили IPv6-адрес службы метаданных, [fd00:ec2::254], добавьте аналогичное правило с помощью ip6tables.
Проверьте настройку со стороны агента. Команда sudo -u agent-review curl -s --max-time 3 http://169.254.169.254/ не должна ничего выводить и должна завершаться с ненулевым кодом, так как пакет отклоняется до того, как покинет хост. Если выводится список, значит, правило не применяется.
Внедрение учетных данных на границе
Шаблон, который действительно решает эту задачу — внедрение учетных данных (credential injection). Агент никогда не хранит настоящий ключ. Он отправляет запрос через локальный шлюз, а шлюз на выходе заменяет заполнитель (placeholder) на реальный секрет. Секрет находится в хранилище шлюза, в отдельном процессе, принадлежащем другому пользователю.
OneCLI — одна из реализаций этого подхода с открытым исходным кодом. Она распространяется по лицензии Apache-2.0, за исключением каталогов ee/, на которые распространяется отдельная корпоративная лицензия, требующая подписки для использования в промышленной среде. OneCLI запускается в контейнерах рядом с агентом. По состоянию на октябрь 2026 года проект документирует следующую конфигурацию Docker Compose, которая считывает три сгенерированных секрета из файла .env, расположенного рядом с файлом compose:
git clone https://github.com/onecli/onecli.git && cd onecli/docker
cat > .env <<EOF
SECRET_ENCRYPTION_KEY=$(head -c 32 /dev/urandom | base64)
GATEWAY_INTERNAL_SECRET=$(head -c 32 /dev/urandom | base64)
BETTER_AUTH_SECRET=$(head -c 32 /dev/urandom | base64)
COMPOSE_PROFILES=runner
EOF
chmod 600 .env
docker compose up -d --waitСоздайте резервную копию файла .env, так как его потеря приведет к тому, что все хранящиеся в нем секреты станут недоступны (orphans) SECRET_ENCRYPTION_KEY. Панель управления прослушивает порт 10254, а шлюз — порт 10255; по умолчанию оба привязаны к 127.0.0.1. Вы сохраняете реальные учетные данные один раз, а затем предоставляете каждому агенту значение-заполнитель вместо ключа, а также его собственный токен доступа с ограниченными правами, который агент отправляет в заголовке Proxy-Authorization. Шлюз сопоставляет исходящий запрос по хосту и пути, расшифровывает соответствующий секрет и подставляет его. В окружении агента нет ничего, что стоило бы красть.
Ценность здесь заключается не в шифровании. Суть в том, что вопрос «что использовал этот агент и когда» превращается в запрос к логам. Вы читаете один журнал аудита вместо того, чтобы гадать, в какой из шести сред хранилась копия ключа.
Передавайте секрет процессу, а не через переменные окружения
Если вы запускаете агент под управлением systemd, переменные окружения не требуются вовсе. LoadCredential= размещает секрет в закрытом каталоге, который может прочитать только пользователь сервиса; он предоставляется как %d в файле юнита и как $CREDENTIALS_DIRECTORY внутри процесса. Это ограничение действует на уровне пользователя, а не сервиса, поэтому создайте для каждого сервиса агента отдельного пользователя: любой другой процесс, запущенный от того же пользователя, сможет открыть этот каталог, если только в юните не используется изоляция файловой системы, например PrivateMounts=yes, которая скрывает его от других сервисов. Значение никогда не появляется в /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. Это подтверждает, что зашифрованный файл расшифровывается на данном хосте. Затем укажите его в юните:
[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 логин с токеном с ограниченными правами (fine grained token), который действует только в рамках одного репозитория. В этом случае gh auth token внутри сессии вернет данные, которые не позволят получить доступ к чему-либо еще. Сначала ограничивайте область действия ресурсами, а затем — временем.
Проверка и постоянный контроль
После любого изменения в настройках агента рекомендуется выполнить три проверки. Запускайте их от имени пользователя, под которым работает агент, а не от своего имени.
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, хотя это лишь подтверждает невозможность просмотра содержимого каталога; проверка cat, выполненная ранее, доказывает, что файл внутри него прочитать нельзя. Третья команда показывает, под какой учетной записью агент обращается к сети — именно на этот вопрос отвечает паттерн шлюза: 401 означает, что запрос достиг GitHub без учетных данных, которые GitHub может принять (они либо отсутствуют, либо недействительны, либо истек срок их действия), а 200 означает, что запрос содержит действительные данные, поэтому вы должны понимать, какой именно токен используется. Если вы запускаете агентов в автоматическом режиме, в разделе контроль расходов на AI-агентов на VPS описаны бюджетные ограничения, которые применяются совместно с этими лимитами доступа.
FAQ
Могу ли я просто довериться модели, чтобы она не раскрыла мои ключи?
Нет, так как в данной модели угроз модель не является атакующим. Агент считывает текст с веб-страниц, из репозиториев и систем отслеживания задач, и этот текст может содержать инструкции. У модели нет надежного способа отличить ваши инструкции от текста, который она получила. Любой механизм контроля, зависящий от правильного выбора модели, даст сбой при первой же убедительной инъекции инструкций, поэтому средства защиты должны находиться на уровне операционной системы или сети.
Действительно ли переменные окружения так плохи для секретов агента?
Они плохи по одной конкретной причине: они наследуются. Каждый дочерний процесс, порождаемый агентом, получает их копию, включая скрипты сборки, средства запуска тестов и любые хуки установки пакетов. Переменные также доступны для чтения через /proc/<pid>/environ тем же пользователем, поэтому любой процесс, запущенный агентом, может прочитать их без участия самого агента. Чтение файла непосредственно в момент использования с помощью LoadCredential= или шлюза ограничивает время воздействия этого секрета.
Решает ли эту проблему само по себе хранение секретов в хранилище (vault)?
Только частично. Хранилище решает вопрос хранения. Если вы используете self-hosted решение, оно требует отдельной настройки безопасности, так как сервер Vaultwarden обычно взламывают через токен администратора или файл резервной копии, а не через зашифрованные элементы, которые он содержит. Это не решает последний этап, на котором некий процесс извлекает секрет из хранилища и передает его агенту в виде переменной окружения, что возвращает вас к исходной точке. Важно то, кто именно выполняет подстановку. Если секрет получает агент, то секрет оказывается у агента. Если подстановку выполняет шлюз или система инициализации вне процесса агента, агент никогда не получает к нему доступ.
Как узнать, не допустил ли агент утечку чего-либо?
Обычно это невозможно определить постфактум, что и является аргументом в пользу использования шлюза. Без него доказательства будут разбросаны по истории оболочки, логам агента и журналам исходящих соединений, которые вы, вероятно, не ведете. При использовании шлюза учетных данных каждое использование учетных данных фиксируется одной строкой с идентификатором агента и меткой времени. Если вы подозреваете утечку, сначала смените ключ, а затем проводите расследование. Ротация ключей обходится дешево, а уверенность — нет.
Что мне нужно сделать в первую очередь уже сегодня?
Переместите все файлы .env из каталогов, в которых работают ваши агенты, и создайте по одному непривилегированному пользователю для каждого агента. Эти два изменения занимают около десяти минут и закрывают самый распространенный путь атаки, при котором агент считывает файл учетных данных, который не должен был находиться рядом с кодом. Шлюз и кратковременные токены — это следующий шаг, а не первый. Тот же подход применим к любой среде выполнения агентов, включая безопасный запуск автономного агента на VPS.