Локальным AI-агентам и CI-задачам нужны разные модели доступа
Локальным AI-агентам и CI-задачам нужны разные модели доступа: одобрение человека, идентичность рабочей нагрузки и раскрытие учетных данных создают разные риски.

Рабочая станция разработчика и раннер сборки могут выполнять команды shell, обращаться к API и отправлять код. Если считать их одной средой безопасности, команда рано или поздно положит учетные данные для развертывания туда, где за ними не сможет нормально отчитаться ни человек, ни сервис.
Локальные AI-агенты работают в среде с участием человека. Человек видит запрос, проверяет diff, отклоняет необычное действие и останавливает процесс, который начал вести себя подозрительно. CI-задачи запускаются из-за события: отправки изменений, pull request, тега, запланированного запуска или ручного запуска. Задаче нужны полномочия, которые можно проверить машинным способом и связать с этим событием. Она не может ждать, пока разработчик одобрит каждый вызов, и не должна получать постоянный доступ разработчика лишь потому, что workflow запустился.
Это различие не теоретическое. От него зависит, получит ли агент секрет, будет ли токен существовать несколько минут или несколько месяцев, что должна содержать запись аудита и сможет ли скомпрометированная зависимость превратиться в инцидент в production.
Присутствие человека меняет смысл одобрения
Одобрение на рабочей станции может быть рественным средством защиты, потому что человек присутствует и оценивает конкретное действие. Запрос должен достаточно ясно описывать вызывающую сторону, иначе решение не имеет смысла. «Агент хочет выполнить HTTP-запрос» дает мало информации. «Процесс, подписанный этим центром сертификации, запущенный в текущем сеансе, хочет использовать учетные данные для развертывания в production» уже позволяет оператору принять или отклонить запрос.
У этой модели есть четкая граница: одобрение означает, что человек берет на себя ответственность за работающий процесс. Это не замена идентичности. Если вредоносный процесс может выдать себя за доверенный, скрыть назначение или повторно использовать одобрение после изменения задачи, запрос превращается в формальность.
На компьютере разработчика мне нужны три отдельных факта:
- защищенное хранилище остается закрытым, пока локальный пользователь его не разблокирует;
- первый запрос нового процесса требует решения для сеанса;
- для чувствительных учетных данных можно запрашивать решение при каждом использовании.
Эти меры отвечают на разные вопросы. Блокировка определяет, может ли вообще произойти какое-либо действие. Авторизация сеанса показывает, может ли этот процесс действовать в рамках текущего запуска. Одобрение каждого вызова определяет, настолько ли важны конкретные учетные данные, чтобы не разрешать их повторное использование без проверки. Команды часто сводят все три меры к одной кнопке «разрешить доступ агенту», а потом обнаруживают, что кнопка одобрила гораздо больше, чем разработчик предполагал.
Локальный агент также должен получать результаты, а не сами учетные данные. Если ему нужно обратиться к API, компонент за пределами агента может подставить учетные данные, выполнить запрос и вернуть ответ. Тогда prompt injection в агенте не сможет просто вывести API-ключ в терминал, патч или историю чата. Маскирование секрета после того, как он уже попал в контекст агента, не дает такой же защиты. Агент может закодировать его, отправить на другой хост или использовать в запросе до того, как секрет увидит система маскирования.
Sallyport использует эту интерактивную модель в macOS: его хранилище закрыто абсолютным шлюзом, а авторизацию можно запрашивать для сеанса процесса или при каждом использовании выбранных учетных данных. Это разумно, когда разработчик находится рядом. Для автономного раннера такой механизм был бы неправильным примитивом.
CI-задаче нужна идентичность, которую можно проверить
CI-задача не может по запросу предоставить намерение человека. Ей нужна идентичность рабочей нагрузки, то есть идентичность, полученная из проверяемых фактов о задаче, а не из секрета, скопированного в окружение задачи.
Для workflow развертывания такими фактами обычно служат издатель CI, репозиторий или проект, коммит или ref, идентичность workflow, окружение и предполагаемая аудитория. Целевой сервис проверяет подписанное утверждение и обменивает его на короткоживущие учетные данные. В этом и состоит польза федерации OIDC: раннер доказывает происхождение задачи, не храня многоразовый облачный ключ в хранилище секретов.
GitHub Actions описывает этот подход через конечную точку токена OIDC и разрешение id-token: write. Название разрешения легко понять неправильно. Оно позволяет workflow запросить токен идентичности, но само по себе не дает права на развертывание. Облачная роль или целевой сервис по-прежнему должны отклонять токены, у которых издатель, аудитория, subject или другие утверждения не соответствуют нужному workflow.
Именно на второй части часто возникают ошибки. Роль, принимающая любой токен из репозитория, передает слишком много полномочий каждому подходящему workflow этого репозитория. Предпросмотр документации, workflow выпуска и развертывание в production не должны становиться равноценными только потому, что используют одну систему контроля версий.
Используйте утверждения так, чтобы роль описывала один класс задач. Точный синтаксис утверждений зависит от CI-провайдера и облака, но политика должна отвечать на простые вопросы:
- Какой репозиторий может запросить эту роль?
- Какой файл workflow или защищенное окружение может ее запросить?
- Какая ветка, тег или условие выпуска дают право на запрос?
- Какую аудиторию должно называть утверждение?
- Как долго выданные учетные данные могут оставаться действительными?
Не пишите условия политики, которые не проверили на реальном токене. Выведите утверждения в безопасной тестовой среде, сравните их с политикой доверия и проверьте случаи отказа. Успешные развертывания тестируют почти все, а опасный путь, pull request из ненадежной ветки, оставляют на уровне предположений.
Секреты и идентичности решают разные задачи
Секрет доказывает владение. Утверждение идентичности сообщает, какая рабочая нагрузка запросила доступ. В итоге оба механизма могут выдать токен предъявителя, но пути к отказу у них совершенно разные.
У сохраненного секрета CI обычно нет сведений о том, почему задача его получила. Если workflow может прочитать DEPLOY_TOKEN, измененный скрипт, скомпрометированный action, вредоносный путь pull request или команда вывода в лог смогут использовать этот токен везде, где позволяют его права. Ротация ограничивает срок полезности секрета, но не ограничивает контекст каждого использования.
Короткоживущая федерация не делает CI безопасным сама по себе. Скомпрометированная задача все еще может пользоваться действительным токеном в течение срока его действия. Выигрыш в меньшем радиусе поражения: атакующему нужно запустить подходящую задачу, пройти правила издателя и утверждений и действовать до истечения срока учетных данных. Кроме того, можно отозвать или изменить принимающую роль, не разыскивая каждую копию секрета.
Не путайте короткий срок действия с малым уровнем привилегий. Токен, действительный десять минут и позволяющий удалить все базы данных production, все равно недопустим. Ограничение времени уменьшает длительность доступа, а область разрешений ограничивает ущерб. Нужны обе меры.
У локального доступа агента обратная проблема. Разработчик может использовать один и тот же инструмент во множестве репозиториев и задач, поэтому единый широкий API-ключ становится привлекательной целью для скомпрометированного агента. Самая безопасная локальная схема оставляет этот ключ за пределами агента и разрешает только конкретный API-вызов или SSH-команду, одобренные пользователем. Если внешний сервис поддерживает токены с точными правами, используйте их и там. Защищенное хранилище не сможет исправить чрезмерные права токена после того, как запрос покинет компьютер.
Опасное удобство, один токен для обоих миров
Использовать личный токен разработчика в CI популярно, потому что это быстро разблокирует остановившееся развертывание. Но это один из худших способов стереть ответственность.
Личный токен часто имеет больше прав, чем нужно конвейеру. Он может принадлежать сотруднику, который сменит команду, уйдет из компании, будет использовать токен с ноутбука, а его права будут определяться личным членством, а не обязанностями по развертыванию. Когда такой токен появляется в CI, журнал аудита может показать, что действовал токен, но не сможет правдиво сказать, исходило ли действие от разработчика или от задачи выпуска.
Встречается и обратная ошибка. Команды открывают локальному агенту секрет развертывания CI, чтобы агент мог «проверить то же самое». Так интерактивная генерация кода получает возможность развертывать в production без участия человека, часто с меньшим числом проверок, чем в workflow выпуска. Реагировать на инцидент становится мучительно сложно: токен использовал агент, shell-скрипт или скопированное значение утекло в другой инструмент?
У каждого окружения должна быть собственная граница авторизации. Локальный разработчик может иметь интерактивный отзывный доступ к endpoint разработки. Задача выпуска может получать федеративную роль, ограниченную защищенным окружением production. У задачи pull request может вообще не быть прав на запись. Это не неудобства, которые нужно сглаживать. Позже именно эти различия помогут объяснить, почему действие было разрешено.
Полезное правило именования: имя учетных данных должно показывать и действующую сторону, и назначение. ci-release-prod-deploy говорит проверяющему гораздо больше, чем deploy-token. Еще лучше, если CI-задача вообще не хранит токен с таким именем. Она запрашивает роль, связанную с идентичностью, а политика доверия этой роли описывает то же назначение.
Состояние раннера превращает временный доступ в остаточные данные
Задача может использовать временные учетные данные и все равно оставить после себя постоянную проблему. Чаще всего секреты утекают в логи, трассировку shell, кэшированные домашние каталоги, слои Docker, артефакты рабочего каталога и файлы, созданные сторонними action.
К собственным раннерам нужно относиться особенно осторожно, потому что они сохраняют состояние между задачами. Задача, которая получает ненадежный код, может подменить исполняемый файл, изменить общий кэш, прочитать оставшийся рабочий каталог или ждать, пока привилегированный workflow снова использует этот хост. Изоляция по имени репозитория не помогает, если задачи используют одну учетную запись операционной системы, сокет контейнеров или файловую систему.
Эфемерные раннеры устраняют большой класс остаточных данных, потому что после задачи раннер исчезает. Но они не отменяют необходимость контролировать входные данные workflow. Привилегированная задача, запускающая скрипты из ненадежного pull request, все равно передает полномочия недоверенному коду, даже на новом компьютере.
GitHub Actions предупреждает, что pull_request_target выполняется в контексте базового репозитория и может получать секреты или права записи. У этого события есть законное применение: сопровождающим может понадобиться поставить метку или оставить комментарий к pull request из fork. Проблема возникает, когда workflow, запущенный этим событием, извлекает head-коммит pull request и выполняет его скрипты. В этот момент workflow совмещает доверенные учетные данные с кодом, которым управляет атакующий.
Пусть схема остается простой:
- выполняйте код из ненадежного pull request без полномочий на развертывание;
- оставляйте доступ к защищенному окружению только для проверенных ref и контролируемых путей workflow;
- фиксируйте сторонние action на неизменяемых ссылках на коммиты, если это допускает ваш процесс;
- не помещайте секреты в кэши, артефакты и диагностический вывод;
- уничтожайте чувствительные экземпляры раннера после задачи.
Первый и четвертый пункты предотвращают больше реальных инцидентов, чем сложные соглашения об именах токенов. Даже идеально ограниченные учетные данные утекут, если shell-команда выведет их в лог или артефакт включит файл конфигурации.
Идентичность развертывания должна быть видна в одном файле
Workflow должен явно показывать запрос внешних полномочий. В этом примере GitHub Actions идентичность OIDC запрашивается только в задаче развертывания, а для нее объявляется окружение production. Сохраненных облачных учетных данных здесь намеренно нет.
name: deploy
on:
push:
tags:
- "v*"
permissions:
contents: read
id-token: write
jobs:
deploy:
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@<full-commit-sha>
- name: Request deployment identity
run: |
token=$(curl -sS \\
-H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \\
"${ACTIONS_ID_TOKEN_REQUEST_URL}&audience=deploy.example.internal")
test -n "$token"
- name: Deploy
run: ./scripts/deploy.sh
На практике для обмена обычно используют action или облачный CLI, а не сохраняют ответ в переменной shell. Здесь важна форма запроса: GitHub предоставляет одноразовый токен и URL, workflow запрашивает конкретную аудиторию, а затем целевой сервис решает, может ли эта идентичность принять роль. Раннер не должен считать полученный токен идентичности универсальными учетными данными API.
Политика доверия на стороне облака должна отклонять токен, если его утверждения не подтверждают именно этот контекст выпуска. Не копируйте пример провайдера и не останавливайтесь на этом. Примеры часто начинаются с широких разрешений, чтобы подходить многим пользователям. До того как роль получит возможность менять production, ужесточите ограничения по репозиторию, окружению, ветке или тегу и аудитории.
Если целевой сервис не умеет напрямую проверять OIDC, поставьте перед ним небольшой брокер учетных данных. Брокер проверяет утверждение CI, сопоставляет утверждения с узким набором действий, выдает временные учетные данные целевого сервиса и записывает это сопоставление в журнал. Не решайте проблему, помещая постоянный секрет целевого сервиса в каждый workflow, которому он нужен.
SSH особенно ясно показывает разницу
SSH-ключи плохо подходят на роль общей мебели CI. Закрытый ключ, скопированный в секрет CI, позволяет аутентифицироваться любой задаче, которая может его прочитать, а открытая часть редко сообщает целевому сервису, какая ревизия репозитория запросила подключение. Принудительные команды, ограничения источника и отдельные учетные записи могут уменьшить ущерб, но базовая идентичность все равно остается многоразовым закрытым ключом.
Для локальной работы SSH может требовать интерактивного шлюза, потому что разработчик видит, что агент хочет подключиться к конкретному хосту. Закрытый ключ должен оставаться в защищенном хранилище, а агент должен запрашивать конкретную команду, а не получать прямой доступ к ключу. Хост по-прежнему обязан сам контролировать права учетной записи и ограничения команд. Локальное одобрение контролирует точку запуска, но не делает опасную удаленную команду безопасной.
Для CI, если центр сертификации SSH и целевые серверы это поддерживают, лучше использовать короткоживущие SSH-сертификаты. Задача с помощью федерации рабочей нагрузки запрашивает сертификат с коротким сроком действия, ограниченным principal и, возможно, принудительной командой. Целевой сервер проверяет центр сертификации, а не принимает навсегда скопированный закрытый ключ.
Если сертификаты недоступны, используйте отдельную учетную запись развертывания и отдельный закрытый ключ для каждого класса развертываний. Ограничьте эту учетную запись в authorized_keys и на сервере. Минимальное ограничение выглядит так:
command="/usr/local/bin/receive-release",no-port-forwarding,no-agent-forwarding,no-X11-forwarding ssh-ed25519 AAAA... ci-release
Эта строка не позволяет ключу открыть произвольную интерактивную оболочку и заставляет сервер запускать одну программу приема. Сама по себе она не определяет исходный репозиторий. Если решение о выпуске зависит от контекста репозитория, дополните ее брокерным короткоживущим ключом или другим проверенным сигналом рабочей нагрузки.
Не устанавливайте StrictHostKeyChecking=no в CI-скрипте ради исправной работы SSH. Это отключает проверку идентичности сервера именно там, где раннеры особенно легко перенаправить. Передавайте известные ключи хостов через контролируемый механизм, планово ротируйте их и завершайте задачу с ошибкой, если проверка хоста неожиданно изменилась.
Записи аудита должны отвечать на разные вопросы
Запись, в которой сказано лишь «развертывание выполнено», это операционный вывод, но недостаточное свидетельство безопасности. Нужно восстановить, кто или что получило полномочия, какой запрос оно выполнило и не изменялась ли сама запись после этого.
Для интерактивного локального агента записывайте идентичность локального процесса, начало сеанса, решение об одобрении, метку учетных данных или класс действия, назначение, время запроса и результат. Не записывайте значения секретов и полные чувствительные полезные данные. Подпись процесса или центр подписи кода полезнее произвольного имени процесса, потому что имя легко скопировать.
Для CI записывайте CI-провайдера, ID запуска, репозиторий, ссылку на workflow, SHA коммита, инициатора запуска, если он доступен, класс раннера, subject и аудиторию OIDC, целевую роль, безопасные для хранения параметры действия и результат. Это позволяет расследованию отличить выпуск по тегу от ручного повторного запуска и доверенный workflow от случайно выданного широкого разрешения.
Храните журналы авторизации отдельно от журналов приложения. Журналы приложения могут изменяться, отбираться выборочно или удаляться в рамках обычной эксплуатации. Цепочка авторизации должна поддерживать добавление записей без изменения предыдущих и независимую проверку. Связывание хешей выявляет изменения в последовательности, если сохранять цепочку и сопоставлять ее с ожидаемыми записями. Но оно не мешает атакующему блокировать будущие записи, поэтому, если позволяет архитектура, отправляйте записи с потенциально скомпрометированной машины во внешнюю систему.
Sallyport создает журналы сеансов и отдельных действий в зашифрованном журнале с цепочкой хешей, недоступном для записи со стороны приложения, а sp audit verify проверяет цепочку офлайн без ключа хранилища. Это полезное свидетельство для действий интерактивного агента. CI должен формировать эквивалентный журнал, связанный с рабочей нагрузкой, в системах, которые выдают и принимают его идентичности.
Усталость от запросов на одобрение, это ошибка локального дизайна
Одобрение каждого вызова может защищать важные локальные учетные данные, но запрос при каждом безобидном чтении приучает людей нажимать кнопку, не читая текст. Когда это происходит, контроль превращается в ритуал, и атакующему остается дождаться обычной работы.
Используйте одобрение каждого вызова для действий, последствия которых трудно обратить: изменений production, изменений DNS, администрирования всей организации, принудительной отправки изменений в систему контроля версий или команд на чувствительных хостах. Для повторяющихся действий с низким риском используйте авторизацию сеанса, которую разработчик может разумно делегировать на один запуск агента. Шлюз хранилища должен оставаться отдельным: заблокированный компьютер запрещает все действия, даже если старое одобрение сеанса еще действует.
У CI есть собственная форма усталости: ручные этапы одобрения, которые появляются в каждой задаче и получают согласие просто потому, что сроки выпуска становятся важнее проверки. Одобрение защищенного окружения может быть уместно для развертывания в production, но оно должно относиться к четко определенному артефакту выпуска и целевому ресурсу. Такое одобрение не должно компенсировать workflow, принимающий ненадежный код, роль с широкими утверждениями или раннер с неизвестным состоянием.
Полезный контрольный вопрос звучит неприятно: что сможет запуститься, если целый месяц это одобрение нажимать автоматически? Для локальных агентов уменьшайте набор одобряемых действий, пока ответ не станет приемлемым. Для CI уберите одобрение из обычных машинных действий и привяжите полномочия к идентичности задачи.
Разделите пути до следующего запроса учетных данных
Когда агент или конвейер запрашивает доступ, сначала классифицируйте вызывающую сторону, а уже потом выбирайте механизм секретов. Присутствует ли человек и может ли он проверить действие? Является ли вызывающая сторона повторяемой рабочей нагрузкой с утверждениями, которые можно проверить? Может ли она получить узкий результат вместо учетных данных? Соответствуют ли права целевого ресурса одной конкретно названной задаче?
Если вызывающая сторона, локальный агент, защищайте учетные данные за пределами его контекста, сохраняйте понятный путь одобрения и записывайте каждое действие в форме, удобной для проверки разработчиком. Если это CI, используйте короткоживущую федерацию, связывайте роли с утверждениями workflow, изолируйте раннер и записывайте факты о рабочей нагрузке, которые разрешили вызов.
Не позволяйте общему токену стирать эту границу только потому, что сегодня так быстрее. Следующий инцидент заставит вас восстанавливать ее одновременно с расследованием: действовал человек, агент или раннер.
Вопросы и ответы
Почему AI-агенты для программирования и CI-конвейеры должны использовать разные учетные данные?
Локальный агент работает в сеансе разработчика, где человек может проверить запрос и остановить процесс. CI-задача запускается без участия человека после события в системе контроля версий, поэтому ее права должны исходить из точно ограниченной идентичности рабочей нагрузки и истекать вместе с задачей.
Можно ли использовать один и тот же процесс одобрения для CI и локальных агентов?
Нет. Запрос на одобрение от человека подтверждает лишь то, что кто-то нажал кнопку в определенный момент. Он не описывает репозиторий, коммит, workflow или целевую облачную роль задачи. Для интерактивной локальной работы используйте одобрение, а для CI, авторизацию, связанную с рабочей нагрузкой.
Допустимы ли долгоживущие API-ключи в CI?
Обычно это неудачный компромисс. Долгоживущий секрет превращает любой скомпрометированный раннер, утечку из лога, архив кэша или вредоносную зависимость в постоянный путь к доступу. Лучше использовать короткоживущие токены, выпущенные на основании утверждения OIDC с ограничениями по репозиторию, ссылке, workflow и аудитории.
Что такое идентичность рабочей нагрузки OIDC в CI?
Токен рабочей нагрузки OIDC, это утверждение CI-провайдера о выполняющейся задаче. Облачный сервис или сервис секретов проверяет это утверждение и выдает короткоживущие учетные данные для конкретной роли, вместо того чтобы заставлять задачу хранить постоянный облачный секрет.
Должен ли локальный AI-агент когда-либо видеть API-ключ?
По возможности агент вообще не должен получать учетные данные. Дайте локальному агенту интерфейс действий, который выполняет запрос за пределами контекста агента и возвращает результат, а исходные API- или SSH-материалы оставьте в защищенном локальном хранилище.
Как не дать CI-задаче получить доступ не к тому окружению?
Нет. Workflow должен явно указывать каждый внешний ресурс, который ему нужен, а у каждого ресурса должны быть собственные идентичность и права. Универсальный токен развертывания удобен до тех пор, пока скомпрометированная сборка документации не получает возможность развернуть изменения в production.
Какой главный риск связан с собственными CI-раннерами?
Считайте состояние раннера опасными остаточными данными. Для чувствительных задач используйте эфемерные раннеры, фиксируйте версии зависимостей, не восстанавливайте каталоги с секретами, очищайте логи и исходите из того, что привилегированную задачу можно обманом заставить выполнить код из ее рабочего каталога.
Как провести аудит учетных данных CI?
Сначала проверьте источник учетных данных. Затем изучите аудиторию, утверждение о репозитории или проекте, ограничение по ветке или окружению, срок действия и права целевого ресурса. Если вы не можете объяснить назначение каждого поля, токену доверяют больше, чем заслуживает задача.
Что должен содержать журнал аудита действий агента?
Разделяйте записи. В журнале интерактивного сеанса должны быть указаны локальный процесс и каждое одобренное действие. Записи CI должны связывать каждое действие с идентификатором запуска у провайдера, ревизией репозитория, workflow, классом раннера и выданной ролью. Простое сообщение об успехе не считается журналом аудита.
Может ли Sallyport работать внутри Linux-раннера CI?
Используйте управляемую облачную идентичность, отдельный брокер развертывания или узкую сервисную учетную запись с короткоживущей федерацией. Sallyport рассчитан на интерактивную сторону этого разделения в macOS, а не на безголовый сервис учетных данных для CI.