# Secure Enclave и Touch ID для защиты секретов AI-агента

AI-агент программирования должен уметь запросить действие, ни разу не получив учётные данные, которые его разрешают. Именно вокруг этого свойства стоит строить защиту. Secure Enclave и Touch ID могут обеспечить часть такой защиты на Mac, но только если секрет остаётся за границей, которую агент не способен прочитать.

Сообщение на Mac о том, что агент может получить доступ к «GitHub», ещё не является системой безопасности. Скопированный токен `ghp_...`, экспортированная переменная `AWS_SESSION_TOKEN` или закрытый SSH-ключ, вставленный в вызов инструмента, дают агенту постоянные полномочия. Поздующее окно Touch ID уже не сможет их отозвать. Сложность не в шифровании строки. Сложность в том, чтобы не передавать строку программе, которая пересылает произвольный текст.

Я не раз видел эту ошибку в разных вариантах: токен лежит в `.env`, помощник для учётных данных выводит пароль, закрытый ключ подключён к контейнеру, а затем агенту говорят «используй любые доступные учётные данные». Каждый такой выбор кажется временным. Но в каждом случае секрет попадает в рабочий набор агента.

## Secure Enclave не делает bearer-токен безопасным для передачи

Secure Enclave может защищать криптографические операции, но не делает bearer-токен безвредным после того, как процесс его прочитал. Именно это различие определяет, защищает ли Touch ID секрет разработчика или лишь добавляет формальное окно подтверждения перед утечкой.

Apple описывает Secure Enclave как изолированное оборудование, которое может создавать и использовать закрытые ключи, не раскрывая их содержимое главному процессору. Важны и задокументированные ограничения: закрытые ключи Secure Enclave создаются внутри него, не могут импортировать существующие закрытые ключи в открытом виде и поддерживают определённые операции подписи и согласования ключей P-256. API-токен к таким закрытым ключам не относится. Обычно это непрозрачная строка, которую удалённый сервис принимает от любого предъявившего её. В Apple Developer Documentation, в разделе «Protecting keys with the Secure Enclave», ясно описаны и изоляция, и её ограничения.

Здесь важно разделять три объекта, которые часто ошибочно считают одним и тем же:

- Закрытый ключ Secure Enclave - неэкспортируемый ключевой материал для ограниченного набора криптографических операций.
- Элемент Keychain - зашифрованные данные приложения, доступ к которым macOS может ограничить.
- Bearer-учётные данные - копируемое значение, открывающее доступ везде, где сервис его принимает.

Если считать эти объекты синонимами, получаются плохие архитектурные решения. Ключ Secure Enclave может подписать запрос, не раскрывая себя. Элемент Keychain может потребовать присутствия пользователя перед возвратом данных. Bearer-токен становится обычным секретом в тот момент, когда процесс получает его байты.

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

Рекомендации Apple по Keychain чётко показывают границу. Keychain Services могут потребовать аутентификацию перед возвратом элемента, а Secure Enclave передаёт только результат биометрической проверки: успех или отказ. Ни приложение, ни операционная система не получают данные отпечатка пальца. Это отличная защита биометрического шаблона. Но она ничего не говорит о том, что авторизованное приложение сделает с возвращёнными байтами пароля.

Не просите Touch ID решить проблему после того, как уже отдали агенту секрет.

Более надёжная модель состоит из двух уровней. Процесс хранилища получает или использует учётные данные после того, как пользователь проходит проверку. Агент может запросить действие, указав учётные данные и назначение, но не может прочитать или заменить секрет и не может попросить другой инструмент вывести его. Процесс хранилища сам выполняет HTTP-запрос или аутентификацию SSH и возвращает намеренно ограниченный результат.

Это более узкий интерфейс. В этом и смысл.

## Граница должна проходить до stdout и переменных окружения

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

Я намеренно говорю прямо, потому что этот сценарий слишком предсказуем: если агент может выполнить `printenv`, прочитать `.env`, вызвать помощник для учётных данных или получить API-ключ в результате MCP-инструмента, значит, учётные данные у него уже есть. Неважно, хранились ли они изначально в Keychain, менеджере паролей или зашифрованном файле. Угроза остаётся той же.

Рассмотрим распространённую последовательность:

