open atlas
↑ К треку
Основы безопасности SECF · 05 · 04

Policy engines и авторизация на масштабе

Когда логика авторизации размазана по 40 сервисам, каждая команда изобретает её заново и никто не ответит «кто что может». Policy engine выносит решение наружу — policy-as-code, вычисляемый сайдкаром, — но это задача про задержку и согласованность.

SECF Senior ◷ 18 min
Уровень
ОсновыJuniorMiddleSenior

Аудитор задаёт, казалось бы, простейший в компании вопрос: «Какие роли могут оформить возврат средств?» Ты открываешь биллинговый сервис и находишь проверку, вшитую прямо в контроллер. Потом кто-то вспоминает, что у админ-консоли своя копия. Потом у внутреннего CSR-инструмента, который ходит в другой эндпоинт, есть третья копия — и её в прошлом квартале пропатчили, чтобы пускать тимлидов, но только там. Три сервиса, три ответа, ни один не авторитетный, и правило дрейфует чуть сильнее с каждым спринтом. Никто не ошибается; каждый просто проверяет авторизацию там, где обрабатывает запрос. Это и есть стена масштабирования: когда решение живёт внутри сорока сервисов, «кто что может» перестаёт быть фактом, который можно посмотреть, и становится археологическими раскопками, которые ты ведёшь во время инцидента.

К концу урока ты поймёшь, почему логика авторизации деградирует, когда она размазана по сервисам, как policy engine вроде OPA выносит решение наружу в виде policy-as-code, и какими компромиссами по задержке и согласованности оплачивается это решение.

Почему размазанная авторизация деградирует

Авторизация начинается просто. Один сервис, пара ролей, if user.role == "admin" в начале обработчика. Это работает, поэтому следующий сервис копирует приём, а за ним и следующий. В отдельно взятый день в этом нет ошибки — это путь наименьшего сопротивления, и каждая команда локально права. Сбой эмерджентен: через год у тебя одно и то же правило выражено сорока чуть различными способами, на четырёх языках, против трёх слегка разных моделей ролей, и ни одного места, способного ответить на вопрос о политике.

У этой деградации три конкретные цены, которые зрелый инженер ощущает в продакшене. Первая — дрейф: когда «менеджеры могут одобрять расходы своей команды до $5k» меняется на «$10k», тебе нужно найти и поправить это в каждом сервисе, который это проверяет, — и ты один пропустишь, молча, потому что ничего не падает, когда проверка всего лишь устарела, а не отсутствует. Вторая — неаудируемость: на вопрос комплаенса или инцидента вроде «кто может удалить запись клиента» нет авторитетного ответа; ты грепаешь сорок репозиториев и надеешься. Третья — несогласованное применение: правило возврата, пускающее тимлидов в CSR-инструменте, но не в биллинге, — это не фича, это уязвимость, которую никто не решал отгружать. Размазанная авторизация не просто беспорядочна; это структурная причина уже знакомых тебе багов broken access control, помноженная на число твоих сервисов.

Вынос решения наружу: PDP и PEP

Лекарство — отделить принятие решения от применения. Это разделение PDP/PEP, и именно эта ментальная модель проясняет всё остальное:

  • Точка применения политики (PEP, Policy Enforcement Point) живёт в твоём сервисе. Это код на границе запроса, который говорит: «Я собираюсь выполнить действие X над ресурсом Y для пользователя Z — мне можно?» Он не содержит правила. Он спрашивает.
  • Точка принятия решения (PDP, Policy Decision Point) — это policy engine. Он держит правила как данные, получает вопрос плюс релевантный контекст (роли пользователя, владельца ресурса, атрибуты), вычисляет политику и возвращает allow или deny.

Код твоего приложения сжимается до одной строки: собрать вход, вызвать PDP, исполнить ответ. Логика — каждое условие, роль и исключение — переезжает в версионируемый policy-as-code, который живёт в одном репозитории, проходит ревью, тестируется в CI и деплоится независимо от потребляющих его сервисов. Выигрыш прямой: у вопроса аудитора теперь ровно одно место для поиска, правило возврата меняется ровно в одном файле, а применение одинаково везде, потому что каждый сервис спрашивает один и тот же мозг.

OPA и Rego на практике

Эталонная реализация этого паттерна — OPA (Open Policy Agent), выпускник CNCF, универсальный policy engine. Политики OPA пишут не на языке приложения; их пишут на Rego — декларативном языке запросов, созданном ровно под эту форму задачи: «для данного входа заключает ли хоть одно правило allow?» Тривиальная политика на Rego выглядит так:

package authz

default allow := false

allow if {
    input.action == "refund"
    input.user.role in {"billing_admin", "team_lead"}
}

Здесь важны две вещи. default allow := false — это deny by default, выраженный одной строкой: если ни одно правило не сработало, ответ «нет», ровно тот рефлекс, которого требует контроль доступа. И правило — это данные, которые вычисляет engine, а не поток управления, который ты сопровождаешь: ты меняешь, кому можно делать возврат, редактируя этот файл, а не охотясь по коду сервисов. OPA годится не только для авторизации сервисов — тот же engine и язык применяют admission control в Kubernetes (через Gatekeeper), проверки Terraform-планов и гейты CI, поэтому вложение в Rego окупается по всей платформе, а не только в одном API.

Операционный вопрос — где работает PDP, и вот тут зрелый инженер отрабатывает свою зарплату:

