SSD Nodes Learn Hosting plans →
Посібники Matt ConnorВід Matt Connor

Як не передавати секрети AI-агентам

API key у змінній середовища доступний усьому процесу: prompt injection може витягнути його одним викликом. Використовуйте короткоживучі токени через gateway.

Що означає не передавати секрети AI-агентам

AI-агент — це звичайний процес Linux, який виконує команди. Код, який запускає цей процес, може прочитати кожну змінну середовища, доступну процесу. Тому API key у середовищі агента може бути надісланий агентом на будь-який доступний йому хост. Не передавати секрети агенту означає надати йому дескриптор замість ключа: короткоживучий токен з обмеженою областю дії або заповнювач, який інший компонент замінює реальним значенням на межі мережі.

Це не історія про те, що модель стала ворожою. Механізм простіший. Агент читає вебсторінку, README або коментар до issue з інструкціями й виконує їх, оскільки для language model немає різниці між текстом, який написали ви, і текстом, який вона отримала ззовні. Це 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 до будь-якого хоста зі значенням у query string.
  • git commit і git push до репозиторію, у який агент може записувати.
  • Скрипт встановлення пакета, який виконує довільний код від імені користувача агента.
  • DNS-запит до hostname, що містить значення. Він залишає систему, навіть якщо HTTP egress заблоковано.

Перевірками це не виправити. Потрібно зробити так, щоб у зоні доступу не було нічого цінного.

Секрет у робочому дереві — це секрет у контекстному вікні

Агент читає файли. Файл .env у репозиторії, з яким він працює, буде прочитано. Після цього файл потрапляє в контекстне вікно, тобто до transcript, у будь-який журнал, який ви ведете, і в усе, що агент напише далі.

Раніше, коли ключ містився в дереві, з яким працює агент:

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

Користувач агента більше не може відкрити файл, оскільки робоче дерево його більше не містить. Правила deny у власній конфігурації агента — це другий рівень захисту, а не перший. Claude Code читає правила дозволів із .claude/settings.json у проєкті:

{
  "permissions": {
    "deny": ["Read(./.env)", "Read(./secrets/**)", "Read(./**/*.pem)"]
  }
}

Це запобігає ненавмисному відкриттю файла агентом під час перегляду. Правила також поширюються за межі інструментів роботи з файлами: Claude Code перевіряє їх для розпізнаних команд роботи з файлами в Bash, зокрема cat і head, а також для цілей перенаправлень на кшталт < .env. Це не заважає ін’єктованій інструкції виконати grep -r KEY . у каталозі проєкту або запустити короткий скрипт Python, який самостійно відкриває .env, оскільки в документації щодо дозволів зазначено, що правила deny не застосовуються до команд, які читають файли без явного зазначення їхніх імен, або до subprocess, які відкривають файли самостійно. Для таких випадків документація вказує на sandbox Claude Code, який блокує доступ на рівні операційної системи. Чи буде перед виконанням такої команди запитано підтвердження, залежить від permission mode сесії. auto mode тепер є вбудованим режимом запуску для інтерактивних сесій Claude Code, тому сервер, за яким ви не стежите, виконуватиме більше таких команд без запиту. Те саме обмеження стосується всього, що формує звички агента, а не його дозволи: skill, який обмежує агента найменшою достатньою зміною, не дає запуску переходити до файлів, які агент не мав відкривати, але це все одно лише рекомендація, від якої модель можна переконати відступити. Розглядайте конфігурацію як захисний бар’єр, а права доступу до файлової системи — як фізичну межу. Такий самий поділ діє всередині контейнерів: env-файли та секрети в Docker Compose описують варіант цієї проблеми на рівень нижче.

Надайте кожному агенту окремого непривілейованого користувача

Якщо агент працює від вашого імені, він отримує доступ до ваших ключів SSH, облікових даних хмарних сервісів і історії shell. Окремий користувач потребує однієї команди та усуває всі ці ризики.

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 варто додати ще один рівень ізоляції. Сервіс метаданих інстансу відповідає на фіксованій link-local адресі й часто надає облікові дані ролі будь-якому клієнту, який їх запитує. Кінцева точка не перевіряє, який локальний користувач надсилає запит: для EC2 AWS зазначає, що виклики IMDSv1 не потребують токена, а для заборони доступу одного користувача до сервісу рекомендує локальне правило firewall, прив’язане до ID цього користувача.

sudo iptables -I OUTPUT 1 -p tcp -m owner --uid-owner agent-review -d 169.254.169.254 -j REJECT

Додайте правило на початку за допомогою -I, а не в кінці за допомогою -A. На хості з ufw і типовою політикою дозволу вихідного трафіку останнє правило ufw у ланцюжку OUTPUT приймає кожне нове TCP-з’єднання, тому додане в кінці правило ніколи не буде досягнуте. Правила iptables також зникають після перезавантаження, якщо їх не зберегти. Якщо ви ввімкнули IPv6 endpoint сервісу метаданих, [fd00:ec2::254], додайте те саме правило за допомогою ip6tables.

Перевірте це зі сторони агента. sudo -u agent-review curl -s --max-time 3 http://169.254.169.254/ має нічого не вивести та завершитися з ненульовим кодом, оскільки пакет буде відхилено до того, як залишить сервер. Якщо команда виведе список, правило не застосовується.

Впровадьте облікові дані на межі

Справді працездатний підхід — ін’єкція облікових даних. Агент ніколи не зберігає справжній cryptographic key. Він надсилає запит через локальний gateway, а gateway під час вихідної передачі замінює placeholder на справжній секрет. Секрет зберігається у сховищі gateway, в іншому процесі, що належить іншому користувачеві.