```text
Agent -\u003e runs a shell command -\u003e credential helper reads Keychain
      -\u003e helper prints token -\u003e shell captures stdout
      -\u003e agent receives token -\u003e token appears in context or logs
```

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

Именно здесь архитектура ломается.

Безопасный путь для учётных данных выглядит иначе:

```text
Agent -\u003e requests \"POST api.example.com/releases\" using credential \"release-bot\"
      -\u003e gateway asks for authorization if required
      -\u003e gateway obtains or uses credential internally
      -\u003e gateway sends HTTPS request with Authorization header
      -\u003e agent receives status, selected headers, and response body
```

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

То же правило действует для SSH. Не передавайте агенту путь к `~/.ssh/id_ed25519`, переменную `SSH_AUTH_SOCK`, через которую можно подписывать произвольные запросы без видимой границы, или команду, способную извлечь ключ из защищённого хранилища. Пусть помощник с узкой областью полномочий установит SSH-соединение и выполнит запрошенную команду. Возвращайте stdout, stderr, код завершения и сведения об идентичности хоста. Закрытый ключ не должен попадать в дерево процессов агента.

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

Не путайте зашифрованный диск с контролируемым путём выполнения.

## Touch ID подтверждает присутствие, а не намерение

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

Фреймворк Apple LocalAuthentication намеренно передаёт приложению ограниченный результат: фреймворк взаимодействует с Secure Enclave и возвращает успех или отказ. Вызывающее приложение задаёт текст причины и выбирает политику аутентификации. Такое разделение правильно. Система не должна делать вид, что способна понять намерение приложения по биометрическому событию.

В работе с агентами рассматривайте Touch ID как шлюз к возможности, а не как одобрение текста. Карточка «Разрешить этому агенту использовать production-учётные данные» даёт человеку слишком мало информации. Карточка с названием подписанного процесса, целевого хоста, меткой учётных данных и сроком действия подтверждения, один вызов или один запуск, даёт человеку основу для решения.

Я предпочитаю три разных момента авторизации:

1. Разблокировка хранилища. Пока хранилище заблокировано, любое действие с секретом завершается отказом. В системе не должно быть варианта «использовать незащищённый запасной путь».
2. Одобрение нового процесса агента на время его запуска. Подтверждение должно указывать издателя подписи кода, а не только изменяемое имя процесса вроде `node` или `python`.
3. Требование присутствия пользователя при каждом использовании учётных данных с необратимыми последствиями, например токена для развёртывания в production, облачной роли владельца или SSH-ключа, способного изменить целый парк машин.

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

Подтверждение каждого вызова должно оставаться редким, но при широких последствиях учётных данных оно должно быть обязательным. Токен, который открывает только доступ для чтения к системе задач, не требует отпечатка для каждого `GET`. SSH-учётные данные, позволяющие выполнить `kubectl apply` в production, требуют. Это создаёт прерывания, и они намеренны.

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

Я бы не стал отдавать production-полномочия грамматике, которую никто не проверяет после пятницы.

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

## У контроля доступа Keychain есть острые углы

Элемент Keychain с биометрической защитой полезен только при осознанном выборе ограничения доступа, понимании его запасных сценариев и защите от превращения авторизованного процесса в раздатчик секретов.

Apple документирует `SecAccessControlCreateWithFlags`, который позволяет связать элемент Keychain с требованиями доступности и авторизации. Для локального секрета разработчика, который не должен переноситься через резервное копирование или iCloud Keychain, обычно разумно выбрать `kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly`. Этот класс требует код-пароль устройства и делает элемент недоступным после его удаления. Суффикс `ThisDeviceOnly` также не даёт перенести элемент на другое устройство.

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

```swift
import Security

let access = SecAccessControlCreateWithFlags(
    kCFAllocatorDefault,
    kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,
    .biometryCurrentSet,
    nil
)!

let item: [CFString: Any] = [
    kSecClass: kSecClassGenericPassword,
    kSecAttrService: "com.example.agent-gateway",
    kSecAttrAccount: "release-bot",
    kSecAttrAccessControl: access,
    kSecValueData: Data(token.utf8)
]

let status = SecItemAdd(item as CFDictionary, nil)
precondition(status == errSecSuccess)
```

