Зачем LDAP
Lightweight Directory Access Protocol (RFC 4511), стандарт доступа к иерархическому справочнику: пользователи, группы, hosts, сертификаты, конфиги приложений. Появился в 90-х как упрощение X.500. Сегодня используется для:
- Centralized auth: Active Directory (Microsoft), FreeIPA (Red Hat), OpenLDAP / 389-DS (linux-only)
- Адресная книга email-серверов (
ou=Peopleдля autocomplete в Outlook/Thunderbird) - Конфиги приложений и DNS-данных в одном дереве
- Хранение SSH-ключей пользователей (с patches/openssh-ldap)
Не БД общего назначения. Чтение быстрое, запись редкая оптимизирован под "много read, мало write".
Иерархическая структура
Дерево объектов (DIT, Directory Information Tree):
dc=example,dc=com ← корень = domain
├─ ou=People
│ ├─ uid=alice (objectClass: posixAccount, inetOrgPerson)
│ ├─ uid=bob
│ └─ uid=carol
├─ ou=Groups
│ ├─ cn=admins
│ └─ cn=developers
└─ ou=Services
└─ cn=ldap-replica
- DN (Distinguished Name), полная "координата":
uid=alice,ou=People,dc=example,dc=com - RDN (Relative DN), последний компонент:
uid=alice - dc (domainComponent), ou (organizationalUnit), uid, cn (commonName), атрибуты, формирующие DN
Чтение справа-налево: dc=com → dc=example → ou=People → uid=alice.
Bind, аутентификация
Перед запросами клиент bind'ится:
- Anonymous bind, без credentials, доступ только к public-данным
- Simple bind, DN + пароль в plaintext (требует TLS!)
- SASL bind, через Kerberos/EXTERNAL/DIGEST-MD5/PLAIN
# Anonymous (для тестов)
ldapsearch -x -H ldap://ldap.example.com -b "dc=example,dc=com" "(uid=alice)"
# Simple bind
ldapsearch -x -D "uid=alice,ou=People,dc=example,dc=com" -W \
-H ldaps://ldap.example.com -b "dc=example,dc=com" "(objectClass=*)"
# SASL Kerberos (когда есть TGT)
ldapsearch -Y GSSAPI -H ldap://ldap.example.com -b "dc=example,dc=com" "(uid=alice)"
-x, simple bind-D, bind DN-W, спросить пароль интерактивно-H, URL (ldap://,ldaps://)-Y, SASL mechanism
Объекты и атрибуты
Каждый объект имеет один или несколько objectClass (определяет
schema), которые задают обязательные и опциональные атрибуты:
dn: uid=alice,ou=People,dc=example,dc=com
objectClass: top
objectClass: posixAccount # uidNumber, gidNumber, homeDirectory
objectClass: inetOrgPerson # mail, sn, givenName, telephoneNumber
objectClass: shadowAccount # shadowLastChange и подобное
uid: alice
cn: Alice Wonderland
sn: Wonderland
givenName: Alice
uidNumber: 1001
gidNumber: 1001
homeDirectory: /home/alice
loginShell: /bin/bash
mail: alice@example.com
userPassword: {SSHA}xxxx # hashedГлавные классы для пользователя в Linux:
posixAccount, uid/gid, чтобы NSS+PAM могли использоватьinetOrgPerson, почта, имяshadowAccount, password expiry
LDAP-фильтры
Поиск через LDAP Filter (RFC 4515):
(uid=alice) # точное совпадение
(cn=Alice*) # wildcard
(mail=*@example.com) # суффикс
(&(uid=alice)(memberOf=cn=admins,ou=Groups,dc=example,dc=com)) # AND
(|(uid=alice)(uid=bob)) # OR
(!(loginShell=/bin/false)) # NOT
(uidNumber>=1000) # >= (только для int)
(objectClass=posixAccount)
Все спецсимволы в значении надо экранировать (\28 для ().
ldapsearch, основная утилита
# Все posixAccount'ы с uidNumber >= 1000
ldapsearch -x -H ldaps://ldap -b "dc=example,dc=com" \
"(&(objectClass=posixAccount)(uidNumber>=1000))" uid uidNumber cn
# Только конкретные атрибуты
ldapsearch ... "(uid=alice)" mail telephoneNumber
# ВСЁ что есть на объекте
ldapsearch ... "(uid=alice)" "+" # operational attributes
# Размер дерева
ldapsearch -x -H ldap://srv -b "" -s base "(objectclass=*)" # rootDSE
LDIF, формат экспорта/импорта
LDIF (LDAP Data Interchange Format), текстовый формат:
dn: ou=Test,dc=example,dc=com
changetype: add
objectClass: organizationalUnit
ou: Test
dn: uid=alice,ou=Test,dc=example,dc=com
changetype: add
objectClass: posixAccount
objectClass: inetOrgPerson
uid: alice
cn: Alice
sn: Wonderland
uidNumber: 1001
gidNumber: 1001
homeDirectory: /home/alice
ldapadd -D "cn=admin,dc=example,dc=com" -W -f users.ldif
ldapmodify -f changes.ldif
ldapdelete "uid=alice,ou=People,dc=example,dc=com"
OpenLDAP vs 389-DS vs FreeIPA vs AD
| Признак | OpenLDAP (slapd) | 389-DS | FreeIPA | Active Directory |
|---|---|---|---|---|
| Default где | Debian/Ubuntu | RHEL | RHEL/Fedora | Windows Server |
| Конфиг | cn=config (LDIF) | dse.ldif + console | веб-UI + CLI | Windows GUI |
| Replication | mirror/N-way multimaster | multi-supplier | suppliers + RO replicas | DC + RODC |
| Kerberos | отдельно (MIT) | можно | встроен + DNS + Kerberos + CA | встроен |
| Сложность setup | средняя | средняя | низкая (всё-в-одном) | низкая (на Windows) |
| Linux-клиенты | sssd / nss-ldap | sssd | sssd (родная интеграция) | sssd через realmd |
Для нового self-hosted на Linux, FreeIPA (если RHEL/Fedora) или 389-DS standalone. OpenLDAP, legacy, но всё ещё распространён.
TLS, обязательно
Без TLS пароли при simple-bind идут в plaintext, tcpdump их
достанет. Варианты:
- ldaps:// на 636, implicit TLS
- STARTTLS на 389, opportunistic, но клиент может не запросить
Современный сервер должен принимать только ldaps:// или запрещать
simple-bind без STARTTLS:
# OpenLDAP
olcSecurity: ssf=128
Когда что-то пошло не так
Invalid credentials (49), неверный DN или пароль. Бывает также если запись не имеет userPassword вообще.Insufficient access rights (50), bind ок, но ACL-rule запрещает чтение/запись. СмотриolcAccess.Server is unwilling to perform (53), operation запрещена конфигом (например write на read-only replica).Constraint violation (19), пароль не прошёл pwd-policy (длина, история).Object class violation (65), добавляешь атрибут которого нет в objectClass'ах или удаляешь обязательный.- Чудовищно медленный поиск, нет индекса по атрибуту.
slapindexпослеolcDbIndex: cn,uid eq. - TLS handshake падает, старые шифры на одной стороне или самоподписанный CA не в trust store клиента.
Альтернативы и связанные
- sssd, Linux-клиент, кэширует LDAP/AD/Kerberos для NSS+PAM
- nslcd / nss-ldap, старый клиент, deprecated в пользу sssd
- Apache Directory Studio, GUI для просмотра/правки дерева
- OpenID Connect / OAuth2, современный пользовательский SSO (приложения уходят с LDAP-аутентификации в OIDC, но LDAP остаётся как backing user-store)