Привет, Хабр! Меня зовут Иван Мороз, я системный администратор в BPMSoft. В нашей компании существовала проблема с контролем прав локального администратора на сотнях корпоративных ноутбуков. Ручной учет через Excel или стандартные GPO оказалось неэффективным, а ошибки могли приводить к проблемам с безопасностью и операционным рискам.
В этой статье я расскажу, как автоматизировал выдачу и изъятие прав локального админа с помощью PowerShell и шедулера, как строилась концепция решения, какие трудности возникли и как их удалось обойти. Я покажу конкретные блоки кода и дам практические советы для внедрения подобных процессов в крупных корпоративных средах.

Задача
В какой-то момент перед отделами ИТ \ ИБ встала комплексная задача — систематизировать процесс выдачи, изъятия и контроля прав локального администратора на корпоративных ноутбуках.
Понять, кто из сотрудников имеет права сейчас.
Забрать права у тех, кому они не нужны.
Автоматизировать процесс так, чтобы выдача и изъятие происходили быстро, контролируемо и без подключения к ноутбуку. Желательно в пару кликов.
Иметь возможность убедиться, что наши действия привели к ожидаемому результату: автоматика отработала корректно, сотрудник, получив права, не выдал их кому-то ещё; если права были изъяты, они действительно изъяты.
Известные решения, и почему они не подошли
Самый простой и неэффективный способ решения задачи — выдавать права “руками” и вести реестр в Excel. Это плохо примерно всем: сплошной “человеческий фактор” и никакого контроля. Вы один раз выдали права и не имеете представления, что происходит с ноутбуком дальше.
Делегирование прав через Group Policy Management (GPO): Computer Configuration → Preferences → Control Panel Settings → Local Users and Groups → Item-level Targeting.
Этот способ уже лучше — групповая политика при каждом применении будет выдавать права, проверять, что они уже есть, и вычищать из группы Administrators всех лишних.
Из минусов:
На каждого сотрудника надо делать отдельную политику, а когда у вас сотня-другая разработчиков, это становится проблемой.
Если на конкретном ноутбуке что-то пошло не так (по любой причине), вы об этом не узнаете. Оснастка GPO не дает отчета об успешном применении политики.
Нельзя сделать оперативно выгрузку и посмотреть, кто на каком ноутбуке имеет повышенные привилегии, а этот вопрос рано или поздно будет задан.
Решение
Концепция — нужно обеспечить приведение каждого ноутбука к следующему состоянию:
Права администратора имеют:
Встроенная учетная запись администратора
Группа Domain Admins
“Легитимный админ” — сотрудник, которому согласованы права локального администратора (если такой есть)
У всех остальных их нужно отобрать.
Выдача и изъятия прав происходит посредством PowerShell — скрипта, который запускается на каждом ноутбуке через мгновенный шедулер (Immediate Task) от имени системы (NT AUTHORITY\System) при каждом обновлении GPO в фоновом режиме. Дальше разберем подробнее составляющие процесса.
Планировщик задач:
Вне зависимости от регистрации пользователя
От имени системной учетной записи
С повышенными привилегиями
При выполнении скрипта — игнорировать ExecutionPolicy

Может возникнуть вопрос: почему процесс запускается через Планировщик задач, а не через Startup \ Logon скрипт? Кажется, что второе проще и интуитивно понятнее.
Ответ: потому что это работает только в “тепличных условиях”, когда машина имеет стабильное соединение с контроллером домена (желательно по кабелю), в том числе при включении компьютера \ входе пользователя в сессию. Если у вас половина компании — удаленщики, которые запускают VPN как придется, это не будет работать стабильно.
В качестве альтернативы, для запуска скрипта, можно использовать не мгновенные шедулеры, а создать постоянный, который будет запускаться по расписанию или по заданным триггерам.
Сетевая папка для хранения скрипта и логов
Я решил хранить скрипт в сетевой шаре и, поскольку он должен выполняться от имени SYSTEM, компьютеры в AD должны иметь к ней доступ. Я выдавал доступ сразу на группу Domain Computers. В шару, где лежит скрипт — только на чтение. Но в ней ещё есть папка для хранения логов — и уже туда запись + чтение.
Active Directory
Выдавая или забирая права админа, система должна понимать \ ориентироваться, откуда-то брать данные “Кому выдать — Где выдать”. Для этих целей я решил использовать Active Directory, записывая в атрибут ManagedBy каждого компьютера - учетную запись пользователя, который должен иметь права админа на этом компьютере. Поэтому, если нужно выдать права, это делается через оснастку ADUC в пару кликов:

PowerShell-скрипт
У скрипта есть несколько логических блоков:
Проверка и установка модуля Active Directory для Powershell, если его нет
Определение переменных\монолитов
Проверка, должен ли кто-то иметь права локального админа на этом ПК
Если никто не должен — переходим к следующему блоку
Если должен — проверить, имеет ли он уже права
Если имеет — все хорошо, ничего делать не нужно
Если не имеет — выдаем, оповещаем пользователя, фиксируем это для лога
Проверка, имеет ли права локального админа кто-то лишний
Смотрим список всех пользователей в группе администраторов
Проверяем каждого
Если это легитимный сотрудник или дефолтная учетка админа (SID = *500) — оставляем
Если это кто-то иной — удаляем его из группы администраторов, оповещаем пользователя, фиксируем это для лога
Смотрим, есть ли группы безопасности, отличные от Domain Admins. Если есть — удаляем, оповещаем пользователя, фиксируем это для лога
Логирование
Если в процессе отработки скрипта не было произведено значимых действий (кому-то выдали, у кого-то забрали) и не было ошибок, лог писать не нужно
Если какие-то сущностные изменения были или были ошибки, делаем запись лога
Разберем подробнее эти блоки
Блок 1. Проверка и установка модуля Active Directory
#Проверяем, что компонент RSAT .ActiveDirectory установлен. Если не установлен — устанавливаем
$folder = "\\share\folder"
$status_AD=(Get-WindowsCapability -Name Rsat.ActiveDirectory.DS-LDS.Tools~~~~0.0.1.0 -Online).state
if ($status_AD -eq "NotPresent") {Add-WindowsCapability -Online -Name Rsat.ActiveDirectory.DS-LDS.Tools~~~~0.0.1.0 -LimitAccess -Source $folder}
Без модуля ActiveDirectory не получится выполнить запрос к AD и проверить наличие “легитимного” админа. Поэтому ставим, если ещё не стоит. Я ставлю из локальной шары через опцию LimitAccess.
Блок 2. Определяем переменные
Я всегда пишу этот блок в начале скрипта. Когда все значения, которые могут потенциально изменится, находятся в одном месте, это упрощает дальнейшую эксплуатацию, а также делает ваш код более читаемым для тех, кто будет поддерживать его после вас. Об этом надо думать сразу.
#кто легитимный локальный админ
try { $check_local_admin_rights = (Get-ADComputer -Identity $(hostname) -Properties ManagedBy -ErrorAction Stop).ManagedBy}
#остановка выполнения скрипта, если не удалось определить
catch {throw "Не удалось определить легитимного админа. Остановка скрипта"}
#определяем название группы доменных администраторов
try { $domain_admins = (Get-ADGroup -filter * -ErrorAction Stop | Where-Object {$_.SID -like '*-512'}).name}
#остановка выполнения скрипта, если не удалось определить
catch {throw "Не удалось определить группу Администраторов Домена. Остановка скрипта"}
#выясняем название учетки локального админа (у неё идентификатор заканчивается на 500)
$build_admin = (Get-LocalUser | Where-Object {$_.SID -like '*500'}).name
#определяем название группы локальных администраторов
$build_admin_group = (Get-LocalGroup | Where-Object {$_.SID -like '*544'}).name
#куда пишутся логи
$error_logs = "\\share\folder\file.txt"
#определяем доменный префикс
$domain = (Get-ADDomain -Current LocalComputer).name
Важные детали:
Запросы к AD нужно обрабатывать через try \ catch с обязательной остановкой скрипта в случае ошибки. Если в этом моменте произойдет ошибка по любой причине (DC был перегружен и не смог ответить, сбой в сети и т.д.), в переменную ничего не запишется. И скрипт продолжит выполнение работы, считая, что на этом компьютере никто не должен иметь права админа. Так можно забрать у того, кто должен их иметь. Потом придется краснеть.
Название учетной записи встроенного админа или группы надо определять по SID, поскольку оно может меняться в зависимости от локализации (Администратор \ Administrator). SID дефолтных учетных записей и групп известны — это т.н. “well-known” идентификаторы безопасности:
https://learn.microsoft.com/en-us/windows/win32/secauthz/well-known-sids
Блок 3. Проверка, должен ли кто-то иметь права локального админа на этом пк
Проверяем, должен ли какой-то сотрудник (его доменная учетка) иметь права админа на данном ПК, если должен - имеет ли, если имеет - отлично, если нет - выдаем, оповещаем пользователя, логгирование
#проверяем должен ли кто-то иметь права локального админа на данном компе
if ($check_local_admin_rights -eq $null) {$exit_legal = $null} #если никто не должен - значит можно завершать этот блок скрипта
else #если должен - продолжаем выполнение
{
#смотрим, кто именно должен иметь права админа
try { $legal_local_admin = (Get-ADUser -Filter * -Properties DistinguishedName -ErrorAction Stop | Where-Object {$_.DistinguishedName -like $check_local_admin_rights}).SamAccountName}
#остановка выполнения скрипта, если не удалось определить
catch {throw "Не удалось определить легитимного админа. Остановка скрипта."}
#получаем список доменных групп и пользователей, которые находятся в группе локальных админов
$all_local_admins = Get-LocalGroupMember -Group $build_admin_group | Where-Object {$_.PrincipalSource -EQ 'ActiveDirectory'}
foreach($user in $all_local_admins)
{
#выбираем из массива только пользователей и проверяем каждого
if (($user.ObjectClass -eq "User") -or ($user.ObjectClass -eq "Пользователь"))
{
#проверяем, есть ли среди членов группы локальных админов - доменная учетная запись сотрудника
if ($user.Name -like "*$legal_local_admin") {$i+=1}
}
}
#если не имеет - выдаем, логируем это и оповещаем пользователя
if ($i -eq $null) {
try {
Add-LocalGroupMember -Group $build_admin_group -Member "$domain\$legal_local_admin" -ErrorAction Stop
#текст для лога
$exit_legal = "Grant rights legitimate admin: $legal_local_admin"
msg * "Доменному пользователю [$legal_local_admin] делегированы права локального администратора ��а этом ПК. Перелогиньтесь, чтобы они вступили в действие."
}
catch {Write-Error "Ошибка при выдаче прав локального админа"}
#дефолтное запись этого блока, которая будет записана в лог, если кого-то не удалят
} else {$exit_legal = $null} #текст для лога остается пустой = значимых изменений не было
}
Важные детали:
К сожалению, в зависимости от локализации, значение ObjectClass может быть и “User”, и “Пользователь”. Я не нашел способа точно определять это значение, поэтому “прибил гвоздями” оба варианта.
Не забываем делать запись в переменную $exit_legal, которая потом понадобится для логирования.
Блок 4. Проверка, имеет ли права локального админа кто-то лишний \ нелегитимный
#получаем список групп и пользователей, которые находятся в группе локальных админов
$all_local_admins = Get-LocalGroupMember -Group $build_admin_group
foreach($user in $all_local_admins)
{
#выбираем из массива ЛОКАЛЬНЫХ пользователей
if ((($user.ObjectClass -eq "User") -or ($user.ObjectClass -eq "Пользователь")) -and ($user.PrincipalSource -eq "Local"))
{ $delete_local_user = $user.Name.split('\')[1] #обрезаем доменный префикс
#если ты учетка локального админа, тебя оставляем
if ($build_admin -eq $delete_local_user) {Write-Host "Оставляем: $build_admin"}
#если ты кто-то иной, тебя удаляем из группы, логируем это и оповещаем пользователя
else {
try {
Remove-LocalGroupMember -Group $build_admin_group -Member $delete_local_user -ErrorAction Stop
#текст для лога
$exit_no_legal += " Deleted no-legitimate admin (local): $delete_local_user"
msg * "У локального пользователя [$delete_local_user] больше нет прав локального администратора на этом ПК."
}
catch {Write-Error "Ошибка при изъятии прав у локального пользователя"}
}
}
#выбираем из массива ДОМЕННЫХ пользователей
if ((($user.ObjectClass -eq "User") -or ($user.ObjectClass -eq "Пользователь")) -and ($user.PrincipalSource -eq "ActiveDirectory"))
{ $delete_user = $user.Name.split('\')[1] #обрезаем доменный префикс
#если ты легитимный сотрудник, у которого должны быть права локального админа, тебя оставляем
if ($legal_local_admin -eq $delete_user) {Write-Host "Оставляем: $legal_local_admin"}
#если ты кто-то иной, тебя удаляем из группы, логируем это и оповещаем пользователя
else {
try {
Remove-LocalGroupMember -Group $build_admin_group -Member "$domain\$delete_user" -ErrorAction Stop
#текст для лога
$exit_no_legal += " Deleted no-legitimate admin: $delete_user"
msg * "У доменного пользователя [$delete_user] больше нет прав локального администратора на этом ПК."
}
catch {Write-Error "Ошибка при изъятии прав у доменного пользователя"}
}
}
#выбираем из массива ГРУППЫ БЕЗОПАСНОСТИ
if (($user.ObjectClass -eq "Group") -or ($user.ObjectClass -eq "Группа"))
{ $delete_group = $user.Name.split('\')[1] #обрезаем доменный префикс
#если ты группа Domain Admins, тебя оставляем
if ($delete_group -eq $domain_admins) {Write-Host "Оставляем: $delete_group"}
#если ты кто-то иной, тебя удаляем из группы, логируем это и оповещаем пользователя
else {
try {
Remove-LocalGroupMember -Group $build_admin_group -Member "$domain\$delete_group" -ErrorAction Stop
#текст для лога
$exit_no_legal += "Deleted no-legitimate group: $delete_group"
msg * "Доменная группа [$delete_group] больше не имеет права локального администратора на этом ПК."
}
catch {Write-Error "Ошибка при изъятии прав у группы безопасности"}
}
}
}
#дефолтное запись этого блока, которая будет записана в лог, если кого-то не удалят = сущностных операций не было
if ($exit_no_legal -eq $null) {$exit_no_legal = $null}
Важные детали:
Локальных и доменных пользователей нужно проверять отдельно и удалять с префиксом (доменные) \ без префикса (локальные). Может возникнуть вопрос, зачем, если можно удалять доменных пользователей без доменного префикса.
Например: Remove-LocalGroupMember -Group $build_admin_group -Member ivan.ivanov
И это действительно работает. До тех пор, пока на ноутбуке не будет создана локальная учетная запись с именем, полностью идентичным доменной.
Когда это произойдет, командлет Remove-LocalGroupMember будет пытаться удалить именно локальную. Может быть кейс, что вам надо выбить из группы локальных администраторов доменную учетку, а командлет будет пытаться выбить локальную и получать ошибку при каждой отработке скрипта. Вероятность этого кейса на грани погрешности, но я с ним столкнулся :-)
Блок 5. Логирование
#проверяем, были ли выполнены какие-то сущностные действия
if ( ($exit_legal -eq $null) -and ($exit_no_legal -eq $null) -and (!$error) ) {Write-Host "Писать лог не нужно"}
else
{
#собираем данные в одну переменную
$export = "$(Get-Date) | $(hostname) | $exit_legal | $exit_no_legal | Error: $error "
$export | Add-Content -Path $error_logs #делаем запись в лог
}
Важные детали:
Логирование — базовое свойство любой информационной системы. Какую бы вы не писали автоматизацию, она должна сообщать вам, что сделала задуманное вами или не смогла, выводя текст ошибки.
Я не хотел видеть в файле лога результат выполнения каждого скрипта на каждом ноутбуке, поэтому решил, что буду писать лог только если произошло что-то сущностное, например выдача прав, изъятие прав, любая ошибка, которая окажется в переменной $error
Структура лога следующая:время и дата выполнения | хостнейм ПК | выдача прав | изъятие прав | ошибки
Пример записей в файле лога:03/04/2025 16:30:03 | WIN10-PC4 | Grant rights legitimate admin: Ivan.Ivanov | | Error:
04/22/2025 22:32:39 | WIN11-PC14 | | Deleted no-legitimate admin: Ivan.Ivanov Deleted no-legitimate group: ИТ-отдел | Error:
Я решил выполнять логирование всех значимых событий в одной строке, которая пишется в самом конце скрипта. Но можно сразу выполнять запись в лог, в момент когда произошло “значимое действие”, не дожидаясь окончания скрипта.
Преимущество моего решения
Простота для исполнителя. Выдача \ изъятие происходит без подключения к рабочей станции, через оснастку ADUC в несколько кликов на вкладке ManagedBy. В ActiveDirectory я указываю “нужное состояние”, а дальше групповые политики приводят реальность к этому состоянию при каждом обновлении.
Отчетность. Путем выполнения простого скрипта я получаю выгрузку “Компьютер — Пользователь” и могу ответить на вопросы “Кто?”, “Где?”, “Сколько всего?”
$Local_Admins = Get-ADComputer -Filter * -Properties managedBy | Where-Object {$_.managedBy -ne $null} | Select-Object @{n="Computer"; e={$_.name}}, @{n="Local Admin"; e={$_.managedBy.split(',')[0]}} $Local_Admins Write-Host "Всего: $($Local_Admins.Count)`r`n" -ForegroundColor Yellow
Логирование. В файле лога я вижу все сущностные операции, которые выполняет политика и скрипт: кому выдала, у кого забрала. Вижу тех, у кого не должно было быть прав, но почему-то были. А когда права нужно выдать — могу отследить, что они действительно были выданы.
Траблшутинг
Насколько бы не была продумана система, иногда она дает сбой. Надо разбираться почему. В этой части не будет подробного разбора кейсов — я лишь покажу, каким должен быть вектор вашей мысли:
Вы не просто так добавляли логирование в логику скрипта. Первым делом идем смотреть, что скрипт написал нам туда из переменной $error и интерпретируем увиденное
Автоматизация работает через групповую политику. А к моему ноутбуку она применяется? Проверяем через командную строку с повышенными привилегиями и gpresult /r
Политика точно применяется, но что она делает? И где логируется это “что”? Первое — создает задачу в планировщике, поэтому идем смотреть логи в Event Viewer - Application and Services Logs - Microsoft - Windows - Task Scheduler - Operational. Там вы должны увидеть, что система:
зарегистрировала задачу (Task registered)
задача затригерилась (Task triggered on scheduler)
создала процесс (Created Task Process)
запустила задачу (Task Started)
выполнила задачу с каким-то кодом (Action completed)
Хорошо, планировщик запускал задачу, она что-то делала. Что?
Запускала PowerShell-скрипт! А значит еще можно посмотреть логи в Event Viewer - Application and Services Logs - Windows PowerShell. Там можно увидеть, какой именно скрипт запускался и запускался ли.Видим, что запускался наш скрипт из сетевой шары — отлично! Но хорошо бы понять, что произошло в процессе выполнения. И чтобы понять это, нужно заблаговременно включить через GPO на ноутбуках расширенное логирование PowerShell, которое будет сохранять транскрипции, например в C:\Windows\Logs\Powershell

Существует официальный баг (https://github.com/PowerShell/PowerShell/issues/2996), из-за которого при выполнении скрипта вы будете получать ошибку Get-LocalGroupMember : Failed to compare two elements in the array
на единичных машинах. Она вызвана пустыми SID в группе администраторов, которые остались после присоединения/выхода из домена.
Фиксится следующим образом:
#смотрим название баганной учетки
([ADSI]"WinNT://./Administrators").psbase.Invoke('Members') | % {([ADSI]$_).InvokeGet('AdsPath')}
#удаляем её
Remove-LocalGroupMember -group "administrators" -member ‘name’
Заключение
Решение будет удобно для тех, кому нужно делегировать права локального админа сильно больше, чем на 10-20 рабочих станциях.
Запуск скрипта посредством Планировщика актуален для тех, у кого в компании много удаленщиков, с нестабильным подключением к контроллеру домена.
Выдачу прав через оснастку ADUC можно делегировать коллегам из ИБ \ Техподдержке, выдав права редактировать атрибут managedBy на уровне AD в конкретной OU. Ровно как и просмотр \ мониторинг логов — выдав права на чтение сетевой шары.
О выдаче \ изъятии прав лучше оповещать пользователя посредством утилиты msg, чтобы этот процесс проходил для него прозрачно и вызывал меньше вопросов:
Отдавая выполнение процесса со сложной логикой PowerShell-скрипту, вы должны базово уметь работать с этим средством автоматизации, читать чужой код, уметь интерпретировать его ошибки.