`biometryCurrentSet` строже, чем `biometryAny`. Он связывает доступ с текущими зарегистрированными отпечатками или данными лица, поэтому изменение набора биометрии делает защищённый элемент недействительным. `biometryAny` принимает любую зарегистрированную биометрию и не даёт такого же сигнала об изменении набора. Apple перечисляет оба флага в `SecAccessControlCreateFlags`. Выбирайте первый, если новый отпечаток должен приводить к осознанной повторной выдаче секрета.

Для извлечения нужны контекст аутентификации и текст операции, который описывает действие понятными человеку словами:

```swift
import LocalAuthentication
import Security

let context = LAContext()
context.localizedReason = "Use release-bot for the requested deployment action"

let query: [CFString: Any] = [
    kSecClass: kSecClassGenericPassword,
    kSecAttrService: "com.example.agent-gateway",
    kSecAttrAccount: "release-bot",
    kSecReturnData: true,
    kSecMatchLimit: kSecMatchLimitOne,
    kSecUseAuthenticationContext: context,
    kSecUseOperationPrompt: "Authorize credential use"
]

var result: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &result)
```

Код намеренно неполон в одном отношении: он не говорит, что делать с `result`. Обычное приложение может преобразовать его в `Data`, создать заголовок `Authorization` и продолжить работу. Шлюз агента должен гарантировать, что эти данные останутся внутри процесса, который выполняет аутентифицированный вызов. Не возвращайте их в ответе MCP. Не записывайте во временный файл. Не помещайте их в журнал при ошибке запроса.

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

Есть ещё одно ограничение. Биометрия может не сработать, быть недоступной или заблокироваться после неудачных попыток. Apple предоставляет политики, разрешающие переход к коду-паролю устройства, и политики, требующие именно биометрию. Выберите ту, которая соответствует вашему утверждению о безопасности. Если вы говорите «Touch ID при каждом развёртывании в production», бесшумное принятие другого запасного пути меняет это утверждение, и текст интерфейса тоже должен измениться.

## Проследите за вредной инструкцией до момента победы

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

Представьте, что агент помогает с pull request. Он читает документ в репозитории, добавленный злоумышленником. В документе сказано, что для проверки релиза нужно запустить вспомогательный скрипт. Скрипт использует существующий сеанс облачного CLI разработчика, перечисляет учётные данные для развёртывания и отправляет закодированный запрос на внешний адрес. У агента есть разрешение выполнять команды оболочки, а также унаследованы `AWS_PROFILE`, `GH_TOKEN` или доступ к SSH-агенту.

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

Touch ID при входе не спас бы такую архитектуру. Человек мог пройти аутентификацию час назад, а затем уйти от компьютера. Элемент Keychain, защищённый только условием «устройство разблокировано», может стать доступным процессу, которому пользователь не намеревался разрешать эту задачу. Сама Apple предупреждает, что доступ при разблокированном устройстве подходит не для всех сценариев.

Теперь изменим архитектуру. Агент запрашивает действие со следующими полями:

```json
{
  "channel": "http",
  "credential": "release-bot",
  "method": "POST",
  "url": "https://api.example.com/releases",
  "body": {"branch": "feature/fix-ci"}
}
```

Шлюз сам находит `release-bot`. Он сопоставляет запрошенное назначение с выполняемым действием, при необходимости запрашивает авторизацию, сам добавляет заголовок и записывает действие. Агент получает статус HTTP и сокращённый ответ. Значение заголовка ему недоступно.

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

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

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

## Дескрипторы возможностей безопаснее строк секретов

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

Слово «возможность» используют слишком свободно. Здесь это ссылка, полезная только внутри шлюза, например `release-bot`, `staging-ssh` или `billing-read`. Это не псевдоним токена, который агент может обменять на токен. Не шаблонная переменная, разворачивающаяся в значение окружения. Это селектор, передаваемый процессу, который сохраняет исключительный доступ к учётным данным.

Такой выбор дисциплинирует интерфейс инструментов. HTTP-инструменты должны принимать метод, URL, заголовки, которые безопасно передать от агента, и тело запроса. Добавление учётных данных должно происходить после проверки, внутри шлюза. SSH-инструменты должны принимать хост, пользователя, команду и выбранную идентичность учётных данных, а затем вызывать помощник, владеющий путём аутентификации. Они не должны возвращать путь `IdentityFile` или предлагать универсальную операцию «прочитать секрет».

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

