Category Archives: Active Directory

Finding the Source of Active Directory Account Lockouts with PowerShell

Finding the Source of Active Directory Account Lockouts with PowerShell

As organizations continue their journey towards Microsoft 365, Entra ID, and cloud-first identity management, traditional on-premises Active Directory environments are becoming less common. Many organizations today operate in hybrid environments where only a handful of services still rely on their local Active Directory.

Despite this shift, one classic problem hasn’t disappeared:

Users still get locked out.

And when they do, finding why and where the lockout originated can still be surprisingly frustrating.

The Challenge

Account lockouts are typically caused by:

  • Stale credentials stored in Windows Credential Manager
  • Scheduled tasks running with old passwords
  • Windows services using outdated service accounts
  • Mapped drives
  • Mobile devices
  • Legacy applications
  • Cached credentials on remote servers

Although Active Directory records lockout events, they’re stored in the Security log on the Domain Controller that processed the lockout. In environments with multiple Domain Controllers, tracking down the relevant event often means logging into several servers and searching manually.

That’s exactly the problem this PowerShell script aims to solve.

What the Script Does

Instead of checking each Domain Controller individually, the script:

  1. Prompts for a user’s sAMAccountName.
  2. Lets you define how far back in time to search (default is 24 hours).
  3. Automatically discovers every Domain Controller in the domain.
  4. Queries the Security log on each Domain Controller for Event ID 4740 (Account Lockout).
  5. Parses the event XML to extract the important fields.
  6. Displays all matching lockout events in a single consolidated table.
  7. Optionally exports the results to CSV for documentation or further investigation.

The result is a quick overview showing:

  • When the lockout happened
  • Which Domain Controller processed it
  • Which account was locked
  • The computer that triggered the lockout
  • The domain involved
  • Which security principal recorded the event

How It Works

The script leverages the Active Directory PowerShell module to discover the current domain and all available Domain Controllers.

For each Domain Controller, it remotely queries the Windows Security event log using:

Get-WinEvent

filtered for:

  • Security Log
  • Event ID 4740
  • Configurable time window

Rather than relying on localized event messages, the script converts each event to XML and extracts the structured EventData fields. This makes the script language-independent and significantly more reliable across different Windows installations.

Only events where the TargetUserName matches the supplied account are returned.

If a Domain Controller cannot be contacted or the Security log cannot be read, the script continues processing the remaining servers and reports the error instead of terminating.

Why This Approach?

Many existing lockout troubleshooting scripts rely on querying a single Domain Controller or using the lockoutstatus.exe utility.

While those tools certainly have their place, they often don’t immediately tell you which device actually caused the lockout.

By reading Event ID 4740 directly from every Domain Controller, this script provides the information administrators usually need first:

Which computer is repeatedly locking out my user?

In many cases, that single piece of information dramatically reduces troubleshooting time.

Example Output

The resulting output is intentionally simple:

TimeDomain ControllerLocked AccountCaller Computer
2026-07-10 08:14DC01jsmithLAPTOP-123
2026-07-10 08:14DC02jsmithLAPTOP-123

Optionally, the data can also be exported to CSV for reporting or attaching to support cases.

Final Thoughts

Even as more organizations embrace cloud identity, hybrid Active Directory environments will likely remain with us for years to come. And as long as on-premises Active Directory exists, account lockouts will continue to generate support tickets.

Having a simple script that automatically searches every Domain Controller and pinpoints the originating device can save valuable troubleshooting time and eliminate much of the guesswork.

Sometimes, the simplest tools end up being the ones you reach for the most.

#Need to run on a DC's, or a machine with PowerShell module ActiveDirectory installed.
#The concurrent user, need access to security logs on each domain conntroller from where the script is running.

Import-Module ActiveDirectory -ErrorAction Stop

$AccountName = Read-Host "Enter the AD account name / sAMAccountName"
$HoursBack = Read-Host "How many hours back would you like to search? Default is 24"

if ([string]::IsNullOrWhiteSpace($HoursBack)) {
    $HoursBack = 24
}

$StartTime = (Get-Date).AddHours(-[int]$HoursBack)

$Domain = Get-ADDomain
$DCs = Get-ADDomainController -Filter * | Sort-Object HostName

