Windows Server для самых маленьких. Часть 5: LDAP изнутри — каталог глазами ADSI Edit
Всё, что читает или меняет каталог — оснастка Active Directory Users and Computers, модуль PowerShell, Exchange, sssd на Linux, система заявок, которая «интегрирована с AD», — говорит с контроллером по LDAP. Active Directory и есть LDAP-сервер: версии 3, по RFC 4510–4519, с расширениями Microsoft, описанными в открытой спецификации MS-ADTS. Оснастки показывают каталог отредактированным и переведённым; LDAP показывает его как есть. В этой части мы будем смотреть именно так, и это единственная часть серии, где графические инструменты — ldp.exe и ADSI Edit — не упоминание, а предмет: они показывают сырой каталог, и рядом с каждым их действием мы будем ставить ту же операцию в PowerShell.
Вторая половина части — о том, что в каталоге ломается при неосторожной правке и не восстанавливается откатом. Каталог хранит и данные, и собственную конфигурацию, и конфигурация защищена от администратора не сильнее, чем данные.
Протокол
LDAP — двоичный протокол поверх TCP, сообщения кодируются ASN.1 BER. Клиент открывает соединение на порт 389 (или 636 с TLS, 3268/3269 для глобального каталога) и отправляет запросы; каждый запрос несёт номер сообщения, и ответы приходят с тем же номером, так что по одному соединению можно вести несколько запросов одновременно. Операций девять:
| Операция | Что делает |
|---|---|
| Bind | аутентификация соединения: кто вы. Все последующие операции выполняются с правами этого субъекта |
| Unbind | закрыть соединение |
| Search | найти объекты: база, глубина, фильтр, список атрибутов. Единственная операция чтения: «прочитать объект» — это поиск с глубиной base |
| Compare | проверить, равен ли атрибут значению, не читая его (так проверяют членство в группе без права читать member) |
| Add, Delete | создать и удалить объект |
| Modify | добавить, заменить или удалить значения атрибутов |
| ModifyDN | переименовать или перенести объект |
| Extended | расширенные операции: StartTLS, WhoAmI и другие по OID |
Плюс controls — дополнения к любой операции, тоже по OID: постраничный вывод (1.2.840.113556.1.4.319, без него контроллер отдаёт не больше 1000 объектов), сортировка на сервере, показать удалённые объекты (1.2.840.113556.1.4.417), вернуть только часть дескриптора безопасности (1.2.840.113556.1.4.801), уведомлять об изменениях. Какие контролы поддерживает сервер, он сообщает в RootDSE, к которому мы скоро придём.
Bind бывает двух видов. Simple — DN или UPN и пароль открытым текстом. Без TLS это передача пароля по сети как есть, и контроллер по умолчанию отвергает simple bind без TLS, а каждую попытку записывает событием 2889 в журнал Directory Service: «клиент такой-то попытался привязаться без подписи». SASL — согласование механизма: GSS-SPNEGO (Kerberos с откатом на NTLM; им пользуется всё в Windows), GSSAPI (чистый Kerberos; им пользуются Linux-клиенты), DIGEST-MD5, EXTERNAL (по клиентскому сертификату TLS). После SASL-привязки по Kerberos соединение ещё и подписывается, и контроллер требует этого: настройка «LDAP server signing requirements» на современных контроллерах стоит в Require, а channel binding, связывающий TLS-сессию с аутентификацией, — в When supported.
Поиск задаётся четырьмя вещами: база (DN, с которого начинать), глубина (base — только сам объект, onelevel — его прямые потомки, subtree — всё поддерево), фильтр и список атрибутов (пусто — все нескрытые; * — все пользовательские, + — операционные). Сервер отвечает потоком записей SearchResultEntry и завершающим SearchResultDone с кодом результата.
Имя объекта
Из прошлых частей: у объекта есть DN. Присмотримся.
CN=Alice Smith,OU=Sales,OU=Departments,DC=corp,DC=example,DC=com
└─── RDN ────┘ └────────── DN родителя ──────────────────────────┘
RDN (relative distinguished name) — имя объекта внутри родителя, атрибут=значение. Для пользователей, групп и компьютеров это CN, для подразделений OU, для компонентов домена DC. RDN уникален среди соседей, DN — во всём лесу. Атрибут, по которому строится RDN, у каждого класса задан в схеме (rDNAttID), а само значение RDN хранится ещё и в атрибуте name: у пользователя cn и name всегда совпадают.
Спецсимволы в значении RDN экранируются обратной косой чертой: запятая, плюс, кавычка, косая черта, <, >, ;, =, пробел или # в начале, пробел в конце. Пользователь «Smith, Alice» — это CN=Smith\, Alice,OU=Sales,…. Поэтому в скриптах DN лучше не собирать из строк, а брать у объекта.
Регистр в DN не важен, пробелы после запятых допустимы, но контроллер хранит и возвращает DN в одном каноническом виде. Есть и другой вид записи — canonical name: corp.example.com/Departments/Sales/Alice Smith, слева направо; его показывает ADSI Edit и возвращает вычисляемый атрибут canonicalName.
Три идентификатора, не зависящих от расположения, были в части 3: objectGUID (у всех объектов, неизменяем), objectSid (у субъектов безопасности), sAMAccountName и userPrincipalName. В LDAP по GUID и SID можно обращаться напрямую: <GUID=7a1b…> и <SID=S-1-5-21-…> вместо DN в качестве базы поиска.
Атрибуты
Атрибут — это имя, синтаксис и одно или несколько значений. Что здесь стоит знать.
Синтаксисы. У каждого атрибута в схеме задан тип значения, и от него зависит, как значение сравнивается и как его читать:
| Синтаксис | Примеры | Как выглядит |
|---|---|---|
| Unicode String | cn, displayName, mail |
строка, сравнение без учёта регистра |
| Integer | userAccountControl, primaryGroupID, logonCount |
32-битное число |
| Large Integer / Interval | pwdLastSet, lastLogonTimestamp, accountExpires, lockoutTime, badPasswordTime |
64-битное число: FILETIME, сотни наносекунд с 1 января 1601 UTC; 0 означает «никогда» или «требуется», 9223372036854775807 — «не истекает» |
| Generalized Time | whenCreated, whenChanged |
строка 20261015093011.0Z |
| Distinguished Name | member, memberOf, manager, managedBy |
DN другого объекта |
| Octet String | objectGUID, objectSid, userCertificate, thumbnailPhoto |
байты |
| Boolean | isDeleted, showInAdvancedViewOnly |
TRUE / FALSE |
| NT Security Descriptor | nTSecurityDescriptor |
двоичный дескриптор безопасности: владелец, DACL, SACL |
Две даты в двух разных форматах у одного объекта — исторический факт, с которым приходится жить: whenCreated читается глазами, а pwdLastSet требует [DateTime]::FromFileTime().
Битовые поля. userAccountControl — 32 флага в одном числе, и это главный атрибут учётной записи:
| Бит | Hex | Флаг |
|---|---|---|
| 1 | 0x0002 | ACCOUNTDISABLE — отключена |
| 4 | 0x0010 | LOCKOUT — только для чтения, реальная блокировка в lockoutTime |
| 5 | 0x0020 | PASSWD_NOTREQD — пароль не обязателен |
| 9 | 0x0200 | NORMAL_ACCOUNT — обычный пользователь |
| 12 | 0x1000 | WORKSTATION_TRUST_ACCOUNT — компьютер |
| 13 | 0x2000 | SERVER_TRUST_ACCOUNT — контроллер домена |
| 16 | 0x10000 | DONT_EXPIRE_PASSWORD — пароль не истекает |
| 19 | 0x80000 | TRUSTED_FOR_DELEGATION — неограниченное делегирование Kerberos |
| 22 | 0x400000 | DONT_REQ_PREAUTH — Kerberos без предварительной аутентификации |
Обычный включённый пользователь — 512, отключённый — 514, с неистекающим паролем — 66048. Оснастка показывает это флажками, ADSI Edit — числом, и там ошибиться в арифметике проще, чем кажется; в PowerShell у Set-ADUser есть именованные параметры -Enabled, -PasswordNeverExpires, которые считают за вас.
Связанные атрибуты. member у группы и memberOf у пользователя — одна связь, записанная дважды. Хранится только прямая ссылка (member, forward link); обратная (memberOf, back link) вычисляется из неё и не реплицируется отдельно. Отсюда два следствия: изменить memberOf напрямую нельзя (только через member группы), и memberOf не показывает членство в группах других доменов, если контроллер не глобальный каталог. То же с manager/directReports.
Вычисляемые атрибуты. Их нет в базе, контроллер считает их при чтении и только по явному запросу: canonicalName, tokenGroups (все SID групп пользователя, включая вложенные), msDS-User-Account-Control-Computed (здесь честный флаг LOCKOUT и признак истёкшего пароля), allowedAttributes, msDS-ReplAttributeMetaData.
Нереплицируемые атрибуты. lastLogon, badPwdCount, badPasswordTime, logonCount хранятся на каждом контроллере отдельно и между ними не ходят: иначе каждый вход пользователя порождал бы репликацию по всему лесу. Чтобы узнать, когда пользователь входил последний раз, нужно опросить все контроллеры и взять максимум. Для грубой оценки есть lastLogonTimestamp: он реплицируется, но обновляется не чаще раза в 9–14 дней, и годится для поиска учётных записей, которыми не пользовались полгода, а не для расследований.
Метаданные репликации. У каждого объекта есть uSNCreated и uSNChanged — порядковые номера изменений на том контроллере, с которого вы читаете (на разных контроллерах они разные), whenChanged, и для каждого атрибута — версия, время и контроллер-источник последнего изменения:
PS C:\> Get-ADReplicationAttributeMetadata -Object 'CN=Alice Smith,OU=Sales,OU=Departments,DC=corp,DC=example,DC=com' -Server dc1 |
Where-Object AttributeName -in 'pwdLastSet','userAccountControl','mail' |
Format-Table AttributeName, Version, LastOriginatingChangeTime, LastOriginatingChangeDirectoryServerIdentity -AutoSize
AttributeName Version LastOriginatingChangeTime LastOriginatingChangeDirectoryServerIdentity
------------- ------- ------------------------- --------------------------------------------
mail 2 14.10.2026 16:21:07 CN=NTDS Settings,CN=DC1,CN=Servers,CN=Office,CN=Sites,CN=Configuration,DC=corp,DC=example,DC=com
pwdLastSet 3 15.10.2026 9:30:11 CN=NTDS Settings,CN=DC1,CN=Servers,CN=Office,CN=Sites,CN=Configuration,DC=corp,DC=example,DC=com
userAccountControl 1 01.10.2026 12:00:45 CN=NTDS Settings,CN=DC1,CN=Servers,CN=Office,CN=Sites,CN=Configuration,DC=corp,DC=example,DC=com
Это ответ на вопрос «кто и когда поменял пароль этому пользователю» без аудита: версия 3 — пароль менялся трижды, последний раз 15 октября через dc1. В части про репликацию мы увидим, как контроллеры по этим метаданным решают конфликты.
Схема
Схема — тоже объекты, в разделе CN=Schema,CN=Configuration,…, двух классов: classSchema описывает классы, attributeSchema — атрибуты.
PS C:\> $schema = (Get-ADRootDSE).schemaNamingContext
PS C:\> Get-ADObject -SearchBase $schema -LDAPFilter '(lDAPDisplayName=pwdLastSet)' -Properties attributeID, attributeSyntax, oMSyntax, isSingleValued, searchFlags, isMemberOfPartialAttributeSet, systemFlags, schemaIDGUID
attributeID : 1.2.840.113556.1.4.96
attributeSyntax : 2.5.5.16
DistinguishedName : CN=Pwd-Last-Set,CN=Schema,CN=Configuration,DC=corp,DC=example,DC=com
isMemberOfPartialAttributeSet : False
isSingleValued : True
lDAPDisplayName : pwdLastSet
Name : Pwd-Last-Set
oMSyntax : 65
searchFlags : 0
systemFlags : 17
attributeID — OID атрибута, глобально уникальный: 1.2.840.113556 — это Microsoft, .1.4 — атрибуты AD. attributeSyntax 2.5.5.16 с oMSyntax 65 — Large Integer. isMemberOfPartialAttributeSet — попадает ли в глобальный каталог. searchFlags — битовое поле: 1 — атрибут индексирован, 4 — участвует в ANR (поиске «по любому имени», о нём ниже), 8 — сохраняется при удалении объекта, 128 — конфиденциальный (читать могут только те, у кого явное право Control Access). У класса список обязательных (mustContain, systemMustContain) и допустимых (mayContain) атрибутов, родительский класс (subClassOf) и вспомогательные классы (auxiliaryClass), чьи атрибуты он тоже получает.
Схема расширяема: Exchange добавляет полторы тысячи атрибутов и классов, Entra Connect — десяток, свои расширения можно описать в LDIF-файле и загрузить ldifde. Два факта о расширении, которые нужно знать до того, как его сделать. Расширение необратимо: созданный класс или атрибут нельзя удалить, можно лишь объявить неработающим (isDefunct = TRUE) и переиспользовать имя. И оно общее на лес: расширение, загруженное для одного приложения в одном домене, появляется на всех контроллерах всех доменов. Поэтому группа Schema Admins по умолчанию пуста, и администратора в неё добавляют на время операции.
RootDSE
RootDSE (DSA-specific entry) — псевдообъект с пустым DN, корень дерева конкретного сервера. Он отвечает на поиск с глубиной base без привязки: это единственное, что контроллер отдаёт анонимно по умолчанию, и именно так клиенты узнают, с кем имеют дело.
PS C:\> Get-ADRootDSE -Server dc1 | Select-Object dnsHostName, serverName, defaultNamingContext, highestCommittedUSN, currentTime, isSynchronized, supportedSASLMechanisms, domainControllerFunctionality
dnsHostName : dc1.corp.example.com
serverName : CN=DC1,CN=Servers,CN=Office,CN=Sites,CN=Configuration,DC=corp,DC=example,DC=com
defaultNamingContext : DC=corp,DC=example,DC=com
highestCommittedUSN : 41237
currentTime : 15.10.2026 9:41:08
isSynchronized : True
supportedSASLMechanisms : {GSSAPI, GSS-SPNEGO, EXTERNAL, DIGEST-MD5}
domainControllerFunctionality : Windows2025
highestCommittedUSN — последний номер изменения на этом контроллере, по нему партнёры репликации понимают, что у него есть нового. isSynchronized — завершил ли контроллер начальную синхронизацию после старта; пока FALSE, он не объявляет себя клиентам. supportedControl и supportedCapabilities — списки OID с тем, что сервер умеет: по наличию 1.2.840.113556.1.4.800 клиент узнаёт, что перед ним Active Directory, а не другой LDAP-сервер.
Фильтры
Фильтр поиска — выражение в префиксной записи по RFC 4515. Элементы:
(attr=value) равно; для строк без учёта регистра
(attr=*) атрибут есть (presence)
(attr=Ali*) подстрока; * может стоять в начале, середине и конце
(attr>=value) больше или равно; (attr<=value) меньше или равно; строгих < и > нет
(&(f1)(f2)...) И
(|(f1)(f2)...) ИЛИ
(!(f1)) НЕ
Спецсимволы в значении экранируются шестнадцатеричным кодом: \2a для *, \28 и \29 для скобок, \5c для \, \00 для нуля.
Два правила сравнения Microsoft, без которых фильтры по AD не написать:
- Битовое И
1.2.840.113556.1.4.803и битовое ИЛИ1.2.840.113556.1.4.804для битовых полей. Отключённые учётные записи:(userAccountControl:1.2.840.113556.1.4.803:=2). - Цепочка
1.2.840.113556.1.4.1941для DN-атрибутов: сравнение идёт транзитивно. Все члены группы, включая вложенные:(memberOf:1.2.840.113556.1.4.1941:=CN=Domain Admins,CN=Users,DC=corp,DC=example,DC=com). Это единственный способ получить вложенное членство одним запросом, и он дорогой.
Правило приближённого сравнения ~= из стандарта контроллер принимает, но выполняет как обычное равенство.
Несколько рабочих фильтров:
(&(objectCategory=person)(objectClass=user)) все пользователи, без компьютеров
(&(objectCategory=computer)(operatingSystem=*Server*)) серверы
(&(objectCategory=person)(objectClass=user)(!(userAccountControl:1.2.840.113556.1.4.803:=2))) включённые
(&(objectClass=user)(userAccountControl:1.2.840.113556.1.4.803:=65536)) пароль не истекает
(&(objectClass=user)(pwdLastSet=0)) должны сменить пароль при входе
(&(objectClass=user)(lockoutTime>=1)) заблокированные (точнее — те, у кого стоит время блокировки)
(&(objectCategory=computer)(lastLogonTimestamp<=133700000000000000)) не входили с некоторой даты в FILETIME
(anr=ali) ANR: поиск по cn, givenName, sn, displayName, sAMAccountName и ещё нескольким сразу
Почему objectCategory=person вместе с objectClass=user: objectClass многозначный (top; person; organizationalPerson; user) и по нему же проходят компьютеры, потому что класс computer — наследник user; objectCategory однозначный и индексированный, по нему поиск быстрее. ANR (ambiguous name resolution) — то, что делает поле поиска в Outlook: один фильтр разворачивается в ИЛИ по нескольким индексированным атрибутам с подстрокой в начале.
Модуль ActiveDirectory принимает фильтр напрямую: Get-ADUser -LDAPFilter '(&(objectCategory=person)(pwdLastSet=0))'. Его собственный -Filter 'Enabled -eq $true -and PasswordNeverExpires -eq $true' — удобная обёртка, которую модуль переводит в LDAP перед отправкой; что именно он отправил, видно в журнале контроллера, о чём в конце.
ldp.exe
ldp.exe — LDAP-клиент из состава инструментов AD DS: есть на каждом контроллере и ставится с RSAT. Он показывает ровно то, что идёт по протоколу, без перевода.
Connection → Connect: сервер dc1.corp.example.com, порт 389 (или 636 с флажком SSL). В правой панели — весь RootDSE, который сервер отдал анонимно. Connection → Bind: по умолчанию «Bind as currently logged on user» через GSS-SPNEGO, и в панели появится Authenticated as: 'CORP\alice.admin'. View → Tree, базовый DN пустой или DC=corp,DC=example,DC=com — слева дерево, которое раскрывается запросами onelevel, а двойной клик по объекту делает поиск base и выводит атрибуты:
Expanding base 'CN=Alice Smith,OU=Sales,OU=Departments,DC=corp,DC=example,DC=com'...
Getting 1 entries:
Dn: CN=Alice Smith,OU=Sales,OU=Departments,DC=corp,DC=example,DC=com
accountExpires: 9223372036854775807 (never);
badPasswordTime: 0 (never);
badPwdCount: 0;
cn: Alice Smith;
codePage: 0;
countryCode: 0;
displayName: Alice Smith;
distinguishedName: CN=Alice Smith,OU=Sales,OU=Departments,DC=corp,DC=example,DC=com;
dSCorePropagationData: 0x0 = ( );
givenName: Alice;
instanceType: 0x4 = ( WRITE );
lastLogonTimestamp: 15.10.2026 9:02:11 W. Europe Daylight Time;
logonCount: 14;
memberOf: CN=Sales,OU=Groups,DC=corp,DC=example,DC=com;
name: Alice Smith;
objectCategory: CN=Person,CN=Schema,CN=Configuration,DC=corp,DC=example,DC=com;
objectClass (4): top; person; organizationalPerson; user;
objectGUID: 9c3f1a2b-7d4e-4f60-8a1b-2c3d4e5f6a7b;
objectSid: S-1-5-21-3623811015-3361044348-30300820-1104;
primaryGroupID: 513 = ( GROUP_RID_USERS );
pwdLastSet: 15.10.2026 9:30:11 W. Europe Daylight Time;
sAMAccountName: alice;
sAMAccountType: 805306368 = ( NORMAL_USER_ACCOUNT );
sn: Smith;
userAccountControl: 0x200 = ( NORMAL_ACCOUNT );
userPrincipalName: alice@corp.example.com;
uSNChanged: 41190;
uSNCreated: 12805;
whenChanged: 15.10.2026 9:30:11 W. Europe Daylight Time;
whenCreated: 01.10.2026 12:00:45 W. Europe Daylight Time;
ldp декодирует FILETIME, SID, GUID и флаги прямо в выводе — это его главное удобство. Browse → Search открывает окно с базой, фильтром, глубиной и списком атрибутов: здесь удобно отлаживать фильтры, прежде чем вставлять их в скрипт. Options → Controls добавляет контролы к поиску: с 1.2.840.113556.1.4.417 в дереве появляется CN=Deleted Objects с удалёнными объектами. Browse → Modify позволяет менять атрибуты, но для этого есть инструмент удобнее.
В PowerShell то же чтение — Get-ADObject -Identity $dn -Properties * или Get-ADUser alice -Properties *, с той разницей, что модуль декодирует даты в DateTime, а SID — в объект SecurityIdentifier; сырые байты даёт [adsi]"LDAP://$dn" — .NET-обёртка над ADSI, которая под капотом делает тот же LDAP.
ADSI Edit
ADSI Edit (adsiedit.msc) — оснастка MMC, которая показывает каталог деревом с полными свойствами каждого объекта. Нужна она потому, что оснастка Users and Computers показывает отфильтрованный каталог: только контейнеры, которые считает нужными, и только те атрибуты, для которых есть вкладки (вкладка Attribute Editor появилась в 2008 и помогает, но объекты вне Domain раздела она не покажет).
Action → Connect to: выбор между четырьмя известными разделами — Default naming context (домен), Configuration, Schema, RootDSE — или произвольным DN, например DC=DomainDnsZones,DC=corp,DC=example,DC=com. К каждому разделу — отдельное подключение, и в дереве они показываются рядом. Дальше — обычное дерево; свойства объекта — все атрибуты с синтаксисом и значением, фильтр «Show only attributes that have values» убирает пустые. Любой атрибут можно отредактировать, и оснастка проверит синтаксис, но не смысл.
Экскурсия по тому, что в Users and Computers не видно.
Корень домена DC=corp,DC=example,DC=com — это тоже объект, и его атрибуты — настройки домена:
| Атрибут | Что хранит |
|---|---|
minPwdLength, pwdHistoryLength, maxPwdAge, minPwdAge, lockoutThreshold, lockoutDuration, pwdProperties |
парольная политика домена. Default Domain Policy при применении пишет значения сюда, и контроллеры читают их отсюда, а не из GPO |
ms-DS-MachineAccountQuota |
сколько компьютеров может ввести в домен обычный пользователь: по умолчанию 10, и это часто ставят в 0 |
fSMORoleOwner |
DN настроек NTDS контроллера с ролью PDC-эмулятора |
wellKnownObjects, otherWellKnownObjects |
где лежат контейнеры Users, Computers, Deleted Objects, Domain Controllers и другие: пары «GUID роли → DN» |
msDS-Behavior-Version |
функциональный уровень домена: 7 — 2016, 10 — 2025 |
rIDManagerReference |
ссылка на CN=RID Manager$,CN=System |
CN=System — служебные объекты домена:
CN=Policies— контейнеры групповых политикCN={GUID}с атрибутамиdisplayName,gPCFileSysPath(путь в SYSVOL),versionNumber.CN=Password Settings Container— гранулярные парольные политики (PSO).CN=AdminSDHolder— шаблон прав для защищённых учётных записей, о нём в разделе о рисках.CN=RID Manager$— пул RID:rIDAvailablePoolхранит, сколько идентификаторов ещё не раздано,fSMORoleOwner— владелец роли RID Master.CN=Infrastructure—fSMORoleOwnerроли Infrastructure Master.CN=DFSR-GlobalSettings— репликация SYSVOL.CN=Default Domain Policy,CN=Domain Updates,CN=File Replication Service,CN=IP Security,CN=RpcServices,CN=WinsockServices,CN=ComPartitions— объекты, которые никто не трогает годами и о которых нужно знать одно: они нужны.
CN=Configuration — лес:
CN=Sites— сайты, подсети, серверы, объектыNTDS Settingsи связи репликации. Эта ветка — тема части 10.CN=Partitions— объектыcrossRefдля каждого раздела леса с его DNS-именем и NetBIOS-именем;fSMORoleOwnerздесь — Domain Naming Master;msDS-Behavior-Version— функциональный уровень леса. Альтернативные UPN-суффиксы леса хранятся вuPNSuffixesэтого контейнера.CN=Services→CN=Windows NT→CN=Directory Service— параметры службы каталога на весь лес:tombstoneLifetimeиdSHeuristics, оба в разделе о рисках.CN=Services→CN=Public Key Services— конфигурация удостоверяющих центров леса, шаблоны сертификатов (часть 25).CN=Services→CN=NetServices— список авторизованных DHCP-серверов (часть 8).CN=Extended-Rights— расширенные права: «сменить пароль», «сбросить пароль», «реплицировать изменения каталога» (то самое право, которое нужно для атаки DCSync и для Entra Connect).CN=WellKnown Security Principals— Everyone, Authenticated Users, SYSTEM и другие субъекты с SID видаS-1-5-11, которых нет ни в одном домене.
CN=Deleted Objects (виден только с контролом удалённых объектов) — tombstones: удалённый объект не исчезает, а переносится сюда, теряет большую часть атрибутов, получает isDeleted = TRUE, lastKnownParent и суффикс \0ADEL:<GUID> в имени, и живёт так tombstoneLifetime дней (180 по умолчанию), чтобы факт удаления успел дойти до всех контроллеров. Если включена корзина AD, объект сначала становится deleted с полным набором атрибутов и может быть восстановлен, и только потом — recycled.
PS C:\> Get-ADObject -Filter 'isDeleted -eq $true -and objectClass -eq "user"' -IncludeDeletedObjects -Properties lastKnownParent, whenChanged | Format-Table Name, lastKnownParent, whenChanged
Name lastKnownParent whenChanged
---- --------------- -----------
Bob Jones
DEL:3d2e1f0a-9b8c-4d7e-6f50-a1b2c3d4e5f6 OU=Sales,OU=Departments,DC=corp,DC=example,DC=com 14.10.2026 18:02:33
Правка атрибутов из PowerShell вместо ADSI Edit — Set-ADObject с одним из четырёх параметров: -Replace @{attr='value'}, -Add, -Remove, -Clear. Разница между Replace и Add имеет значение для многозначных атрибутов: первый заменит весь список, второй добавит значение к существующим.
Что ломается
Контроллер проверяет синтаксис, обязательные атрибуты и права. Он не проверяет, что вы понимаете, что делаете. Список того, что ломается от правок в ADSI Edit и Set-ADObject, и не восстанавливается Ctrl+Z.
Схема. Любое добавление необратимо, об этом уже было. Опасность тоньше: атрибут с чужим OID или lDAPDisplayName, которые потом захочет создать Exchange или другой продукт, сделает установку этого продукта невозможной; смена searchFlags на индексирование у часто меняющегося атрибута замедлит все контроллеры; снятие флага 8 (сохранять при удалении) у атрибута, нужного для восстановления, сделает корзину бесполезной. Расширения тестируют в отдельном лесу, а потом загружают в продуктивный через LDIF, который прошёл этот тест.
Служебные контейнеры. У объектов CN=System, CN=Infrastructure, CN=RID Manager$, объектов NTDS Settings, разделов и контейнеров из wellKnownObjects в systemFlags стоят биты «нельзя удалить» и «нельзя переименовать», и ADSI Edit откажется. Но администратор домена может снять эти биты, и тогда контроллер подчинится. Удалённый CN=RID Manager$ означает, что ни на одном контроллере домена больше нельзя создать ни одного объекта; удалённый объект NTDS Settings — что контроллер перестал быть контроллером для всех остальных; удалённый CN=Users — что нельзя создать пользователя через половину инструментов. Восстановление — из резервной копии с авторитетным восстановлением (часть 20), и это часы.
wellKnownObjects. Атрибут показывает, где находятся контейнеры по умолчанию, и его меняют утилиты redirusr и redircmp, когда новых пользователей и компьютеры хотят класть в свою OU, а не в CN=Users и CN=Computers. Это правильный способ. Правка атрибута руками на несуществующий или неподходящий DN делает невозможным создание объектов этого класса без явного указания контейнера, причём ошибка будет невнятной.
dSHeuristics. Строка, в которой каждый символ — отдельный переключатель для всего леса: третий символ управляет правом List Object, седьмой — анонимными операциями LDAP (2 разрешает анонимному клиенту читать весь каталог, не только RootDSE), девятый — поддержкой атрибута userPassword как пароля, и так до трёх десятков позиций. Десятый символ обязан быть 1, двадцатый — 2, иначе строка считается повреждённой и перестаёт действовать целиком. Правят её, когда нужно, например, скрыть объекты от пользователей без прав (List Object mode) или разрешить Linux-клиентам читать пароли-атрибуты, и каждый раз — в документации MS-ADTS, с подсчётом позиции, на тестовом лесу.
tombstoneLifetime. Срок жизни удалённых объектов, 180 дней. Снижать его опасно: контроллер, не реплицировавшийся дольше этого срока, после возвращения в сеть начнёт возвращать удалённые объекты обратно (lingering objects), и чистить это — отдельная глава части про диагностику.
AdminSDHolder. Раз в час процесс SDProp на контроллере с ролью PDC-эмулятора берёт дескриптор безопасности объекта CN=AdminSDHolder,CN=System и записывает его всем членам защищённых групп (Domain Admins, Enterprise Admins, Schema Admins, Administrators, Account Operators, Backup Operators, Server Operators, Print Operators) и их учётным записям компьютеров, отключая у них наследование и выставляя adminCount = 1. Так каталог защищает администраторов от того, чтобы права на их учётные записи выдавались через OU. Следствия: права, выданные на учётную запись администратора напрямую, исчезают в течение часа; изменение ACL на AdminSDHolder — это изменение прав на всех администраторов леса разом; пользователь, выведенный из Domain Admins, сохраняет adminCount = 1 и отключённое наследование, пока их не вернуть руками, и из-за этого ему, например, не придёт синхронизация пароля в Entra ID или не сработает делегированный сброс пароля. Поиск таких учётных записей: Get-ADUser -LDAPFilter '(adminCount=1)'.
nTSecurityDescriptor. Права на объект — обычный дескриптор безопасности Windows с ACE на SID, расширенные права и отдельные атрибуты. Два правила. Не трогать записи для SYSTEM, Enterprise Domain Controllers и Domain Controllers: через них контроллеры реплицируют друг другу этот объект, и объект без них перестаёт реплицироваться. И не снимать наследование у контейнеров ради «уберём лишние права»: дескриптор по умолчанию для класса (defaultSecurityDescriptor в схеме) рассчитан на наследование, и половина служб перестанет видеть то, что им нужно. Править права — через dsacls или Set-Acl на диске AD:, читая результат, а не через флажки.
primaryGroupID и member. Основная группа пользователя (513, Domain Users) не хранится в member; попытка убрать пользователя из Domain Users через member не удастся, а смена primaryGroupID на группу, где его нет, — тоже. Учётная запись krbtgt: её нельзя удалять, включать, переименовывать и менять ей что-либо, кроме пароля (и смена пароля — отдельная процедура со своими правилами, часть 6).
Три защиты, которые стоит включить до того, как понадобятся. Корзина AD: Enable-ADOptionalFeature 'Recycle Bin Feature' -Scope ForestOrConfigurationSet -Target corp.example.com — необратимо, бесплатно, после включения удалённые объекты восстанавливаются Restore-ADObject со всеми атрибутами и членством. Защита от случайного удаления: флажок в оснастке, он же Set-ADObject -ProtectedFromAccidentalDeletion $true; технически это запрещающая ACE Delete и Delete Subtree для Everyone, и защищённую OU нельзя удалить и перенести, пока флаг не снят. По умолчанию стоит на OU, созданных оснасткой, и не стоит на созданных New-ADOrganizationalUnit без параметра. И резервная копия состояния системы хотя бы одного контроллера, которой мы займёмся в части 20.
Смотрим руками
Декодирование атрибутов, которые модуль не декодирует сам:
PS C:\> $u = Get-ADUser alice -Properties pwdLastSet, lastLogon, objectSid, userAccountControl, tokenGroups
PS C:\> [DateTime]::FromFileTime($u.pwdLastSet)
15.10.2026 9:30:11
PS C:\> $u.objectSid.Value
S-1-5-21-3623811015-3361044348-30300820-1104
PS C:\> $uac = $u.userAccountControl
PS C:\> 0..31 | Where-Object { $uac -band (1 -shl $_) } | ForEach-Object { '0x{0:X}' -f (1 -shl $_) }
0x200
PS C:\> $u.tokenGroups | ForEach-Object { (New-Object System.Security.Principal.SecurityIdentifier($_, 0)).Translate([System.Security.Principal.NTAccount]).Value }
CORP\Domain Users
CORP\Sales
BUILTIN\Users
tokenGroups — тот список групп, который пользователь получит в билете Kerberos, включая вложенные и основную; это атрибут вычисляемый, и его нужно запрашивать явно.
Последний вход по всем контроллерам, потому что lastLogon не реплицируется:
PS C:\> Get-ADDomainController -Filter * | ForEach-Object {
$dc = $_.HostName
Get-ADUser alice -Server $dc -Properties lastLogon | Select-Object @{n='DC'; e={$dc}}, @{n='LastLogon'; e={[DateTime]::FromFileTime($_.lastLogon)}}
}
DC LastLogon
-- ---------
dc1.corp.example.com 15.10.2026 9:02:11
dc2.corp.example.com 13.10.2026 17:44:50
Какие LDAP-запросы приходят на контроллер и во что их переводит модуль. На контроллере есть журнал «дорогих» и неэффективных запросов: событие 1644 в Directory Service, включается ключом диагностики:
PS C:\> Set-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Services\NTDS\Diagnostics' -Name '15 Field Engineering' -Value 5
PS C:\> New-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Services\NTDS\Parameters' -Name 'Search Time Threshold (msecs)' -Value 1 -PropertyType DWord -Force | Out-Null
PS C:\> Get-ADUser -Filter 'Enabled -eq $true -and PasswordNeverExpires -eq $true' | Out-Null
PS C:\> Get-WinEvent -LogName 'Directory Service' -MaxEvents 1 -FilterXPath '*[System[EventID=1644]]' | Select-Object -ExpandProperty Message
Internal event: A client issued a search operation with the following options.
Client:
192.168.1.100:51722
Starting node:
DC=corp,DC=example,DC=com
Filter:
( & ( ! (userAccountControl&2) ) (userAccountControl&65536) )
Search scope:
subtree
Attribute selection:
distinguishedName,name,objectClass,objectGUID,objectSid,sAMAccountName,userPrincipalName,givenName,sn,userAccountControl
Server controls:
1.2.840.113556.1.4.801 (SD flags)
1.2.840.113556.1.4.319 (paged results)
Visited entries:
148
Returned entries:
3
Used indexes:
idx_userAccountControl:1:N;
Так выглядит -Filter 'Enabled -eq $true -and PasswordNeverExpires' после перевода: два битовых сравнения, постраничный вывод, индекс по userAccountControl. Порог в 1 мс мы поставили, чтобы увидеть любой запрос; в рабочей системе порог оставляют на умолчании (30 секунд) или ставят в несколько сотен миллисекунд, чтобы ловить только то, что действительно нагружает контроллер — и это один из главных инструментов части про диагностику. Не забудьте вернуть 15 Field Engineering в 0.
И наконец, сам протокол в пакетах. Снимаем, как в части 2, pktmon с фильтром на порт 389 во время Get-ADUser alice, открываем в Wireshark и видим:
No. Source Destination Protocol Info
1 192.168.1.100 192.168.1.10 LDAP bindRequest(1) "<ROOT>" sasl
2 192.168.1.10 192.168.1.100 LDAP bindResponse(1) saslBindInProgress
3 192.168.1.100 192.168.1.10 LDAP bindRequest(2) "<ROOT>" sasl
4 192.168.1.10 192.168.1.100 LDAP bindResponse(2) success
5 192.168.1.100 192.168.1.10 LDAP SASL GSS-API Privacy: searchRequest(3) ...
6 192.168.1.10 192.168.1.100 LDAP SASL GSS-API Privacy: searchResEntry(3) ... searchResDone(3) success
Два хода SASL-привязки (GSS-SPNEGO с билетом Kerberos на ldap/dc1.corp.example.com внутри), и дальше поиск, который Wireshark уже не может прочитать: после привязки по Kerberos данные зашифрованы. Это и есть ответ на вопрос, зачем Kerberos нужен LDAP, и переход к следующей части.
Дальше — часть 6: Kerberos изнутри. Что такое realm и KDC, зачем нужна учётная запись krbtgt, как устроены обмены AS и TGS, что лежит внутри билета и зачем ему PAC, чем отличаются типы шифрования AES и RC4, что такое SPN и почему без него аутентификация откатывается на NTLM, как работает делегирование, и как читать klist и ошибки KRB5KDC_ERR_*.
Серия «Windows Server для самых маленьких»
- Windows Server с нуля — редакции, Server Core, установка, первые команды PowerShell, имя, сеть, время, обновления, роли.
- Диски и сеть: diskpart, netsh и их PowerShell-замены — разметка дисков и сетевой стек через старые утилиты и модули Storage, NetTCPIP, NetSecurity, и что умеют только утилиты.
- Как устроен Active Directory — лес, домен, OU, объекты, разделы каталога, глобальный каталог, SYSVOL, DC Locator и протоколы домена.
- Первый контроллер домена — имя домена, роль AD DS,
Install-ADDSForestи что происходит при повышении,dcdiag, первые члены домена и первая учётная запись администратора. - LDAP изнутри (эта статья) — операции протокола, имена и атрибуты, схема, RootDSE, фильтры,
ldp.exeи ADSI Edit, и что в каталоге нельзя трогать.
Оставить комментарий могут только зарегистрированные пользователи.
Войдите на сайт или зарегистрируйтесь, чтобы оставить комментарий.