Sallyport устроен по этому принципу: агент использует встроенный stdio-переходник `sp mcp` для запроса действий HTTP или SSH, а приложение хранит API- и SSH-учётные данные в зашифрованном хранилище и само выполняет действие. Агент получает результат, а не учётные данные в открытом виде.

Ограничение здесь честное. Инструмент, который знает только HTTP и SSH, автоматически не охватит все настольные приложения, клиенты баз данных, реестры пакетов и локальные бинарные файлы в окружении разработчика. Добавление нового канала должно требовать проектирования модели действий, правил сокрытия данных, семантики авторизации и полей аудита. Универсальный переключатель «запустить что угодно с моим входом» проще выпустить и сложнее защищать.

Используйте явные метки учётных данных, которые показывают предполагаемую область действия. `prod-deployer` лучше, чем `token-4`. `github-readonly-org` лучше, чем `github`. Метка становится частью решения человека и записи аудита, поэтому неоднозначность превращается в операционную проблему, а не просто в вопрос именования.

Формат запроса должен быть достаточно узким, чтобы шлюз мог показать его без интерпретации. Рецензент поймёт `POST https://api.example.com/releases`. Но он не сможет надёжно определить эффект от Base64-строки, переданной через оболочку.

## Журнал аудита должен описывать действие, не копируя секрет

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

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

Каждое действие записывайте отдельно. Для HTTP сохраняйте метку учётных данных, метод, назначение, статус, время выполнения и тщательно выбранное резюме тела. Для SSH сохраняйте метку учётных данных, хост, удалённого пользователя, команду, код завершения и время выполнения. Не записывайте заголовки `Authorization`, bearer-значения, байты закрытых ключей, полные тела запросов с данными клиентов или неограниченный вывод команд.

Журналы с секретами превращаются в ещё одно хранилище, только с худшим контролем доступа.

Защита от незаметного изменения важна, потому что расследование инцидента с агентом часто начинается со спора о хронологии: «Вызывал ли агент этот адрес?» «Был ли сеанс одобрен?» «Кто-то изменил локальную историю?» Журнал событий с цепочкой хешей даёт проверке конкретный объект. Проверка должна работать без расшифровки каждого события, иначе человеку, проверяющему целостность, сначала придётся выдать чувствительные данные, которые журнал должен был защищать.

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

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

Apple Platform Security и документация Apple Developer Security здесь особенно полезны, потому что разделяют аппаратную защиту, системные средства контроля и ответственность приложения. Именно такое разделение нужно для безопасности агентов. Защищённое оборудование может управлять доступом, но приложение всё равно решает, что отправить по сети и что записать на диск.

## Для production-доступа нужно меньше путей, а не более умные догадки

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

Проверьте следующее перед тем, как разрешить агенту работать с учётными данными:

1. Убедитесь, что агент не может прочитать секрет через переменные окружения, файлы, помощника для учётных данных, вывод инструмента или дочерний процесс.
2. Убедитесь, что доверенный процесс сам выполняет HTTP-запрос или аутентификацию SSH и добавляет учётные данные после получения структурированных параметров от агента.
3. Настройте блокировку хранилища так, чтобы она запрещала любое действие с секретом. Проверьте это при заблокированном приложении, а не только при заблокированном экране Mac.
4. Требуйте нового одобрения сеанса при запуске каждого нового процесса агента, причём подтверждение должно указывать издателя подписи.
5. Пометьте учётные данные с возможностями развёртывания, администрирования, разрушительных действий или широкого доступа SSH как требующие подтверждения каждого использования.

Проверяйте неприятные сценарии. Добавьте тестовый секрет вроде `canary-agent-secret-9f31` в непроизводственные учётные данные. Попросите агента изучить репозиторий, запустить тесты и вывести результаты инструментов. Затем поищите эту точную строку в его расшифровке сеанса, истории терминала, временных каталогах, журналах, окружении дочерних процессов и записях аудита. Если она окажется где-то за пределами процесса хранилища, архитектура передала агенту путь к секрету.

Сделайте то же для отзыва. Одобрите запуск, выполните один безопасный запрос, отзовите запуск, пока процесс остаётся открытым, а затем повторите запрос. Второй запрос должен завершиться отказом до обращения к удалённому сервису. Контроль отзыва, который начинает действовать только после перезапуска, - это документация, а не изоляция.

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