РазвёртываниеЗадержка решенияРадиус поражения при сбоеСвежесть политики
Центральный PDP-сервис (сетевой хоп)~1–10мс+ на вызов, на пути запросаОбщий SPOF: PDP упал → все сервисы заблокированыМгновенная: одно место обновления
Сайдкар / встроенный OPA (локально)Субмиллисекунда, в процессе / по loopbackИзолирован: один сайдкар упал → один подСогласована в конечном счёте: лаг подтяжки бандла
Встроенная библиотека (в твоём бинаре)Минимальная, без IPC вообщеСвязан с жизненным циклом процессаПривязана к ритму твоего деплоя/перезагрузки

Доминирующий продакшен-ответ — модель сайдкара / локального демона: OPA работает рядом с каждым сервисом (тот же под, тот же хост), политика раздаётся как подписанные бандлы, которые агент периодически подтягивает, а решение — loopback-вызов, измеряемый микросекундами. Ты меняешь сетевой round-trip, который не можешь позволить на каждом запросе, на согласованность в конечном счёте для политики: изменённое правило достигает каждого узла за интервал опроса бандла. Для авторизации это обычно нормально; но чтобы отозвать скомпрометированного админа прямо сейчас, ты обязан понимать, что лаг распространения реален, и проектировать с учётом этого.

Почему это работает

Почему просто не спрятать PDP за сетевым вызовом и не держать одну авторитетную копию? Потому что ты добавишь синхронную зависимость к каждому авторизованному запросу в компании. При 50k req/s хоп авторизации в 5мс — это 250 engine-секунд добавленной задержки на каждую секунду реального времени, и в тот момент, когда у этого сервиса плохой деплой или пауза GC, все остальные сервисы встают за ним — ты построил общекомпанийную точку отказа на самом горячем пути, какой бывает. Модель сайдкара сохраняет единый источник истины (один репозиторий политик), распределяя при этом вычисление на край, так что сбой — это один под, а не платформа. Цена, которую ты принимаешь взамен, — что «источник истины» и «что сейчас применяется на узле N» согласованы в конечном счёте, а не мгновенно.

Что зрелый инженер делает неверно (и верно)

Соблазнительная ошибка — считать «мы внедрили OPA» финишной чертой. Вынос engine наружу не выносит наружу данные, которые нужны политике: если политика должна знать, что пользователь 42 владеет инвойсом 4815, этот факт владения обязан как-то дойти до PDP — переданным во входе, реплицированным в engine или дотянутым по ходу решения. Каждый вариант — реальный компромисс (раздувание входа против устаревшей реплики против зависимости на момент решения, которой ты пытался избежать). И engine теперь на твоём критическом пути: тебе нужно определённое поведение, когда PDP недоступен. Fail-closed (отказ при ошибке engine) — безопасный дефолт и правильный выбор для чувствительных действий; fail-open изредка оправдан для низкорисковых чтений, где доступность важнее строгости, — но это решение, которое ты принимаешь явно, на каждое действие, с открытыми глазами, а не запасной вариант, который ты обнаруживаешь во время аварии.

Выбери лучший вариант

Ты раскатываешь policy engine (OPA) для авторизации по ~40 сервисам, обрабатывающим десятки тысяч req/s. Выбери развёртывание и поведение при сбое, которое выберет зрелый инженер.

Викторина

В разделении PDP/PEP что живёт в твоём сервисе приложения?

Викторина

Ты запускаешь OPA как сайдкар на сервис с раздачей политики бандлами. Админа скомпрометировали, и ты отзываешь его роль. Почему он всё ещё может пройти проверку авторизации через секунды?

Расставь шаги по порядку

Упорядочи жизненный цикл одного вынесенного решения авторизации, от запроса до применения:

  1. 1 Запрос попадает в сервис; PEP перехватывает его на границе
  2. 2 PEP собирает вход: субъект, действие, ресурс, контекст
  3. 3 PEP спрашивает PDP (OPA), который вычисляет загруженную политику Rego
  4. 4 PDP возвращает allow или deny (deny по умолчанию, если правило не сработало)
  5. 5 Сервис исполняет решение — продолжает или отклоняет
Вспомните перед уходом
  1. 01
    Объясни разделение PDP/PEP и почему вынос решения авторизации наружу лучше встроенных проверок в сорока сервисах.
  2. 02
    Что даёт сайдкар-развёртывание OPA и какие компромиссы по согласованности и сбою оно заставляет проектировать?
Итог

Когда логика авторизации живёт встроенной в сорока сервисах, «кто что может» перестаёт быть фактом и становится археологией: правила дрейфуют, не аудируются и применяются несогласованно — это просто broken access control, помноженный на число твоих сервисов. Policy engine чинит это, отделяя принятие решения от применения. Точка применения политики (PEP) — тонкий код в каждом сервисе, который собирает вход и спрашивает; точка принятия решения (PDP) — OPA, вычисляющий политику на Rego с deny-by-default, — держит правила как версионируемый policy-as-code в одном репозитории. В продакшене ты запускаешь OPA сайдкаром на сервис, питаемым подписанными бандлами, меняя синхронный сетевой хоп, который не позволить на горячем пути, на согласованность политики в конечном счёте: изменённое или отозванное правило распространяется за интервал опроса, а на ошибках engine ты делаешь fail-closed для чувствительных действий. Теперь, когда аудитор спросит «какие роли могут оформить возврат», твой первый ход — не грепать сорок репозиториев, а открыть один файл политики, которому каждый сервис уже подчиняется.

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 6 завершено

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

Примени это

Примени этот урок в реальном проекте.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources2
expand
  1. 01
  2. 02

Trademarks belong to their respective owners. Editorial reference only.