Читать 7 мин

Безопасность выхода из macOS: завершите полномочия локального агента

Безопасность выхода из macOS должна отозвать доступ к хранилищу, согласия агента и действия в очереди до того, как ушедший разработчик сможет запустить ещё один вызов.

Безопасность выхода из macOS: завершите полномочия локального агента

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

Особенно важно это для агентов программирования, потому что их полезная работа выходит за пределы компьютера. Они вызывают API, открывают SSH-сеансы, создают тикеты, публикуют пакеты и меняют инфраструктуру. Если полномочия были получены от человека за клавиатурой, они заканчиваются, когда этот человек покидает сеанс macOS. Система должна сохранить свидетельства произошедшего, но не должна сохранять возможность продолжать действия.

Я постоянно вижу одну и ту же ошибку: смешивают три разные вещи, секрет, сохранённый локально, согласие, выданное процессу, и действие, которое уже выполняется. Для них нужны разные правила завершения. Одного общего обработчика quit недостаточно: он срабатывает слишком поздно и не объясняет, что именно нужно остановить.

Выход из системы это граница полномочий, а не событие приложения

Безопасность выхода из macOS означает, что отсутствие интерактивного пользовательского сеанса должно запрещать будущие действия с учётными данными, даже если не каждое приложение завершилось аккуратно. При обычном выходе приложение рабочего стола может получить уведомления о завершении, но безопасность не должна зависеть от того, придут ли эти уведомления, успеют ли обработчики закончить работу и в каком порядке всё произойдёт.

Пользователь может закрыть ноутбук, сменить пользователя, принудительно завершить приложение, потерять питание или начать выход из системы, пока агент ждёт медленный сервер. Операционная система тоже может завершить процессы в порядке, которого ваш код не ожидал. Проектирование в духе «отзовём доступ в applicationWillTerminate» уже оставляет слишком много неопределённости.

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

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

  1. Открыто ли сейчас хранилище для этого пользовательского сеанса?
  2. Относится ли запрос к работающему авторизованному процессу агента в том же сеансе?
  3. Требуют ли эти учётные данные отдельного подтверждения для данного использования?
  4. Не увеличилось ли поколение сеанса из-за выхода из системы или отзыва сеанса?

Четвёртая проверка предотвращает незаметную гонку. Запрос может пройти первые три проверки, задержаться в очереди и попасть к исполнителю уже после начала выхода из системы. Номер поколения, который проверяется ещё раз непосредственно перед передачей учётных данных или запуском SSH, заставляет такой устаревший запрос завершиться отказом.

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

Заблокированное хранилище должно отказывать до начала очистки

Шлюз хранилища должен закрываться первым, причём с точки зрения исполнителя это должно происходить синхронно. После закрытия шлюза нельзя начинать передачу новых HTTP-учётных данных или новую SSH-аутентификацию. Очистка может идти следом, но именно она не должна обеспечивать безопасность.

Порядок кажется очевидным, пока приложение не начинает поддерживать очередь запросов. Типичный сбой выглядит так: агент отправляет пять вызовов развёртывания, интерфейс начинает очистку при выходе, приложение убирает видимую карточку сеанса, а поток-обработчик извлекает из очереди четвёртый вызов со ссылкой на учётные данные, которую получил раньше. Приложение выглядит вышедшим из системы, но действие всё равно достигает внешнего сервиса.

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

Достаточно небольшой структуры:

AuthorityState {
  sessionID: 6C17...
  generation: 41
  vaultOpen: true
  logoutStarted: false
}

execute(request):
  lease = authority.issueLease(request, generation: 41)
  vault.perform(request, lease)

beginLogout():
  authority.logoutStarted = true
  authority.vaultOpen = false
  authority.generation = 42
  cancelPendingRequests()

vault.perform должен отклонять пропуск, если его поколение больше не совпадает с текущим. Второе сравнение нужно выполнять как можно ближе к моменту, когда хранилище передаёт HTTP-заголовок, запускает SSH-помощник или подписывает запрос. Проверка только при добавлении запроса в очередь оставляет достаточно большую гонку, чтобы она имела значение.

