Вложенные группы в AD: как не положить контроллер домена рекурсией
Магия LDAP_MATCHING_RULE_IN_CHAIN кажется спасением, пока двадцать сервисов не начинают опрашивать вложенные группы одновременно. Разбираем, почему рекурсивный поиск в Active Directory вешает CPU контроллера на 100% и как нормализовать группы через шлюз доступа.

Утро понедельника, 09:15. Сотрудники приходят в офис, включают ноутбуки, открывают корпоративную почту, GitLab и внутренний трекер задач.
И в этот момент мониторинг начинает истошно выть: процессор на основном контроллере домена уходит в полку на 100%, процесс lsass.exe пожирает все доступные ресурсы, а время ответа на простейший LDAP-запрос подскакивает с 5 миллисекунд до 35 секунд. Сервисы падают по таймауту, сисадмины нервно пьют корвалол.
Знакомая картина? Если да, передайте привет главному архитектурному костылю энтерпрайза — глубоко вложенным группам безопасности (Nested Groups).
---
Как строят «удобные» группы и почему это катастрофа
В теории концепция вложенных групп в Active Directory выглядит логично и красиво:
- Есть группа отдела:
Dep-Backend; - В неё входит группа проекта:
Project-Billing-Devs; - В неё входит группа роли:
GitLab-Maintainers; - А в неё входит группа доступа к серверам:
SSH-Prod-Access.
Админу кажется, что он гений автоматизации: добавил нового джуна в Project-Billing-Devs — и он автоматически получил права везде, где нужно!
Но вот приходит веб-приложение (например, корпоративный портал или мониторинг) и задаёт контроллеру домена простой вопрос: *«А состоит ли пользователь Иванов в группе GitLab-Maintainers?»*.
---
Знакомьтесь: магический OID 1.2.840.113556.1.4.1941
Обычный атрибут memberOf в Active Directory возвращает только те группы, в которые пользователь добавлен напрямую. Вложенные группы он не видит.
Чтобы приложение могло узнать обо всех косвенных правах, разработчики раскапывают в документации Microsoft специальное заклинание:
LDAP_MATCHING_RULE_IN_CHAIN с OID 1.2.840.113556.1.4.1941.
Поисковый LDAP-фильтр начинает выглядеть так:
(&(objectCategory=user)(memberOf:1.2.840.113556.1.4.1941:=CN=GitLab-Maintainers,OU=Groups,DC=company,DC=local))
Что делает контроллер домена, получая такой запрос?
Он запускает рекурсивный обход дерева в глубину. Он берёт целевую группу, читает всех её членов, для каждого члена проверяет, не является ли он группой, разворачивает её, затем разворачивает следующий уровень...
А теперь умножьте это на реальность:
1. Глубина матрешки в 4–6 уровней.
2. Циклические ссылки. Сисадмин Вася три года назад случайно добавил группу Admins-Secondary в Admins-Primary, а стажёр Петя через год сделал наоборот. Контроллер домена вынужден на лету отслеживать петли графа, сжигая циклы CPU.
3. Отсутствие кэширования в сервисах. Древний Jenkins или самописная админка дёргает этот рекурсивный фильтр на каждый HTTP-запрос от пользователя.
Когда сто человек одновременно открывают по 5 вкладок в браузере, контроллер домена получает лавину из десятков тысяч рекурсивных вычислений в секунду. Финал предсказуем.
---
3 шага к наведению порядка
Шаг 1. Отказ от циклов и чистка «матрешек»
Проведите аудит домена простым PowerShell-скриптом на поиск групп с глубиной вложенности больше двух.
Если для понимания того, почему разработчик имеет доступ к базе данных, вам нужно развернуть цепочку из пяти групп — эту цепочку нужно принудительно расплющить. Старый добрый принцип Role-Based Access Control (RBAC) предполагает, что права привязываются к понятным функциональным ролям, а не к бесконечным генеалогическим древам.
Шаг 2. Изоляция контроллера от сырых запросов приложений
Сервисы компании (GitLab, Grafana, Nextcloud, Wiki) вообще не должны ходить в Active Directory напрямую и уж тем более исполнять тяжелые поисковые запросы по дереву объектов.
Шаг 3. Делегирование расчёта прав шлюзу OwnGate
В современной архитектуре между Active Directory и сервисами встаёт шлюз доступа (OwnGate):
[Active Directory]
│
(1 запрос при логине: вычисление членства)
▼
[Шлюз OwnGate] ──► Кэширует плоский массив ролей в JWT
│
(Отдаёт готовые claims: ["devops", "billing-admin"])
▼
[Сервисы: GitLab / Grafana / Nextcloud]
Как это решает проблему нагрузки:
1. Расчёт один раз. Шлюз вычисляет полное членство пользователя в группах ровно один раз — в момент прохождения аутентификации.
2. Плоский JSON вместо рекурсивного дерева. Шлюз формирует компактный массив строк:
"groups": ["Billing-Devs", "GitLab-Maintainers", "All-Staff"].
3. Нулевая нагрузка при работе. Сервисы читают готовый массив из подписанного токена JWT за 0.001 миллисекунды. Ни один пакет не летит на контроллер домена при переходах по страницам и кликах в интерфейсе.
Перестаньте мучить Active Directory рекурсивными фильтрами из прошлого века. Перевод авторизации на единый шлюз доступа снимет нагрузку с контроллера домена и навсегда избавит от утренних зависаний инфраструктуры.