Why Discovery First?
Ghost Accounts
Accounts tied to decommissioned applications. The app is gone; the account persists with full permissions and a password that hasn't changed in years.
Never-Rotated Passwords
Service accounts routinely excluded from password policies because developers feared breaking integrations. Common: 5–15 year old passwords.
Shadow IT Accounts
Accounts created by developers, DBAs, or middleware teams without IT's knowledge. Often named ad-hoc, stored in scripts or config files.
MSAs & gMSAs
Managed Service Accounts and Group Managed Service Accounts. Often overlooked in discovery because they self-manage passwords — but still require inventory and governance.
The Discovery-First Methodology
Discovery
Scan
Scan
Scoring
Build
Rotate
AD Query Techniques
Start with the known patterns — "svc_", "sa_", "_svc", "-service". This is fast and catches formally-created accounts.
# Find accounts with common service account naming conventions Get-ADUser -Filter { (SamAccountName -like "svc_*") -or (SamAccountName -like "sa_*") -or (SamAccountName -like "*_svc") -or (SamAccountName -like "*service*") } -Properties SamAccountName, PasswordLastSet, LastLogonDate, PasswordNeverExpires, Enabled, Description, MemberOf | Select-Object SamAccountName, Enabled, PasswordLastSet, LastLogonDate, PasswordNeverExpires, Description | Sort-Object PasswordLastSet | Export-Csv ".\svc_naming_$(Get-Date -f yyyyMMdd).csv" -NoTypeInformation
This is the single most important flag. Legitimate service accounts often have PNE set — this query surfaces every one of them regardless of name.
# All enabled accounts with Password Never Expires set Get-ADUser -Filter { PasswordNeverExpires -eq $true -and Enabled -eq $true } -Properties PasswordLastSet, LastLogonDate, Description, PasswordNeverExpires, MemberOf, ServicePrincipalNames | Select-Object @{n='Account';e={$_.SamAccountName}}, @{n='PwdAge_Days';e={ (New-TimeSpan -Start $_.PasswordLastSet).Days }}, LastLogonDate, Description, @{n='HasSPN';e={$_.ServicePrincipalNames.Count -gt 0}} | Sort-Object PwdAge_Days -Descending | Format-Table -AutoSize
Any account with an SPN registered can be Kerberoasted by any authenticated domain user. These are high-priority discovery targets.
# Find all accounts with Service Principal Names (SPNs) Get-ADUser -Filter {ServicePrincipalNames -ne "$null"} -Properties * | ForEach-Object { foreach ($spn in $_.ServicePrincipalNames) { [PSCustomObject]@{ Account = $_.SamAccountName SPN = $spn PwdLastSet = $_.PasswordLastSet PwdNeverExp = $_.PasswordNeverExpires Enabled = $_.Enabled LastLogon = $_.LastLogonDate } } } | Export-Csv ".\spn_accounts.csv" -NoTypeInformation # Alternatively use LDAP filter (faster on large domains) Get-ADUser -LDAPFilter "(servicePrincipalName=*)" -Properties ServicePrincipalNames
Accounts that haven't authenticated in 90+ days are candidates for orphan status. Cross-reference against application inventory to confirm.
# Accounts inactive for 90+ days (adjust threshold as needed) $threshold = (Get-Date).AddDays(-90) Search-ADAccount -AccountInactive -TimeSpan (New-TimeSpan -Days 90) -UsersOnly | Get-ADUser -Properties PasswordNeverExpires, PasswordLastSet, LastLogonDate, Description, ServicePrincipalNames | Where-Object { $_.PasswordNeverExpires -eq $true } | Select-Object SamAccountName, Enabled, LastLogonDate, PasswordLastSet, Description | Sort-Object LastLogonDate | Format-Table -AutoSize # Also check accounts that have NEVER logged on Get-ADUser -Filter {LastLogonDate -notlike "*" -and Enabled -eq $true} -Properties LastLogonDate, PasswordNeverExpires | Where-Object {$_.PasswordNeverExpires}
MSAs and gMSAs self-manage their passwords but still require governance — especially to understand which hosts use them and whether they're still needed.
# List all Managed Service Accounts Get-ADServiceAccount -Filter * -Properties * | Select-Object Name, SamAccountName, Enabled, PrincipalsAllowedToRetrieveManagedPassword, ServicePrincipalNames, HostComputers | Format-List # Group Managed Service Accounts specifically Get-ADObject -LDAPFilter "(objectClass=msDS-GroupManagedServiceAccount)" -Properties * | Select-Object Name, DistinguishedName, Created, Modified, @{n='ManagedBy';e={$_.managedBy}}
Run this master query to pull all potential service accounts from AD in a single pass. This is your raw material for the inventory.
# Master sweep: PNE + SPN + naming convention + disabled with SPN $allSvcAccounts = Get-ADUser -Filter { PasswordNeverExpires -eq $true -or ServicePrincipalNames -ne "$null" -or SamAccountName -like "svc_*" -or SamAccountName -like "sa_*" } -Properties SamAccountName, Enabled, PasswordLastSet, LastLogonDate, PasswordNeverExpires, Description, MemberOf, ServicePrincipalNames, DistinguishedName, Created, Modified, PasswordNotRequired $allSvcAccounts | Select-Object -Property * | Export-Csv ".\master_svc_discovery_$(Get-Date -f yyyyMMdd_HHmm).csv" -NoTypeInformation Write-Host "Found $($allSvcAccounts.Count) candidate service accounts" -ForegroundColor Cyan
Common LDAP Filters Reference
| Purpose | LDAP Filter | Risk Level |
|---|---|---|
| Password never expires | (userAccountControl:1.2.840.113556.1.4.803:=65536) | High |
| Password not required | (userAccountControl:1.2.840.113556.1.4.803:=32) | Critical |
| Has SPN registered | (servicePrincipalName=*) | High |
| Account is disabled | (userAccountControl:1.2.840.113556.1.4.803:=2) | Medium |
| Pre-authentication not required (AS-REP roastable) | (userAccountControl:1.2.840.113556.1.4.803:=4194304) | Critical |
| Group Managed Service Account | (objectClass=msDS-GroupManagedServiceAccount) | Info |
Delinea Scanner Config
One discovery source per AD domain or forest. The source defines the credentials used to scan and the scope of the search.
The scanner configuration controls what Delinea looks for. Use the reference panel below to set optimal values for a full discovery sweep.
Scan Items define what types of accounts Delinea will identify — local accounts, domain accounts, service accounts, scheduled tasks. Enable all scan item types for initial discovery.
After the scan, navigate to Discovery → Unmanaged Accounts. Filter by "Service Account" type. Review each result and either import to a secret or flag as non-applicable.
| Scan Item Type | Enable? | Rationale |
|---|---|---|
| AD Domain Users | ✓ Enable | Core discovery — identifies all AD user objects |
| Windows Services — Domain Accounts | ✓ Enable | Finds domain accounts running as Windows services |
| Windows Services — Local Accounts | ✓ Enable | Finds local service accounts (often overlooked) |
| Scheduled Tasks — Domain | ✓ Enable | Critical — high frequency of embedded credentials here |
| Scheduled Tasks — Local | ✓ Enable | Local task accounts are common in legacy environments |
| IIS Application Pools | ⚠ Enable | Web farms often run pools under domain service accounts |
| SQL Server Service Accounts | ✓ Enable | SQL agent, SQL server engine often use privileged svc accounts |
| COM+ Applications | Optional | Older middleware; enable for legacy Windows environments |
Services & Scheduled Tasks
Use this to interrogate a single server for services running under non-SYSTEM/LOCAL SERVICE accounts.
# Query services on a single host — filter out built-in accounts Get-WmiObject -Class Win32_Service | Where-Object { $_.StartName -notmatch '^(LocalSystem|NT AUTHORITY|NT SERVICE)' -and $_.StartName -ne $null } | Select-Object Name, DisplayName, StartName, StartMode, State | Format-Table -AutoSize # More detail with service paths (for script injection risk assessment) Get-WmiObject Win32_Service | Where-Object { $_.StartName -match '\\' } | # Domain accounts contain backslash Select-Object Name, StartName, PathName, StartMode | Sort-Object StartName
Run against all domain-joined Windows servers. Requires WinRM enabled or remote WMI access. Run from an elevated session.
# Get all domain-joined servers from AD $servers = Get-ADComputer -Filter {OperatingSystem -like "*Server*"} | Select-Object -ExpandProperty Name $results = foreach ($server in $servers) { try { Invoke-Command -ComputerName $server -ScriptBlock { Get-WmiObject Win32_Service | Where-Object { $_.StartName -match '\\' -and $_.StartName -notmatch 'NT SERVICE' } | Select-Object @{n='Host';e={$env:COMPUTERNAME}}, Name, StartName, StartMode, State } -ErrorAction Stop } catch { [PSCustomObject]@{Host=$server; Error=$_.Exception.Message} } } $results | Export-Csv ".\domain_service_accounts_$(Get-Date -f yyyyMMdd).csv" -NoTypeInformation Write-Host "Scanned $($servers.Count) servers — found $($results.Count) service account usages"
Scheduled tasks with "Run As" users set to domain accounts are among the most common sources of forgotten credentials. The password is stored in the Windows credential store on the host.
# Local machine — find tasks with domain RunAs accounts Get-ScheduledTask | ForEach-Object { $task = $_ $principal = $task.Principal if ($principal.UserId -match '\\') { # domain\user format [PSCustomObject]@{ TaskName = $task.TaskName TaskPath = $task.TaskPath RunAsUser = $principal.UserId LogonType = $principal.LogonType State = $task.State LastRun = (Get-ScheduledTaskInfo -TaskName $task.TaskName -TaskPath $task.TaskPath -EA SilentlyContinue).LastRunTime } } } | Format-Table -AutoSize # Remote — scan all servers for scheduled tasks $servers | ForEach-Object { Invoke-Command -ComputerName $_ -ScriptBlock { schtasks /query /fo CSV /v } -ErrorAction SilentlyContinue } | ConvertFrom-Csv | Where-Object { $_.'Run As User' -match '\\' } | Select-Object 'HostName', 'TaskName', 'Run As User', 'Status'
IIS application pools running under domain accounts are common in web-heavy environments. Query the IIS configuration directly.
# Requires WebAdministration module (available on IIS servers) Import-Module WebAdministration Get-WebConfiguration "system.applicationHost/applicationPools/add" | Where-Object {$_.processModel.userName -ne ""} | Select-Object Name, @{n='Identity';e={$_.processModel.identityType}}, @{n='UserName';e={$_.processModel.userName}}, State, ManagedRuntimeVersion | Format-Table -AutoSize # Equivalent appcmd approach (works without PowerShell module) & "$env:windir\system32\inetsrv\appcmd.exe" list apppool /processModel.userName:*
# Search common config files for credential-bearing patterns # SENSITIVE — do not run in logged sessions without encryption $patterns = @( 'password\s*=', 'pwd\s*=', 'connectionstring.*password', 'credentials.*user', 'RunAs.*user' ) $searchPaths = @( 'C:\inetpub', 'C:\Windows\System32\Tasks', 'C:\Program Files' ) foreach ($path in $searchPaths) { Get-ChildItem $path -Recurse -Include *.config,*.xml,*.json,*.ini,*.bat,*.ps1 -EA SilentlyContinue | Select-String -Pattern ($patterns -join '|') -CaseSensitive:$false | Select-Object Filename, LineNumber, @{n='Snippet';e={$_.Line.Trim().Substring(0,[Math]::Min(80,$_.Line.Trim().Length))}} | Export-Csv ".\cred_patterns.csv" -Append -NoTypeInformation }
Account Risk Signals Reference
| Signal | Risk | Recommended Action |
|---|---|---|
| Password age > 365 days + service account | High | Vault immediately; plan rotation window |
| Password age > 3 years | Critical | Treat as compromised; emergency rotation |
| No LastLogon + PNE + has SPN | High | Investigate orphan status; disable if confirmed |
| Domain admin group member | Critical | Immediate review; remove DA if not justified |
| Plaintext password in config file | Critical | Vault, rotate, remove from file, use DPAPI or vault SDK |
| MSA/gMSA not in Delinea inventory | Medium | Add to inventory record; verify host associations |
| Service account with interactive logon rights | Medium | Remove interactive logon right via GPO |
| AS-REP roastable (no pre-auth) | Critical | Enable Kerberos pre-authentication immediately |
Inventory Builder
Inventory Checklist — Required Fields
Every service account in your inventory must have these fields completed before vaulting. Check off each category as you've sourced the data for your accounts.
Vaulting Readiness Criteria
Your inventory is the foundation. Before you begin vaulting, verify these organizational prerequisites are also in place.