LDAP-каталог
Вход по учётным записям корпоративного каталога и автоматическая синхронизация ролей Meridian с группами и атрибутами LDAP. Настройка подключения в config.json, правила сопоставления и журнал синхронизаций в панели управления.
Обзор
Интеграция с LDAP решает две разные задачи, и их полезно различать с самого начала:
- Аутентификация — кто пользователь. Пароль проверяется не в Meridian, а в каталоге: Meridian выполняет bind от имени пользователя и не хранит его хеш.
- Синхронизация прав — что ему разрешено. Периодический обход каталога переводит членство в группах и значения атрибутов в привязки ролей Meridian (role bindings).
Возможность опциональна и аддитивна: без блока ldap в config.json ничего не меняется, а с ним локальные учётные записи продолжают работать ровно как раньше. Интеграция входит во все редакции — community, pro и enterprise — без отдельной лицензии и без флагов сборки.
Ключевые свойства:
- Единственный писатель. Права из каталога записывает только лидер кластера. Аутентифицировать может любой узел, но изменять учётные записи и привязки — один.
- Отказ в закрытую сторону. Недоступность каталога приводит к отказу входа и к неуспешному циклу синхронизации, но ничего не деактивирует: уже выданные сессии продолжают работать, локальные учётные записи не затрагиваются.
- Настройки подключения живут только в файле. Блок
ldapникогда не попадает в хранилище конфигурации и не редактируется через API — он читается изconfig.jsonпри старте. Пароль сервисной учётной записи, таким образом, не может утечь через экспорт конфигурации. - Правила сопоставления, наоборот, живут в базе и редактируются в панели управления во время работы.
Настройки подключения (адрес, сервисная учётная запись, фильтры) меняются только в config.json и вступают в силу после перезапуска. В кластере блок должен присутствовать и быть одинаковым на каждом узле — он не реплицируется.
Подключение каталога
Минимальная рабочая конфигурация — семь полей. Всё остальное имеет разумные значения по умолчанию.
{
"governance": {
"auth_config": {
"is_enabled": true,
"admin_username": "admin",
"admin_password": "env.MERIDIAN_ADMIN_PASSWORD",
"ldap": {
"enabled": true,
"url": "ldaps://ldap.example.com:636",
"bind_dn": "uid=meridian-svc,ou=people,dc=example,dc=com",
"bind_password": "env.MERIDIAN_LDAP_BIND_PASSWORD",
"user_base": "ou=people,dc=example,dc=com",
"user_filter": "(&(objectClass=person)(uid=%s))"
}
}
}
}user_filter обязан содержать ровно один %s. При входе на его место подставляется экранированное имя пользователя, при полном обходе каталога — *. Тот же фильтр определяет, кто вообще попадает в Meridian.
Основные параметры
| Поле | По умолчанию | Назначение |
|---|---|---|
enabled | false | Главный выключатель. При false весь блок игнорируется. |
url | — | ldap://host:389 или ldaps://host:636. Любая другая схема (включая starttls) — ошибка запуска. |
bind_dn / bind_password | — | Сервисная учётная запись только для чтения. Пароль поддерживает форму env.ИМЯ_ПЕРЕМЕННОЙ. |
user_base | — | База поиска пользовательских записей. |
user_filter | — | Фильтр пользователей с одним %s. |
admin_filter | — | Дополнительный фильтр. Совпадение даёт роль g.admin на глобальной области. |
restricted_user_filter | — | Совпадение ⇒ учётная запись отключается, сессии сбрасываются. |
restricted_group_dns | — | Членство в любой из этих групп ⇒ то же самое. |
attribute_username | uid | Атрибут с именем для входа. Значение приводится к нижнему регистру. |
member_of_attribute | memberOf | Прямой режим членства (Active Directory, LLDAP). |
group_search_enabled | false | Обратный режим для классического OpenLDAP: обходить группы вместо чтения memberOf. |
scope_role_attribute | meridianScopeRole | Многозначный атрибут с назначениями (метод B). |
sync_interval_minutes | 10 | Периодичность полного обхода каталога. Минимум 1. |
sync_log_level | warn | Что попадает в журнал синхронизаций: off, error, warn, info. |
sync_log_retention_days | 14 | Срок хранения записей журнала, 1–365. |
request_timeout_ms | 10000 | Таймаут одной операции. Минимум 1000. |
allow_deactivate_all | false | Предохранитель массовой деактивации — см. ниже. |
Конфигурация проверяется строго: неправильная схема URL, отсутствие обязательного поля или %s в фильтре приводят к отказу запуска процесса, а не к «тихому отключению LDAP». Это сделано намеренно — молча выключенная аутентификация опаснее упавшего процесса.
Права сервисной учётной записи
Учётная запись из bind_dn только ищет и никогда не пишет в каталог. Достаточно доступа на чтение к поддереву пользователей, к поддереву групп (если включён group_search_enabled) и к вычислению фильтров на отдельных записях.
Предпочитайте ldaps://: и служебный bind, и bind самого пользователя передают пароль в открытом виде, поэтому обычный ldap:// допустим только в доверенном сегменте сети или в локальном тестовом стенде. skip_tls_verify — исключительно для разработки.
Как каталог превращается в роли
Одновременно работают три источника, и их результаты объединяются.
Метод A — группа каталога → (роль, область)
Правило связывает DN группы с парой «роль + область»:
cn=acme-admins,ou=groups,dc=example,dc=com → s.admin @ customer «Acme»Сопоставление — точное совпадение строк в нижнем регистре, без шаблонов и без учёта вложенности поддерева. Одна группа может нести несколько правил, все они применяются. Это подходящий метод, когда модель доступа уже закодирована членством в группах и менять схему каталога не хочется.
Вложенные группы не раскрываются. Учитывается только прямое членство: пользователь в группе, которая сама является членом сопоставленной группы, не получит ничего. Сопоставляйте те группы, в которых пользователи состоят напрямую.
Метод B — атрибут meridianScopeRole
Многозначный атрибут пользовательской записи несёт назначения напрямую:
| Значение | Результат |
|---|---|
global:g.admin | g.admin на глобальной области |
customer:acme:s.viewer | s.viewer у клиента, на которого указывает псевдоним acme |
team:payments:s.admin | s.admin у команды, на которую указывает псевдоним payments |
Средняя часть — псевдоним, а не идентификатор. Данные каталога не должны нести UUID Meridian, поэтому псевдонимы регистрируются отдельно (вкладка Scope mappings) и разрешаются на каждом цикле синхронизации.
Некорректное значение пропускается с предупреждением и не влияет на остальные значения того же пользователя: неизвестная роль, неизвестный псевдоним, псевдоним не того типа, роль, не привязываемая к такой области. Каждый такой пропуск попадает в журнал синхронизаций отдельной строкой — с самим значением и причиной.
Атрибут метода B должен быть доступен на запись только администраторам каталога. Meridian не может определить, кто записал значение, поэтому ACL каталога на этот атрибут — единственное, что отделяет обычного пользователя от global:g.admin. Если ограничить запись нельзя, не используйте метод B: оставьте атрибут необъявленным и опирайтесь на метод A, где право следует из членства в группе.
admin_filter
Самый короткий путь к первому администратору: один фильтр в config.json, без правил и без атрибутов. Все, кто ему соответствует, получают g.admin на глобальной области.
Это правило живёт в файле, а не в базе, поэтому в таблице сопоставлений оно показано закреплённой строкой с пометкой config.json и без кнопок редактирования — чтобы правило, выдающее административные права, было видно на той же странице, где перечислены остальные.
Панель управления
Раздел Settings → LDAP появляется, когда каталог настроен и аутентификация включена. Он доступен только глобальным ролям: справочник в кластере один, поэтому администратору отдельного клиента или команды здесь нечего делать.
Если блок ldap не настроен, страница показывает краткую справку о том, что настройки живут в config.json и применяются после перезапуска.
Groups mappings
Таблица правил метода A: DN группы, роль, область, дата создания. Есть поиск по DN, фильтры по роли, типу области и конкретной области, сортировка и постраничный вывод по 50 строк.
Создание и редактирование — в боковой панели: DN группы, роль (список фильтруется по выбранному типу области), тип области и сама область с поиском по мере ввода.
Scope mappings
Реестр псевдонимов метода B: псевдоним → клиент или команда. Псевдоним проверяется по шаблону ^[a-z0-9_-]{1,255}$ ещё до отправки формы.
Synchronizations
Состояние и история:
- Шапка состояния — включённость, хост каталога, интервал, текущий уровень журнала, статус последнего цикла, время последнего запуска и последнего успеха, счётчики. Ошибка последнего цикла показывается отдельным предупреждением.
- Кнопка Force sync — запустить обход немедленно, не дожидаясь таймера. В кластере запрос автоматически перенаправляется на узел-лидер, поэтому обращаться можно к любому узлу.
- Журнал синхронизаций — фильтры по уровню, коду события, идентификатору запуска и времени. У каждой строки есть копирование в YAML: полная запись со всеми деталями попадает в буфер обмена одним нажатием — удобно вставлять в тикет.
Изменения не применяются мгновенно. Создание, изменение и удаление правила отвечают «применится при следующей синхронизации». Права пересчитываются на очередном цикле — по умолчанию в пределах 10 минут. Удаление правила отзывает выданные им привязки тоже на следующем цикле.
Журнал синхронизаций
Строка состояния отвечает на вопрос «как каталог себя чувствует сейчас». Журнал отвечает на вопрос «что произошло в том цикле, который упал час назад» — и именно он превращает счётчик values_skipped: 5 в пять конкретных строк с значением, пользователем и причиной.
Уровни
sync_log_level — это лестница: каждый уровень включает всё, что записывает уровень ниже.
| Уровень | Что записывается |
|---|---|
off | Ничего. |
error | Только отказы цикла и обхода: каталог недоступен, реестр не прочитан, деактивация отклонена предохранителем. |
warn (по умолчанию) | Плюс проблемы отдельных записей: пропущенное значение сопоставления, нечитаемое имя, конфликт с локальной учётной записью, неудачное создание пользователя. |
info | Плюс жизненный цикл цикла и все изменения: созданные, обновлённые, отключённые и восстановленные пользователи, изменения привязок. |
На уровне по умолчанию успешный цикл не пишет ничего. Пустой журнал — это нормальное состояние, а не признак поломки; интерфейс показывает подсказку с текущим уровнем, если журнал пуст.
Один запуск ограничен 5000 событиями: по достижении лимита записывается одна строка events_suppressed, и остаток цикла в журнал не пишется. Записи старше sync_log_retention_days удаляются раз в два часа на узле-лидере.
Запись журнала — операция best-effort: её сбой попадает в лог узла и никогда не влияет на сам цикл синхронизации.
Метрики
Восемь серий Prometheus доступны на /metrics, все с меткой node_id (обход выполняет только лидер, а лидерство перемещается):
| Метрика | Тип | Метки |
|---|---|---|
ldap_sync_runs_total | counter | result = ok | partial | error | aborted |
ldap_sync_duration_seconds | histogram | — |
ldap_sync_last_success_timestamp_seconds | gauge | — |
ldap_sync_in_progress | gauge | — |
ldap_sync_users_total | counter | outcome = seen | created | updated | disabled | reenabled | skipped | failed |
ldap_sync_bindings_total | counter | op = created | deleted |
ldap_sync_values_skipped_total | counter | — |
ldap_sync_directory_over_limit | gauge | — |
Стройте тревогу на свежести, а не на счётчиках запусков: time() - ldap_sync_last_success_timestamp_seconds. Счётчик запусков остаётся неизменным и когда всё хорошо, и когда таймер вообще перестал срабатывать, — эти два случая по нему неразличимы.
Все серии инициализируются нулями при старте, поэтому rate(ldap_sync_users_total{outcome="failed"}[15m]) > 0 корректно работает и на узле, который ещё не выполнил ни одного цикла. Если каталог не настроен, серии ldap_sync_* не публикуются вовсе.
Ограничение на число учётных записей
Число учётных записей из каталога ограничено редакцией:
| Редакция | Лимит активных LDAP-аккаунтов |
|---|---|
| Community | 50 |
| Pro | 200 |
| Enterprise | 1000 |
Ограничение считает только учётные записи. Правила сопоставления, псевдонимы и привязки ролей не ограничены ничем.
Лимит действует в трёх местах:
- При обходе каталога — сколько записей обрабатывается за цикл. Если каталог вернул больше записей, чем разрешено, синхронизируются первые N в алфавитном порядке имён; остальные попадают в счётчик
users_skipped. Никто при этом не деактивируется — превышение лимита не отзывает права у уже заведённых пользователей. В журнал пишется строкаdirectory_size_limit_reached, аldap_sync_directory_over_limitпереходит в 1. - При обходе каталога — сколько учётных записей становится активными. Цикл может создать (или снова включить) ровно столько записей, сколько остаётся до лимита: «лимит минус число активных учётных записей из каталога». Поэтому запись может попасть в первые N по алфавиту и всё равно получить отказ, если место уже занято — в журнал пишется одна строка
directory_cap_reachedза цикл. Запись, созданная сразу отключённой (ограниченная фильтром или группой), место не занимает; когда каталог снимает ограничение, её повторное включение расходует место так же, как создание. Если число учётных записей не удалось прочитать, за цикл не создаётся и не включается ничего, а в журнале появляетсяdirectory_count_failed— это сбой базы, а не достигнутый лимит. - При первом входе нового пользователя. Если лимит уже достигнут, учётная запись не создаётся и вход отклоняется — с тем же ответом, что и при неверном пароле. Отказ виден в журнале как
jit_create_refused.
Второй случай влияет на вход: пользователь получает обычную ошибку авторизации и по ней ничего не поймёт. Если новые сотрудники внезапно не могут войти, проверьте ldap_sync_directory_over_limit и журнал синхронизаций.
Правильное решение при превышении — сузить user_filter, чтобы каталог возвращал только нужную аудиторию, либо перейти на старшую редакцию. Отключённая учётная запись освобождает место на следующем цикле.
Что происходит при входе
- Meridian находит учётную запись и смотрит, кто отвечает за её пароль. Для
local— прежняя проверка хеша, каталог не участвует вовсе. - Для
ldapпустой пароль отклоняется до обращения к сети. Затем выполняется bind сервисной учётной записи, поиск пользователя поuser_filterи bind уже от имени найденного DN. - Ноль совпадений или больше одного — отказ: неоднозначный каталог не угадывается.
- Любая ошибка сводится к одному ответу, поэтому «нет такого пользователя», «неверный пароль» и «каталог недоступен» снаружи неразличимы.
Пользователь, которого обход ещё не завёл, может войти сразу: учётная запись создаётся на месте — пустая, без единой привязки. Права приходят со следующим циклом. Ограничения (restricted_user_filter, restricted_group_dns) на этом пути проверяются немедленно, потому что отвечать по флагу «отключён» ещё нечему.
HTTP Basic ведёт себя иначе: он находит только уже существующие записи и не создаёт новых. Пользователь, добавленный в каталог между циклами, может войти в панель управления сразу, но будет получать 401 на вызовах API и inference — до следующей синхронизации. Создавать учётные записи из неаутентифицированного inference-трафика было бы худшим из двух вариантов.
Проверка Basic кешируется на 60 секунд для успеха и на 10 — для отказа, чтобы каждый вызов inference не превращался в обращение к каталогу. Сама запись пользователя перечитывается всегда, поэтому отключение учётной записи вступает в силу со следующего же запроса.
Отзыв доступа
| Что сделали в каталоге | Когда подействует |
|---|---|
| Удалили или отключили запись пользователя | Немедленно для новых входов — bind отклоняет каталог. Учётная запись Meridian отключается, а сессии сбрасываются на следующем цикле. |
Перевели в restricted_group_dns / под restricted_user_filter | На следующем цикле: учётная запись отключается, все её сессии удаляются. До этого момента — до 10 минут — существующая сессия продолжает работать. |
| Убрали из сопоставленной группы | На следующем цикле: привязки, выданные этим правилом, отзываются. |
| Удалили правило сопоставления в Meridian | На следующем цикле, там же. |
Ручное удаление LDAP-привязки через RBAC API — временная мера. Следующий цикл создаст её заново, потому что каталог всё ещё говорит, что она должна существовать. То же самое с удалением самого пользователя через API: запись вернётся на следующем цикле под новым идентификатором. Чтобы отозвать доступ по-настоящему, меняйте каталог или правило.
Ручные привязки (source: manual) синхронизация не трогает никогда — она владеет только строками с source: ldap.
Предохранители
Опечатка в user_base превращает здоровый каталог в «ноль пользователей», а опечатка в attribute_username — в «записи, чьи имена нечитаемы», что по числу записей выглядит совершенно нормально. Без защиты любой из этих случаев отключил бы все LDAP-учётные записи установки разом.
- Пропорциональный клапан деактивации. Цикл, который отключил бы больше половины активных LDAP-аккаунтов (и больше двух), отклоняется, а сам цикл помечается неуспешным — молчаливый предохранитель не предохранитель. Обычное увольнение далеко ниже порога и обрабатывается как обычно.
allow_deactivate_all: true— это осознанное заявление, что потерять большую часть каталога разом допустимо. - Пустой обход групп — это отсутствие ответа, а не «никто ни в чём не состоит». В обратном режиме (
group_search_enabled) цикл прерывается до записи чего бы то ни было, если обход не разрешил ни одного членства, а сопоставления групп или ограничивающие группы настроены. - Неоднозначное имя не идентифицирует никого. Если две записи каталога дают одно имя пользователя, обе пропускаются с ошибкой, а имя помечается как увиденное — существующая учётная запись не деактивируется и сохраняет права, выданные последним однозначным циклом.
- Ошибка проверки — не приговор. Если фильтр не удалось вычислить или проверку области не удалось выполнить, запись оставляется полностью в покое: «не смог решить» никогда не приводит к отзыву прав.
Ограничения
| Ограничение | Подробности |
|---|---|
| Настройки подключения не редактируются из интерфейса | Блок ldap — файловый и применяется при перезапуске. В панели управления настраиваются только правила сопоставления. |
| Вложенные группы не раскрываются | Учитывается только прямое членство. |
| Один адрес каталога | Ни списка реплик, ни отказоустойчивого переключения; переходы по referral не выполняются. |
| Переименование = новая учётная запись | Идентичность привязана к имени пользователя: jdoe → jane выглядит как уход одного и приход другого, с новыми привязками и новым идентификатором. |
| Нет автоматического создания клиентов и команд | Правило или псевдоним, ссылающиеся на несуществующую область, отклоняются при сохранении. |
Нет области user | Доступны глобальная область, клиент и команда. |
| StartTLS не поддерживается | Только ldap:// и ldaps://; схема starttls:// — ошибка запуска, а не молчаливое понижение защиты. |
| Нет блокировки учётной записи на стороне Meridian | Ограничивается только частота попыток; блокировка — политика самого каталога. |
| Нет полнотекстового поиска по журналу | Фильтры — уровень, код, идентификатор запуска и диапазон времени. |
Обратный режим (group_search_enabled) не покрыт тестами на реальном каталоге | Проверен только на моках; сквозные тесты работают в прямом режиме memberOf. |
Проверка настройки
Запустите Meridian с блоком ldap. Неверная конфигурация не даст процессу подняться — это ожидаемое поведение. Первый обход каталога при старте не выполняется: недоступный каталог никогда не блокирует запуск.
Откройте Settings → LDAP → Synchronizations. Шапка покажет хост каталога и статус idle — обход ещё не запускался.
Заведите клиентов и команды, на которые будут ссылаться правила, затем добавьте правила на вкладке Groups mappings и псевдонимы на вкладке Scope mappings.
Нажмите Force sync вместо ожидания таймера и обновите состояние. Статус ok и ненулевой users_seen означают, что каталог прочитан.
Проверьте результат — привязки, выданные каталогом, видны в Governance → Roles & Permissions и через GET /api/v1/rbac/bindings?source=ldap. Если values_skipped не ноль, откройте журнал синхронизаций: там будет по строке на каждое пропущенное значение с причиной.