OneCLI — це одна з open source реалізацій такого рішення. Вона ліцензована за Apache-2.0, за винятком каталогів ee/, на які поширюється окрема enterprise-ліцензія. Для використання в production за цією ліцензією потрібна підписка. OneCLI працює в контейнерах поруч з agent. Станом на October 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, оскільки втрата SECRET_ENCRYPTION_KEY робить недоступними всі секрети, збережені разом із ним. Dashboard прослуховує порт 10254, а gateway — порт 10255; за замовчуванням обидва прив’язані до 127.0.0.1. Збережіть справжні облікові дані один раз, а кожному agent передайте замість ключа значення-заповнювач і власний токен доступу з обмеженою областю дії, який він надсилає в заголовку Proxy-Authorization. Gateway зіставляє вихідний запит за host і path, розшифровує відповідні облікові дані та підставляє їх. У середовищі agent не зберігається нічого цінного для викрадення.

Цінність тут не в шифруванні. Вона в тому, що запит «що використав цей агент і коли» перетворюється на запит до журналу. Ви переглядаєте один аудиторський журнал замість того, щоб вгадувати, у якому з шести середовищ зберігалася копія ключа.

Передавайте секрет процесу, а не середовищу

Якщо агент запускається через systemd, змінні середовища взагалі не потрібні. LoadCredential= розміщує секрет у приватному каталозі, який може читати лише користувач сервісу. У unit-файлі цей каталог доступний як %d, а всередині процесу — як $CREDENTIALS_DIRECTORY. Це обмеження діє для користувача, а не для сервісу. Тому для кожного сервісу агента використовуйте окремого користувача. Будь-який інший процес, запущений від імені того самого користувача, може відкрити каталог, якщо unit також не використовує ізоляцію файлової системи, наприклад 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. Це підтверджує, що зашифрований файл можна розшифрувати на цьому хості. Потім додайте посилання на нього в 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 логін із fine-grained токеном, обмеженим одним репозиторієм, з яким він працює, щоб 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 вище доводить, що файл усередині каталогу неможливо прочитати. Третя команда показує, яку ідентичність представляє мережевий шлях агента. Саме на це запитання має відповідати схема з gateway: 401 означає, що запит дійшов до GitHub без облікових даних, які GitHub приймає — їх або немає, або вони недійсні чи прострочені. 200 означає, що запит містить дійсні облікові дані, тому потрібно знати, який саме токен використовується. Якщо агенти працюють без нагляду, у матеріалі контроль витрат AI-агентів на VPS описано ліміти бюджету, які доповнюють ці обмеження доступу.

FAQ

Чи можу я просто довірити моделі не розкривати мої ключі?

Ні, оскільки в цій моделі загроз модель не є атакувальником. Агент читає текст із вебсторінок, репозиторіїв і систем відстеження завдань, а цей текст може містити інструкції. Модель не має надійного способу відрізнити ваші інструкції від тексту, який вона отримала. Будь-який захист, що залежить від правильного рішення моделі, не спрацює, щойно нав’язана інструкція виявиться переконливою. Тому контроль потрібно реалізувати в операційній системі або мережі.

Чи справді змінні середовища настільки небезпечні для секретів агента?

Вони небезпечні з однієї конкретної причини: вони успадковуються. Кожен дочірній процес, який запускає агент, отримує їхню копію, зокрема build script, test runner і будь-який install hook пакета. Змінні також можна прочитати через /proc/<pid>/environ від імені того самого користувача. Тому все, що запускає агент, може прочитати їх без передавання агентом. Файл, який читається безпосередньо в момент використання, за допомогою LoadCredential= або gateway, обмежує доступ цим моментом.

Чи вирішує розміщення секретів у vault проблему само по собі?

Лише частково. Vault вирішує проблему зберігання. Якщо ви розміщуєте цей vault самостійно, його також потрібно окремо захистити, оскільки сервер Vaultwarden зазвичай зламують через його admin token або backup file, а не через зашифровані елементи, які він зберігає. Це не вирішує проблему останнього кроку, коли щось отримує секрет із vault і передає його агенту як змінну середовища. У результаті ви повертаєтеся до початкової ситуації. Важливо, хто виконує підстановку. Якщо секрет отримує агент, секрет опиняється в агента. Якщо підстановку виконує gateway або init system поза процесом агента, агент ніколи не отримує секрет.

Як дізнатися, чи агент уже витік щось назовні?

Зазвичай після події це неможливо визначити напевно, і саме це є аргументом на користь gateway. Без нього докази розподілені між shell history, transcript агента та журналами вихідних з’єднань, які ви, найімовірніше, не зберігаєте. З credential gateway кожне використання облікових даних фіксується одним рядком із ідентифікатором агента та міткою часу. Якщо ви підозрюєте витік, спочатку замініть ключ, а потім проводьте розслідування. Ротація коштує недорого, а впевненість — ні.

Що мінімально потрібно зробити сьогодні?

Перемістіть усі .env файли за межі каталогів, у яких працюють ваші агенти, і створіть окремого непривілейованого користувача для кожного агента. Ці дві зміни займають приблизно десять хвилин і усувають найпоширеніший шлях атаки: агент читає файл з обліковими даними, якому не потрібно було перебувати поруч із кодом. Gateway і короткоживучі токени — це наступний крок, а не перший. Цей самий початковий підхід застосовується до будь-якого середовища виконання агента, зокрема безпечного запуску автономного агента на VPS.

Чи була ця стаття корисною?