Для приложения macOS, которое защищает хранилище через Secure Enclave и Touch ID, аппаратно защищённый доступ полезен: он даёт шлюзу чётко определённого локального владельца. Но необходимость в состоянии сеанса не исчезает. Биометрическое подтверждение, принятое до выхода из системы, не может авторизовать вызов после него.

Шлюз хранилища Sallyport следует этому правилу: пока хранилище заблокировано, каждое действие получает отказ. Такая фиксированная граница лучше списка исключений для завершения работы, потому что списки исключений разрастаются, и в итоге никто не может объяснить, какой вызов всё ещё проходит мимо них.

Активное согласие относится к одному процессу и одному сеансу

Согласие пользователя нужно привязывать к конкретному процессу агента, его полномочиям подписи кода и текущему сеансу входа в macOS. Оно никогда не должно означать: «этот аккаунт когда-то одобрил этот инструмент».

Идентичность процесса это больше, чем его идентификатор. Идентификаторы процессов переиспользуются. Вызывающий компонент, который записывает только PID 4812, после достаточного количества перезапусков может случайно разрешить действия несвязанному процессу. Записывайте вместе с PID время запуска процесса и его идентичность подписи кода. Если процесс запущен терминалом или интеграцией редактора, храните достаточно информации о родителе, чтобы объяснить маршрут в записи согласия, но не делайте родословную единственным сигналом доверия. Оболочки и менеджеры процессов постоянно её меняют.

Карточка согласия должна начинаться с полномочий подписи, потому что так разработчик получает понятный вопрос: «Хочу ли я, чтобы этот подписанный процесс агента действовал в этом сеансе?» Имя пакета или произвольная строка, переданная через MCP, на этот вопрос не отвечает.

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

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

Есть полезное различие, которое многие реализации размывают:

  • Разблокировка хранилища разрешает локальному шлюзу рассматривать действия.
  • Авторизация сеанса разрешает одному идентифицированному процессу агента отправлять действия.
  • Согласие для отдельного использования разрешает одну конкретную операцию с учётными данными.

Выход из системы делает недействительными все три уровня, но свидетельства и момент действия у них различаются. Хранилище закрывается немедленно. Согласия сеанса становятся недействительными одной группой. Запросы на отдельное использование завершаются отказом по одному, потому что изменилось поколение сеанса. Если свести всё к одному Boolean, будет трудно понять по аудиту, почему вызов прошёл или получил отказ.

Работающие инструменты требуют отмены и честной фиксации неопределённости

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

Разделяйте действие на состояния с понятным практическим смыслом:

queued -> authorized -> dispatched -> response received
                    \-> cancelled

Запрос в состоянии queued ещё не покинул Mac. Удалите его из очереди и сообщите cancelled_before_dispatch. Авторизованное действие может удерживать только короткоживущий внутренний пропуск. Сделайте пропуск недействительным до отправки и сообщите revoked_before_dispatch, если рабочий поток дошёл до него слишком поздно.

Отправленное действие отличается от них. Удалённая система могла получить его, даже если локальный процесс не получил ответа. Не сообщайте, что оно отменено, только потому, что вы закрыли сокет или убили помощник. Запишите logout_during_dispatch, сохраните идентификатор запроса, если его предоставляет удалённый протокол, и сообщите пользователю, что результат неизвестен, пока он не проверит удалённую систему.

HTTP-запросы требуют особого внимания. Закрытие клиентского соединения может остановить передачу до того, как сервер прочитает её, а может произойти после фиксации изменения сервером. Токены идемпотентности уменьшают ущерб при повторной попытке, но не превращают неопределённый запрос в отменённый. Для операций, создающих внешний ресурс, отправляйте идентификатор идемпотентности, который удалённый API действительно поддерживает, а затем записывайте этот идентификатор, не сохраняя учётные данные.

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

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

Фоновое сохранение процесса меняет модель угроз

Проверяйте систему после сомнительного вызова
Команда sp audit verify проверяет целостность цепочки офлайн поверх шифротекста, не открывая хранилище.

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

