
Manage Windows AD with PowerShell
Organized
On many networks, Active Directory (AD) is the must-have setup for authentication and assignment of rights and as a directory service. With such a central service, everything should run smoothly with PowerShell automation. In this article, I show you how to search in AD, how to secure critical accounts, and which PowerShell helpers you will want to use.
Administrators have gained a lot of experience in maintaining and operating Active Directory over its 20-year history. The tools and how they programmatically and automatically trigger changes in the directory have also changed, both in terms of data administration (i.e., managing users, computers, service accounts, and all the other objects in the directory) and in terms of the scripts for controlling the directory service itself (i.e., the service that runs on Windows and provides the domain function). Tasks that used to be automated by VBScripts, plain vanilla LDAP, Win32 calls, and, later, .NET are now a little easier for admins and abstracted by PowerShell.
PowerShell Helpers
Even newcomers or occasional scripters should have a few decent tools for creating scripts or one-off commands in their toolbox. On the one hand, the commands can be assembled with autocompletion, after which parameters can be suggested and easily inserted; on the other hand, tools allow the one-liners or scripts to be executed directly with color coding, thus making copy and paste into a separate PowerShell session unnecessary. The tools also allow individual lines from longer scripts to be executed separately for step-by-step testing. Of course, it is also possible to open a separate PowerShell session and enter and process the commands directly, but why make things more difficult than necessary?
Windows comes with the PowerShell Integrated Scripting Environment (ISE) as an add-on: It is immediately ready for use in PowerShell but is no longer actively developed by Microsoft. You can still create your Windows AD commands with it, specifically because the tool is on board and available on domain controllers with the same feature set.
One alternative is Visual Studio Code (Figure 1), which is downloadable free of charge for all current Windows versions, and it offers PowerShell language support for Visual Studio Code as an extension. The extension then comes with intelligent suggestions for parameters and command highlighting for improved visual processing of tasks.

Preparing for PowerShell Access to AD
Microsoft provides some ready-made PowerShell commands for AD that, once installed, support easy interaction. These cmdlets then interact with the corresponding services that work on domain controllers and use the APIs that Microsoft provides as part of AD. You don't have to worry about the actual API or functions, as long as the PowerShell wrappers are all you need. These PowerShell commands became part of the OS in Windows 10 version 1809 and are activated manually as a feature; older Windows 10 versions require the Remote Server Administration Tools, which also includes the PowerShell module [1].
On domain controllers, when you promote the server you will be prompted as to whether you want to install the administration tools and PowerShell together with the domain controller role. If the module is not available, you can install it later with Server Manager, which lets you enable the Windows feature (Role Administration Tools | AD DS and AD LDS Tools | Active Directory module for Windows PowerShell). In PowerShell you can enter:
Import-Module ServerManager Add-WindowsFeature -Name "RSAT-AD-PowerShell" -IncludeAllSubFeature
Once complete, you can display an overview of all the available cmdlets that you can use for Microsoft Active Directory:
Get-Command -Module ActiveDirectory
You will quickly see that the commands all have the familiar PowerShell verb at the beginning and then the command with the AD*
prefix. You will also recognize some known objects among the cmdlets – ADUser
, ADGroup
, ADComputer
, ADGroupMember
, ADAccount
, and many more.
Searching in Active Directory
Users in AD, which you can query with Get-ADUser
, are also of interest. Either the sAMAccountName
, the DistinguishedName
, the ObjectGUID
, or the SID
are used as the search keys:
Get-ADUser -Identity flofromm
If you are looking for all employees of a certain department, the filter helps narrow things down on the attribute level:
Get-ADUser -Filter "Department -like 'IT*'"
The filter works with all common attributes. If all relevant users are already grouped into organizational units, you can find them by specifying the directory path as SearchBase
. The LDAP notation is used here; the Externals
organizational unit (OU) below the corp.frickelsoft.net domain, would be written as:
Get-ADUser -Filter * -SearchBase "OU=Externals,DC=corp,DC=frickelsoft,DC=net"
Of course, you can also combine SearchBase
with a filter. The Search-ADAccount
cmdlet is also useful if you are looking for AD accounts but do not want to search by user or computer.
The following commands find all locked-out accounts and inactive accounts belonging to both users and computers:
Search-ADAccount -LockedOut Search-ADAccount -AccountInactive -TimeSpan 120.00:00:00 | ft Name,LastLogonDate,Enabled
To inspect groups, your best option is the Get-ADGroup
cmdlet:
Get-ADGroup -Filter * -Properties member
The cmdlet gives you a good overview of the properties of a group. Besides SearchBase
, groups can also be narrowed down by Filter
(e.g., if you are only looking for security groups):
Get-ADGroup -Filter "GroupCategory-eq 'Security'"-SearchBase "OU=Groups,DC=corp,DC=Frickelsoft,DC=net"
If you explicitly query the group members as an attribute with the Get-ADGroup
cmdlet, you are only given text output in return. For further use of the group members as PowerShell objects, try the Get-ADGroupMember
cmdlet, which only returns the group members:
Get-ADGroupMember -Identity 'Enterprise Admins' -Recursive Get-ADGroupMember -Identity 'Domain Admins' -Recursive
The Recursive
option also resolves nested group memberships. If you want to reuse the member list in another command with a pipe, the cmdlet of choice is Get-ADGroupMember
:
Get-ADGroupMember -Identity 'Domain Admins' -Recursive | Get-ADUser -Properties Emailaddress, lastLogonDate | Export-CSV -Path "C:\ temp\csv\Domain Admins.CSV"
However, all groups can be queried with the Get-ADGroup
cmdlet,
Get-ADGroup -Filter "Name -like 'HR*'" -SearchBase 'OU=Groups, DC=nttest,DC=corp,DC=frickelsoft,DC=net' -SearchScope SubTree | Get-ADGroupMember Get-ADGroup -Filter "Name -like 'HR*'" -SearchBase 'OU=Groups,DC=nttest,DC=corp,DC=frickelsoft,DC=net' -SearchScope SubTree | Export-CSV -Path 'C:\temp\csv\HR_departmental_groups.csv'
and exported (e.g., to a CSV file as in the second command), if so desired.
Making Changes
You can modify users of a certain OU with a one-liner so that they all have a certain attribute value, allowing other programs (e.g., AADConnect
for synchronization to the cloud) to find and process them:
Get-ADUser -Filter * -SearchBase "OU=Externals,DC=corp,DC=frickelsoft,DC=net" | Set-ADUser -Add @{extensionAttribute4 = "M365"}
An imported CSV file lets you edit multiple users:
Import-CSV 'C:\temp\csv\users.csv' | % { if($_. mail-like '*@frickelsoft.net') { Set-ADUser $_.sAMAccountName -Add @{extensionAttribute4 ="M365"}}}
This command imports a CSV file that contains user definitions and parses it line by line. Two columns in the CSV are inspected more closely: mail and sAMAccountName (Figure 2). If the mail address ends with @frickelsoft.net, extensionAttribute4
of the user in AD is set to M365
. Nothing happens for the other users found in the CSV file.

