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 для самых маленьких»

  1. Windows Server с нуля — редакции, Server Core, установка, первые команды PowerShell, имя, сеть, время, обновления, роли.
  2. Диски и сеть: diskpart, netsh и их PowerShell-замены — разметка дисков и сетевой стек через старые утилиты и модули Storage, NetTCPIP, NetSecurity, и что умеют только утилиты.
  3. Как устроен Active Directory — лес, домен, OU, объекты, разделы каталога, глобальный каталог, SYSVOL, DC Locator и протоколы домена.
  4. Первый контроллер домена — имя домена, роль AD DS, Install-ADDSForest и что происходит при повышении, dcdiag, первые члены домена и первая учётная запись администратора.
  5. LDAP изнутри (эта статья) — операции протокола, имена и атрибуты, схема, RootDSE, фильтры, ldp.exe и ADSI Edit, и что в каталоге нельзя трогать.

Оставить комментарий могут только зарегистрированные пользователи.

Войдите на сайт или зарегистрируйтесь, чтобы оставить комментарий.