Не устанавливайте привилегированный помощник или задание launchd в системном домене только ради сохранения агента после выхода. Такой подход популярен, потому что долгие задачи кажутся надёжнее. Но он отделяет путь действия от человека, который его одобрил, и часто расширяет доступ за пределы исходного пользовательского сеанса.

Если команде действительно нужно продолжать работу после ухода разработчика, создайте для неё отдельное место. Используйте явную сервисную идентичность, учётные данные с узкими полномочиями и сроком действия, определённого владельца, аудит-лог и процедуру отмены, которой сможет воспользоваться другой оператор. Сделайте передачу полномочий видимой. Локальный агент не должен молча получать такую роль.

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

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

Логи должны переживать полномочия, но не превращаться во второе хранилище секретов

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

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

Аудит-лог с хеш-цепочкой даёт свойство, которого нет у обычного журналирования приложения: офлайн-проверка позволяет обнаружить удалённые или изменённые записи. Это полезно после спорного развёртывания или предполагаемого локального взлома, но лог не становится правдивым автоматически. Он доказывает непрерывность содержащихся в нём записей, но не доказывает, что вредоносный процесс не прекратил журналирование перед действием.

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

Sallyport хранит журналы сеансов и активности в одном зашифрованном аудит-логе с хеш-цепочкой, доступном только для записи, а sp audit verify может офлайн проверить эту цепочку поверх шифротекста. Команда может проверить непрерывность записей, не открывая хранилище лишь для того, чтобы узнать, отозван ли запуск при выходе из системы.

Полезный вопрос для аудита звучит конкретно: «Какой процесс имел полномочия, какой путь к учётным данным он запросил и что шлюз знал в момент ухода пользователя?» Огромный отладочный лог редко даёт на него ответ.

Проверяйте гонки, а не аккуратный выход

Требуйте согласие для каждого ключа
Помечайте важные ключи для подтверждения одним нажатием или через Touch ID при каждом использовании.

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

Начните с конечной точки под вашим контролем, которая принимает запрос, записывает факт его получения, а затем задерживает ответ. Отправьте действие через шлюз агента и запустите выход из системы после того, как шлюз пометил действие как отправленное, но до возврата ответа. При следующем входе сравните локальное состояние аудита с записью конечной точки. Ожидаемый результат не всегда «отменено». Это должно быть точное состояние, например logout_during_dispatch, вместе с идентификатором запроса для дальнейшего расследования.

Отдельно проверьте работу в очереди. Приостановите поток после получения элемента из очереди, но до запроса пропуска действия у хранилища. Начните выход из системы, возобновите поток и убедитесь, что он получает результат отзыва, а не отправляет что-либо. Этот тест выявляет распространённую ошибку, когда авторизацию проверяют только при добавлении запроса в очередь.

Затем проверьте неприятные случаи:

  • Переключитесь на другого пользователя, пока исходный сеанс остаётся открытым.
  • Заблокируйте экран, разбудите компьютер и проверьте выбранную вами политику для этого перехода.
  • Принудительно завершите агент, пока запрос ждёт согласия, а затем запустите новый процесс с повторно использованным PID, если тестовый стенд может это организовать.
  • Прервите работу шлюза во время завершения и проверьте, указывает ли аудит-запись на незавершённую работу.
  • Отправьте SSH-команду, которая запускает удалённую работу, а затем проверьте поведение на удалённой стороне вместо того, чтобы доверять коду выхода локального помощника.

В macOS проверяйте домены запуска во время подготовки теста, чтобы понимать, что именно вы запустили:

uid="$(id -u)"
launchctl print "gui/$uid" | grep -E "(agent-gateway|your-test-label)"

Вывод зависит от установленных заданий и версии macOS, но в нём должно быть соответствующее задание внутри текущего домена gui/<uid>. Если тестовое задание оказалось в системном домене, результат выхода мало говорит о поведении обычного приложения рабочего стола в пользовательском сеансе.

Не автоматизируйте настоящий выход из системы сначала на основном аккаунте разработчика. Используйте отдельную локальную учётную запись, отдельные API-учётные данные и конечную точку, где можно проверить каждый запрос. Тесты выхода могут уничтожить несохранённую работу и оставить удалённое состояние изменённым лишь наполовину. Это не повод их пропускать, а повод перестать считать их обычным модульным тестом.