To transfer the value of the region column to an AD attribute (e.g., preferredDataLocation
for Office 365), you need to modify the command
Import-CSV '.\Downloads\users.csv' | % { Set-ADUser -Identity $_.sAMAccountName-Add @{preferredDataLocation =$_.region }}
to take the cell value from the CSV file instead of using a fixed value.
Managing Special Accounts
Of course, you will not only want to manage normal user accounts, but also accounts belonging to external partners and suppliers or service accounts. You can simply disable accounts that are no longer needed:
Get-ADUser -Identity svc_low_SQL3 | Disable-ADAccount
This method also works with accounts belonging to external identities that are grouped in an OU. To block people or service accounts that have not logged in for 120 days or more, use:
$lastLogonCutOff = (Get-Date). AddDays(-120) Get-ADUser -Filter { LastLogonDate -lt $lastLogonCutOff -and Enabled -eq $true } -SearchBase "OU=Externals,DC=corp,DC=frickelsoft,DC=net" | Disable-ADAccount
In the best case, the service accounts that you create as normal user accounts and not as group-managed service accounts (gMSA) are restricted to the extent that the logon can only take place on certain machines to avoid misuse. To define this case, use the LogonWorkstations
parameter:
Set-ADUser svc_low_SQL3 -LogonWorkstations "SQL3"
This attribute expects a comma-separated list of machine names. For example, you can create a new service account that you want to log on to all SQL servers with:
$sqlServers = Get-ADComputer -Filter "Name -like 'SQL*'" -SearchBase "OU=Servers,OU=Tier1,DC=corp,DC=frickelsoft,DC=net" | SELECT sAMAccountName |%{$_.sAMAccountName.Trim("$")} $sqlServers = $sqlServers -join "," Set-ADUser svc_high_SQL3 -LogonWorkstations $sqlServers
If you only want the service account to be valid for a time-limited project, or if the owner of the account is required to come back and renew the account within 90 days at the latest, you can set an expiration date:
Set-ADAccountExpiration -Identity svc_low_SQL3 -TimeSpan 90.00:00:00
You will certainly want to minimize the number of accounts whose passwords never expire. In fact, apart from service accounts, there should be no accounts in AD whose passwords do not expire:
Search-ADAccount -PasswordNeverExpires | Export-CSV C:\temp\csv\neverexpires.csv
Alternatively, you can move these accounts to a group for delegated administration:
Search-ADAccount -PasswordNeverExpires | %{Add-ADGroupMember -Identity "PWDNeverExpires" -Members $_.sAMAccountName }
If you specifically need to harden certain accounts against hijacking and ensure that the passwords used are appropriately complex, you can attach custom password policies to these accounts. For example, with a few lines of PowerShell, you can create a new password settings object that contains custom password policies and assign it to a group of service accounts. First, create the password policy object that enforces manual unlocking and long, complex passwords (Listing 1).
Listing 1: Enforcing Complex Passwords
New-ADFineGrainedPasswordPolicy -Name "HighSecServiceAccountsPolicy" -Precedence 500 -ComplexityEnabled $true -Description "Password Policy for High Sec Service Accts" -DisplayName "High Sec Service Accs PassPolicy" -LockoutDuration "00:00:00" -LockoutObservationWindow "00:00:00" -LockoutThreshold 7 -MinPasswordAge "01:00:00" -PasswordHistoryCount 20 -MinPasswordLength 16
After that, define a new AD group to which you then add the service accounts as members before assigning the policy (Listing 2).
Listing 2: Defining New AD Group
New-ADGroup -Name "High Sec Service Accts" -SamAccountName HighSecSer-viceAccts -GroupCategory Security -GroupScope Global -DisplayName "High Sec Service Accts" -Path "OU=Groups,OU=Service Accounts, DC=corp,DC=frickelsoft,DC=net" -Description "Members of this group are High Security Service Accounts" Add-ADFineGrainedPasswordPolicySubject -Identity HighSecServiceAccountsPolicy -Subjects 'HighSecServiceAccts'
Finally, search and find the service accounts to be protected in AD:
Get-ADUser -Filter 'DisplayName -like "SVC_HIGH_*"' -SearchBase "OU=Service Accounts,DC= corp,DC=frickelsoft,DC=net" | % { Add-ADGroupMember "High Sec Service Accounts" -Members $_ }
The next time the password is changed, the service accounts – or the admin who resets and changes the passwords – has to comply with the new password policy.
Managing Forests and AD Services
PowerShell lets you inspect the AD service itself, as well as manage the data in the directory. Cmdlets let you create new domain controllers, domains, AD objects, and partitions and inspect existing objects:
$forest = Get-ADForest -Server "corp.frickelsoft.net" foreach($domain in $forest.Domains) { Get-ADDomainController -Filter * -Server $Domain }
The following two cmdlets show you the flexible single master operation (FSMO) role holders:
Get-ADForest | SELECT DomainNamingMaster, SchemaMaster Get -ADDomain -Name corp.frickelsoft.net | SELECT InfrastructureMaster, PDCEmulator, RIDMaster
The Get-ADDomain
and Get-ADForest
cmdlets have additional properties that you can use for an inventory or check: The DomainFunctionalLevel
can be found in DomainMode
for each domain, and the ForestFunctionalLevel
is found in ForestMode
for forest objects.
If you want to provision devices that have been added to the domain to an Azure AD, you need to configure a hybrid Azure AD join. One step in the configuration is to create the service connection object manually or automatically in the configuration partition of the AD. To check whether the object has been created, use:
configPartition = (Get-ADRootDSE).configurationNamingContext Get-ADObject -Filter * -SearchBase "LDAP://CN=62a0ff2e-97b9-4513-943f-0d221bd30080,CN=Device Registration Configuration,CN=Services,$($configPartition)"
You can check the AD schema version with PowerShell with:
Get-ADObject (Get-ADRootDSE).schemaNamingContext -Property objectVersion
Output of version 88 means Windows Server 2019, 87 means Windows Server 2016, and 69 means Windows Server 2012 R2.
If you use Exchange, you can find the schema version for Exchange with:
Get-ADObject -Identity "CN=ms-Exch-Schema-Version-Pt,$((Get-ADRootDSE).schemaNamingContext)" -Properties rangeUpper | SELECT rangeUpper
Exchange Server 2019 has version numbers starting from 17000, whereas Exchange version 2016 is in the range of 15317 to 15333.
Evaluating Password Protection
If you use Azure AD and have premium licenses, you probably also use the AD Password Protection for Windows feature, which extends password checking on domain controllers to include logic and insights from Azure AD. The feature prohibits users from choosing common or easy-to-guess passwords when changing passwords or from choosing passwords that you list as undesirable in Azure AD [2]. When it runs, the agent required for this on the domain controller logs how many password changes were rejected because they were too weak or are on your undesirables list:
Get-AzureADPasswordProtectionSummaryReport -DomainController NTTEST-DC-01 DomainController: NTTEST-DC-01 PasswordChangesValidated: 4 PasswordSetsValidated: 2 PasswordChangesRejected: 7 PasswordSetsRejected: 5 ...
Conclusions
This AD PowerShell exploration shows that you can automate common searches and tasks with very little overhead and create tiny scripts that you store in your favorite development environment to make your work easier. Often it doesn't take much work at all: If you structure your administration workstation with a good code editor for PowerShell, you can start automating Active Directory quite quickly and flexibly.