Write-Host ""
Write-Host "Domain: $($Domain.DNSRoot)"
Write-Host "Searching for lockout events for '$AccountName' since $StartTime"
Write-Host "Domain Controllers found: $($DCs.Count)"
Write-Host ""

$Results = foreach ($DC in $DCs) {
    Write-Host "Checking $($DC.HostName)..."

    try {
        $Events = Get-WinEvent -ComputerName $DC.HostName -FilterHashtable @{
            LogName   = 'Security'
            Id        = 4740
            StartTime = $StartTime
        } -ErrorAction Stop

        foreach ($Event in $Events) {
            $Xml = [xml]$Event.ToXml()

            $EventData = @{}
            foreach ($Data in $Xml.Event.EventData.Data) {
                $EventData[$Data.Name] = $Data.'#text'
            }

            if ($EventData['TargetUserName'] -ieq $AccountName) {
                [PSCustomObject]@{
                    TimeCreated        = $Event.TimeCreated
                    DomainController   = $DC.HostName
                    LockedOutAccount   = $EventData['TargetUserName']
                    CallerComputerName = $EventData['CallerComputerName']
                    TargetDomainName   = $EventData['TargetDomainName']
                    SubjectUserName    = $EventData['SubjectUserName']
                    EventId            = $Event.Id
                }
            }
        }
    }
    catch {
        if ($_.Exception.Message -notlike "*No events were found*") {
            [PSCustomObject]@{
                TimeCreated        = $null
                DomainController   = $DC.HostName
                LockedOutAccount   = $AccountName
                CallerComputerName = "Unable to read the Security log"
                TargetDomainName   = $null
                SubjectUserName    = $_.Exception.Message
                EventId            = $null
            }
        }
    }
}

Write-Host ""
Write-Host "Results:"
Write-Host ""

$Results |
    Sort-Object TimeCreated -Descending |
    Format-Table -AutoSize

$Export = Read-Host "Would you like to export the results to CSV? (Y/N)"

if ($Export -match '^[Yy]') {
    $Path = ".\AD-Lockout-$AccountName-$(Get-Date -Format 'yyyyMMdd-HHmmss').csv"

    $Results |
        Sort-Object TimeCreated -Descending |
        Export-Csv -Path $Path -NoTypeInformation -Encoding UTF8

    Write-Host "Results exported to: $Path"
}

Selfsign with Windows Certification Authority

If you just create a certificate, and it signed by a Windows Certification Authority, the certs is working just fine in the old Internet Explorer, but Chrome/Firefox and the new Edge browser will failed, with Common Name invalid, the precise error in Chrome is: NET::ERR_CERT_COMMON_NAME_INVALID.

This is because a normal certificate request, does not included a subject alternative name.
So to fix this you new to request the certficate, by an inf file instead

So first create a new Certreq.inf file and paste this into it:

;----------------- request.inf -----------------
[Version]

Signature="$Windows NT$"

[NewRequest]

Subject = "CN=kennethdalbjerg.dk, OU=Hosting, O=KennethDalbjerg, L=DK, S=DK, C=DK" ; replace attribues in this line
KeySpec = 1
KeyLength = 2048
; Can be 2048, 4096, 8192, or 16384.
; Larger key sizes are more secure, but have
; a greater impact on performance.
Exportable = TRUE
FriendlyName = "Kennethdalbjerg.dk-2024"
MachineKeySet = TRUE
SMIME = False
PrivateKeyArchive = FALSE
UserProtected = FALSE
UseExistingKeySet = FALSE
ProviderName = "Microsoft Strong Cryptographic Provider"
ProviderType = 12
RequestType = PKCS10
KeyUsage = 0xa0

[EnhancedKeyUsageExtension]

OID=1.3.6.1.5.5.7.3.1 ; this is for Server Authentication

[RequestAttributes]

SAN="kennethdalbjerg.dk&dns=www.kennethdalbjerg.dk"
;-----------------------------------------------

Replace kennethdalbjerg.dk and www.kennethdalbjerg.dk, with the name you want. If you don’t want more than one common name, in yours certificate. Then you just in last line remove:
&dns=www.kennethdalbjerg.dk
Also replace the name in FriendlyName.

Now run this command to prepare the CSR file:

certreq -new certreq.inf certreq.csr

Copy the certreq.csr to yours Certication Authority server, and the run this command on that server

certreq -Submit -Attrib "CertificateTemplate:webserver" -Config - c:\certreq.txt

Replace CertificateTemplate:webserver with the correct name for the template that you want to use.
Save the certificate by name, i use signcert.cer
Copy signcert.cert back to the server, that you have generate the CSR file on, and run this command on that server

certreq -accept signcert.cer

ADFS – Bad Request 400 in Internet Explorer

Today i have a customer that got an Bad Request Error 400, when he try to logon to ADFS from inside their own network, with domain connected computers.
The error is only showing up in Internet Explorer, witch lead me to it might be a kerberos thing.

The service account that run the ADFS services, is “ita_service_admin”.

So looking at that account with setspn i can see witch SPN is register to ita_service_admin account.

setspn -l XXX\ita_service_admin

The where no SPN for the account. The ADFS url is: https://fs.YYY.dk so i added the following SPN to ita_service_Admin account:

setspn -S HOST/FS.YYY.DK XXX\ITA_SERVICE_ADMIN
setspn -S HTTP/FS.YYY.DK XXX\ITA_SERVICE_ADMIN

But I got an error saying that HOST/FS.YYY.DK already exist. So i look for the SPN in the domain

This Code, will show all SPN in use in you Active Directory

cls
$search = New-Object DirectoryServices.DirectorySearcher([ADSI]“”)
$search.filter = “(servicePrincipalName=*)”
$results = $search.Findall()

#list results
foreach($result in $results)
{
$userEntry = $result.GetDirectoryEntry()
Write-host “Object Name = “ $userEntry.name -backgroundcolor “yellow” -foregroundcolor “black”
Write-host “DN      =      “  $userEntry.distinguishedName
Write-host “Object Cat. = “  $userEntry.objectCategory
Write-host “servicePrincipalNames”        $i=1

foreach($SPN in $userEntry.servicePrincipalName)
{
  Write-host “SPN(“ $i “)   =      “ $SPN       $i+=1
  }
Write-host “”

}

Find the SPN, and delete it from the account, i find the account under Administrator, so it looks like someone have change the service account for ADFS service from Administrator to ITA_SERVICE_ADMIN account in the past, without moving the SPN.

So delete the SPN from administrator account

setspn -D HOST/FS.YYY.DK XXX\administrator

Then try to add it again to ITA_Service_Admin account.

setspn -S HOST/FS.YYY.DK XXX\ITA_SERVICE_ADMIN

Restart the ADFS service, and the ADFS is working again also from Internet Explorer.

Remote Desktop seesions – See sessions on all servers in Active directory

If you have enabled that a user get lock out in Active Directory, if the user type the password wrong X times.
Then you might have experienced, that yours admin account get locked out multiple times, just after you have change the password for the admin account.

It could be that you have left a session on a server in you Active Directory logged on, and that session now trying to authenticated it self, by using you old password.

You can run this script, to check all servers in Active Directory, to see where there are user session on it.

function Get-LoggedOnUsers ($server) {
    if($server -eq $null){
        $server = "localhost"
    }
 
    $users = @()
    # Query using quser, 2>$null to hide "No users exists...", then skip to the next server
    $quser = quser /server:$server 2>$null
    if(!($quser)){
        Continue
    }
 
    #Remove column headers
    $quser = $quser[1..$($quser.Count)]
    foreach($user in $quser){
        $usersObj = [PSCustomObject]@{Server=$null;Username=$null;SessionName=$null;SessionId=$Null;SessionState=$null;LogonTime=$null;IdleTime=$null}
        $quserData = $user -split "\s+"
 
        #We have to splice the array if the session is disconnected (as the SESSIONNAME column quserData[2] is empty)
        if(($user | select-string "Disc") -ne $null){
            #User is disconnected
            $quserData = ($quserData[0..1],"null",$quserData[2..($quserData.Length -1)]) -split "\s+"
        }
 
        # Server
        $usersObj.Server = $server
        # Username
        $usersObj.Username = $quserData[1]
        # SessionName
        $usersObj.SessionName = $quserData[2]
        # SessionID
        $usersObj.SessionID = $quserData[3]
        # SessionState
        $usersObj.SessionState = $quserData[4]
        # IdleTime
        $quserData[5] = $quserData[5] -replace "\+",":" -replace "\.","0:0" -replace "Disc","0:0"
        if($quserData[5] -like "*:*"){
            $usersObj.IdleTime = [timespan]"$($quserData[5])"
        }elseif($quserData[5] -eq "." -or $quserData[5] -eq "none"){
            $usersObj.idleTime = [timespan]"0:0"
        }else{
            $usersObj.IdleTime = [timespan]"0:$($quserData[5])"
        }
        # LogonTime
        $usersObj.LogonTime = (Get-Date "$($quserData[6]) $($quserData[7]) $($quserData[8] )")
 
        $users += $usersObj
    }
    return $users
}

