Least-privilege — одна из старейших и самых надёжных идей в безопасности. Дайте идентичности ровно тот доступ, который ей нужен, и ничего больше, и радиус поражения при любой компрометации останется небольшим. Для пользовательских учётных записей и долгоживущих сервисных аккаунтов эта модель держится вполне неплохо: роли проверяются, гранты ограничиваются по области действия, сертификация доступов проводится с ежеквартальной периодичностью.
AI-агенты тихо ломают эту модель. Агенту выдают учётные данные, инструмент, подключение к MCP-серверу — и с этого момента его поведение становится динамическим. Он решает в рантайме, какие ресурсы читать, какие писать, какой инструмент вызвать следующим. Грант, который вы записали в первый день, описывает потолок, а не то, что агент делает. А поскольку агенты продуктивны, этот потолок обычно ставят с запасом: широкие роли в базе данных, доступ на запись «на всякий случай», сервисный аккаунт, общий для полудюжины рабочих процессов. В итоге получается ландшафт, где разрешённый доступ и фактически используемый доступ расходятся — тихо, каждый день.
Независимые отраслевые исследования отражают масштаб этого слепого пятна: примерно 82% организаций сообщают, что у них работают AI-агенты, о которых они не знали, при этом лишь около 21% ведут их инвентаризацию в реальном времени (CSA/Token Security, n=418). Вы не сможете применить least-privilege к ландшафту, который не видите.
Определение дрейфа от least-privilege
Назову это явление точно. Дрейф от least-privilege — это растущий разрыв между доступом, который агенту разрешён, и доступом, который он, как наблюдается, использует. У него есть два направления сбоя, и очевидно только одно из них.
Очевидное направление — это недоиспользование: агент держит доступ на запись к таблице, в которую ни разу не писал. Это мёртвая привилегия, и это риск, который вы несёте без какой-либо выгоды. Опасное направление — это обратный сигнал, который оно подразумевает: когда наблюдаемое множество содержит то, что разрешённое множество никогда намеренно не предоставляло, у вас есть действие, которое никто не проверял. Задание экспорта, которое внезапно пишет в бакет, из которого оно только читало. Агент, которому политика ограничила область одной схемой, дотягивается до другой. Это не экзотика; это повседневная фактура агентов, связанных между собой грубыми грантами.
Причина, по которой это сложно, и причина, по которой это специфично именно для агентов, а не для людей, в том, что поведение генерируется, а не конфигурируется. Человек с избыточным доступом по большей части его не использует. Агент с избыточным доступом использует всё, что помогает ему выполнить стоящую перед ним задачу, включая пути, которых никто не предвидел. Статическая проверка политик не успевает за поведением, которое меняется от запуска к запуску.
Превращение дрейфа в проверяемый сигнал
Дрейф опасен лишь пока он невидим. Задача — превратить его в сигнал, который человек может проверить, а для этого нужны две вещи, работающие вместе: непрерывное наблюдение за тем, к чему агенты на самом деле обращаются, и стабильная запись того, к чему им было разрешено обращаться.
Наблюдение должно вестись в режиме read-first. Сборщик, который наблюдает из логов, трейсов OpenTelemetry и сигналов ядра eBPF, находится вне пути данных агента. Это не прокси, он не пропускает вызовы через себя, и если он отказывает, то отказывает «открыто» в безопасном смысле: агент продолжает работать, вы теряете видимость, а не доступность. Эта асимметрия важна: средство защиты, способное положить прод, — это средство, которое команды тихо отключают. Слой eBPF, в частности, выступает как ground truth на уровне ядра — та часть, которую агент не может обойти, — и именно поэтому подсказки на уровне протокола, такие как аннотации инструментов MCP (readOnlyHint, destructiveHint), сверяются с ним, а не принимаются на веру. Сама спецификация MCP говорит, что этим аннотациям доверять нельзя; сигналы ядра — это то, что делает такую сверку реальной.
Результатом наблюдения является карта доступа: для каждого агента — к каким ресурсам он обращался и читал ли он (R) или читал и писал (RW). Карта хранит отношения доступа, а не полезные нагрузки, секреты или PII. Интересная часть — это сравнение:
| Агент | Ресурс | Разрешено | Наблюдается | Дрейф |
|---|---|---|---|---|
| data-export-job | prod-postgres | R | R | нет |
| data-export-job | s3://billing-exports | R | RW | непроверенная запись |
| report-builder | analytics-db | R | (не используется) | мёртвая привилегия |
Значимая строка — средняя. Политика дала чтение бакета экспорта; сборщик наблюдал запись. Эта единственная строка — главный риск, который дрейф от least-privilege и призван выявить: задействованная привилегия, которую никто не проверял, отнесённая к конкретному агенту, а не к общему сервисному аккаунту, потому что именно идентичность на уровне отдельного агента вообще делает возможными атрибуцию и аудит.
Применение в момент доступа, а не просто логирование
Обнаружение говорит вам, что дрейф произошёл. Чтобы замкнуть цикл, вы хотите, чтобы непроверенная запись стала запрещённым путём, а не залогированным. Здесь и вступает в дело policy-as-code, оцениваемый в момент доступа. То самое сравнение, которое выявило дрейф, становится правилом, которое его предотвращает.
Рассмотрим закрепление задания экспорта в режим «только чтение» для производственной базы данных и прямой запрет записи, где нарушение блокирует и оповещает, а не проходит молча:
agent "data-export-job" {
# Read-only on the operational database. No writes, ever.
access "prod-postgres" {
mode = "read"
deny = ["write", "delete", "ddl"]
}
# The export target the job is *supposed* to use.
access "s3://billing-exports" {
mode = "read"
}
on_violation {
action = "block" # deny the call at access time
alert = "security-oncall"
audit = "append" # write to the tamper-evident ledger
}
}
Пройдём «до» и «после» конкретно.
До. Задание экспорта, обладая широкой ролью, выполняет запись в s3://billing-exports. Ничто его не останавливает. Действие проходит успешно, сливается с обычным трафиком и проявляется днями позже, если вообще проявляется, как аномалия на карте доступа. Разрыв между разрешённым и наблюдаемым расширился, а единственный артефакт — строка лога, которую никто не прочитал.
После. Приходит та же запись. Политика оценивается в момент доступа, видит write на ресурсе, ограниченном до read, и возвращает отказ ещё до того, как операция совершится. Нарушение блокирует вызов, поднимает оповещение для дежурной смены и добавляет запись в дополняемый только в конец, связанный хешами журнал аудита. Непроверенная запись так и не становится непроверенным изменением. Дрейф в тот же момент конвертируется обратно в путь, соответствующий least-privilege.
Два свойства сохраняют честность этого подхода. Во-первых, каждый привилегированный просмотр карты доступа сам подвергается аудиту — кто и на что смотрел, — потому что карта чувствительна, а средство защиты, которое не может отчитаться за собственных операторов, не заслуживает доверия. Во-вторых, уверенность показывается явно: действие, отнесённое к агенту на основании доказательств уровня ядра, помечается иначе, чем выведенное приблизительно. Вас никогда не просят действовать на основании сфабрикованной уверенности.
Вывод
Least-privilege не подвёл для AI-агентов; подвела периодичность проверок. Гранты грубы, поведение динамично, а ежеквартальные сертификации не могут отслеживать поверхность доступа, которая меняется от запуска к запуску. Решение — не более тяжёлый прокси на критическом пути. Это непрерывное наблюдение в режиме read-first, которое формирует сравнение «разрешено vs наблюдается», плюс policy-as-code, который применяет исправленную границу в момент доступа, — так что чрезмерно привилегированный агент обнаруживается как проверяемый сигнал задолго до того, как он превратится в инцидент.
Если вы хотите увидеть, как сборщик, карта доступа и применение в момент доступа складываются вместе, не находясь на пути данных ваших агентов, страница архитектуры разбирает дизайн, а продукт показывает, как выглядит представление «разрешено vs наблюдается» на реальном ландшафте.