Удалённые учётные данные должны ограничивать ущерб от позднего вызова

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

Локальный отзыв не может задним числом отозвать bearer-токен, который удалённый сервис уже принял. Проектирование удалённых учётных данных ограничивает ущерб, если шлюз слишком поздно обнаружил запрос, приложение завершилось с ошибкой или компьютер был скомпрометирован до выхода из системы.

Предпочитайте удалённые учётные данные с узкими разрешениями и коротким сроком действия, если целевая система это поддерживает. Используйте отдельные учётные данные для разных окружений. Агент, которому разрешено обновлять развёртывание в staging, не должен получать учётные данные для удаления производственных данных только потому, что обе конечные точки используют одного поставщика API.

Для SSH используйте отдельную удалённую учётную запись и ограничьте её возможности. Если команда должна запускать долгую задачу, сделайте владельца и механизм отмены этой задачи видимыми на удалённой стороне. Личная SSH-идентичность с широким доступом к оболочке удобна, пока не приходится объяснять, почему задача продолжила работу после исчезновения локального родительского процесса.

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

Такой дизайн даёт выходу из системы чёткую задачу: закрыть хранилище, сделать активные разрешения недействительными, отклонить устаревшие пропуски, остановить ожидающую работу и зафиксировать неопределённость для уже отправленных действий. Он не может обещать отменить произошедшее в интернете. Зато он может не дать следующему вызову воспользоваться полномочиями человека, которого уже нет.

Условие завершения должно легко объясняться

Сформулируйте правило так, чтобы уставший разработчик мог применить его во время инцидента: когда пользовательский сеанс macOS заканчивается, ни один локальный процесс агента не может начать новое действие с учётными данными от имени этого пользователя.

Из этого правила следует всё остальное. Шлюз хранилища закрывается до очистки. Согласия умирают вместе с процессом и сеансом, которые их получили. Запрос в очереди теряет свой пропуск. Отправленный запрос получает честное неизвестное состояние, пока удалённая система не подтвердит результат. Логи остаются доступными для проверки, а секреты и исполняемые полномочия исчезают.

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

Вопросы и ответы

Блокировка экрана Mac равнозначна выходу из системы с точки зрения безопасности агента?

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

Что должно произойти, если агент отправляет запрос во время выхода из системы?

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

Должен ли одобренный агент оставаться одобренным после повторного входа пользователя?

Нет. Согласие относится к одному процессу агента в рамках одного пользовательского сеанса, а не к аккаунту в целом. При новом входе нужно заново проверить идентичность процесса и принять новое решение об авторизации.

Должны ли аудит-логи сохраняться после выхода из macOS?

Безопасная система должна сохранять свидетельства, но не полномочия. Храните зашифрованную запись о сеансе, согласиях, отклонённых вызовах и отзыве при выходе из системы, но не восстанавливайте по ней исполняемые учётные данные или состояние согласий.

Можно ли безопасно остановить уже выполняющуюся SSH-команду или HTTP-запрос при выходе из системы?

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

Что делать, если пользовательский процесс macOS переживает выход из системы?

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

Должна ли автономная работа агента продолжаться после выхода разработчика из системы?

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

Как агент может использовать API- или SSH-учётные данные, не сохраняя их после выхода из системы?

Да, если приложение работает на том же Mac и само владеет учётными данными. Оно может отозвать полномочия при выходе из системы, не помещая секреты в процесс агента. Sallyport хранит секреты в зашифрованном хранилище и само выполняет HTTP- или SSH-действие, поэтому агент получает результат, а не пригодные для повторного использования учётные данные.

Что должен записывать аудит-лог при завершении локальных полномочий агента?

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

Подходит ли выход из macOS для долгих производственных задач агента?

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

Sallyport

Sallyport выполняет API-вызовы и SSH-команды за вашего ИИ-агента. Ключи остаются в локальном хранилище на вашем Mac; вы подтверждаете каждый запуск, и каждое действие попадает в запечатанный журнал.

© 2026 Sallyport · Открытый код по лицензии Apache-2.0 · Oleg Sotnikov