$strCategory = "computer" 
$strOperatingSystem = "Windows*Server*" 
 
$objDomain = New-Object System.DirectoryServices.DirectoryEntry 
 
$objSearcher = New-Object System.DirectoryServices.DirectorySearcher 
$objSearcher.SearchRoot = $objDomain 

$objSearcher.Filter = ("OperatingSystem=$strOperatingSystem") 
 
$colProplist = "name" 

foreach ($i in $colPropList){$objSearcher.PropertiesToLoad.Add($i)} 

$colResults = $objSearcher.FindAll() 

foreach ($objResult in $colResults) 
    { 
    If ($Err -ne $null) {
        Remove-Variable Err
    }
    $objComputer = $objResult.Properties;  
    $ServerName = $objComputer.name
    $isalive = Test-Connection -BufferSize 32 -Count 1 -ComputerName $servername -Quiet
    if($isalive -eq $True) {
        $wmi = Get-WmiObject -class "Win32_Process" -namespace "root\cimv2" -computername $ServerName -EV Err -EA SilentlyContinue
        if ($Err) {
         } Else {
            Get-LoggedOnUsers $ServerName | ft -autosize
        }

    }
}

 

 

You can change this last command:

Get-LoggedOnUsers $ServerName | ft -autosize

to

Get-LoggedOnUsers $ServerName | ? { $_.Username -eq ‘kda’ } | ft -autosize

to only see where KDA is loggedon

Problems deleting a old mailbox database

I have a customer, where we have taken over the support from an old supplier, so we don’t know much about their background.

The first thing we do, was to update the Exchange Server.
But after have move all mailboxes, public folders, system mailbox.

I still got this error:

This mailbox database contains one or more mailboxes, mailbox plans, archive mailboxes, or arbitration mailboxes. To ge
t a list of all mailboxes in this database, run the command Get-Mailbox -Database <Database ID>. To get a list of all m
ailbox plans in this database, run the command Get-MailboxPlan. To get a list of archive mailboxes in this database, ru
n the command Get-Mailbox -Database <Database ID> -Archive. To get a list of all arbitration mailboxes in this database
, run the command Get-Mailbox -Database <Database ID> -Arbitration. To disable a non-arbitration mailbox so that you ca
n delete the mailbox database, run the command Disable-Mailbox <Mailbox ID>. To disable an archive mailbox so you can d
elete the mailbox database, run the command Disable-Mailbox <Mailbox ID> -Archive. Arbitration mailboxes should be move
d to another server; to do this, run the command New-MoveRequest <parameters>. If this is the last server in the organi
zation, run the command Disable-Mailbox <Mailbox ID> -Arbitration -DisableLastArbitrationMailboxAllowed to disable the
arbitration mailbox. Mailbox plans should be moved to another server; to do this, run the command Set-MailboxPlan <Mail
boxPlan ID> -Database <Database ID>.

Mailbox Plan is only used on the cloud.

I have look every places, unto i run this command:

dsquery * domainroot -attr * -limit 0 > result.txt

i then open the result.txt in notepad, and found some reference to the old database

msExchDisabledArchiveDatabaseLink: CN=Exch2013MB_Archive,CN=Databases,CN=Exchange Administrative Group (FYDIBOHF23SPDLT),CN=Administrative Groups,CN=Fibia,CN=Microsoft Exchange,CN=Services,CN=Configuration,DC=XXXX,DC=local

After delete this reference, i could delete the database.