Find the service accounts nobody has rotated
The Commvault Metallic breach ended with CISA telling customers to rotate static application secrets. Your Active Directory is full of the same thing: service accounts whose passwords were set once, years ago, and never touched. Here is the PowerShell to find them and put them on a cadence.
The Commvault Metallic breach did not end with a patch. It ended with a rotation order.
In May 2025, CISA advised the affected customers, the limited set that controlled their own Commvault application secrets, to rotate those secrets and the credentials on their Metallic apps and service principals that had been live between February and May 2025. A suspected nation-state actor had used CVE-2025-3928 to plant web shells on Commvault’s Azure-hosted web server and read the client secrets Commvault stored on behalf of its Microsoft 365 backup customers. One static secret, sitting where it had always sat, reached into other tenants. The fix was not a new build. It was changing credentials that should have been on a schedule and never were.
Your Active Directory has the same class of secret, and probably more of it. Every database service account, monitoring agent, backup job, CI/CD runner, and scheduled task runs as some identity, and a lot of those identities are user accounts whose password was typed in once during a project years ago, flagged “password never expires,” and forgotten. A leaked one is durable initial access. This is how to find them, in about ten minutes of PowerShell, and what to do with the list.
It pairs with the host-hardening checks from the same series: those tell you whether a box is safe to trust, this tells you whether the identities running on it are.
Who this affects
Any Windows and Active Directory shop that runs services, scheduled tasks, or integrations under named accounts. The ones that matter most: SQL Server and IIS app-pool identities, which carry a service principal name and are kerberoastable; backup and monitoring agents; and any account whose password is pasted into a config file or a vendor console. Cloud tenants have the same problem in a different shape, in Entra app registrations with long-lived client secrets, which is exactly what bit Commvault. Run all three checks below from a management host with the AD PowerShell module and RSAT installed.
Check 1: AD accounts that carry a service principal name
An account with an SPN is almost always a service account, and an SPN plus a never-expiring password is the kerberoasting target crews go looking for first. Microsoft documents the base filters for finding these accounts; this composes them into one ranked list.
Get-ADUser -Filter * -Properties servicePrincipalName, PasswordLastSet, PasswordNeverExpires, Enabled |
Where-Object { $_.servicePrincipalName -and $_.Enabled } |
Select-Object SamAccountName, PasswordLastSet, PasswordNeverExpires,
@{N='PwdAgeDays'; E={ if ($_.PasswordLastSet) {
[int]((Get-Date) - $_.PasswordLastSet).TotalDays } else { 'never-set' } }} |
Sort-Object PasswordLastSet
Bad answer: any enabled account here with PasswordNeverExpires = True and PwdAgeDays in the hundreds or thousands. That password is a permanent static secret, and because the account has an SPN, any domain user can request a service ticket and crack it offline at their leisure. Every day it stays is another day the same hash keeps working. A PwdAgeDays of never-set is its own flag: the password was never changed through a normal reset.
Check 2: Scheduled tasks running as a named account
Get-ScheduledTask | Where-Object {
$_.Principal.LogonType -eq 'Password' -and
$_.Principal.UserId -notmatch '^(NT AUTHORITY\\|.*\$$)'
} | Select-Object TaskName, TaskPath,
@{N='RunAs'; E={ $_.Principal.UserId }},
@{N='LogonType'; E={ $_.Principal.LogonType }}
A task with LogonType of Password stores the account’s password so it can run unattended, and that stored credential is static until someone changes it. The filter drops the built-in NT AUTHORITY principals and anything ending in $ (gMSAs and computer accounts, which Windows rotates for you), leaving the tasks that run as a named user with a saved password.
Bad answer: any task outside \Microsoft\ running as a domain account with LogonType Password. Note what it runs as, because you will rotate that account and the task’s stored password has to be updated in the same motion, or the task starts failing at its next scheduled run.
Check 3: Windows services running as a named account
Get-CimInstance Win32_Service |
Where-Object {
$_.StartName -and
$_.StartName -notmatch '^(LocalSystem|NT AUTHORITY\\|NT SERVICE\\|.*\$$)'
} |
Select-Object Name, StartName, State, StartMode |
Sort-Object StartName
StartName is the identity the service runs as. LocalSystem, the NT AUTHORITY built-ins, and NT SERVICE\ virtual accounts manage themselves. An account ending in $ is a gMSA or a computer account. What the filter leaves is a service running as a plain domain or local user account, which means a password someone set by hand and a service that breaks the moment that password changes without the service being updated to match.
Bad answer: a Running service under domain\svc-something whose account also came back stale in Check 1. That intersection is what you care about: a live service, a static password, and no automatic rotation behind it.
What’s already handled
Before you build a rotation list, subtract what you don’t have to touch. Get-ADServiceAccount -Filter * lists the managed service accounts in the domain, gMSAs and sMSAs, whose passwords Windows rotates on its own. The gap between the named accounts from Checks 1 through 3 and this list is your actual backlog. If the command returns nothing, you have no managed service accounts at all, and every service identity in the estate is hand-managed.
Rank, then rotate or replace
You now have three lists. Rank them on two axes before you touch anything: staleness, meaning password age, oldest first; and blast radius, meaning privilege and reachability. An SPN account that sits in Domain Admins or runs an internet-reachable service jumps the queue regardless of age. A stale account driving one internal report can wait a week.
Then decide, per account, rotate or replace. Rotating buys time. Replacing removes the problem. This table maps the common classes to both.
| Account class | How to find it | Durable fix | Interim rotation cadence |
|---|---|---|---|
| AD user account with an SPN (SQL Server, IIS app pools) | Check 1 (Get-ADUser SPN filter, sort by PasswordLastSet) | Convert to gMSA | 1 year; 90 days if privileged or internet-reachable |
Scheduled task with a stored password (LogonType Password) | Check 2 (Get-ScheduledTask → Principal) | gMSA-backed task | Rotate with the account it runs as |
| Windows service under a named account | Check 3 (Get-CimInstance Win32_Service, StartName) | gMSA | Rotate with the account it runs as |
| Local (non-domain) service or admin account on a member server | Local SAM; check Windows LAPS coverage | Windows LAPS-managed rotation | LAPS handles it; 1 year if unmanaged |
| App integration secret / API token / OAuth client secret | Entra app registrations, secrets vault, config files | Managed identity or workload identity federation | 90 days to 1 year; immediately on exposure |
| DB / monitoring / CI-CD / backup credential in a config file | Config files, CI/CD variables, vendor consoles | Managed identity or vault-brokered short-lived creds | 90 days to 1 year; immediately on exposure |
The durable fix is to stop holding the password yourself
Rotation is maintenance you will forget again. The exit is an account type where nobody holds the password to begin with.
On-premises, that is a group managed service account. Windows generates a 240-byte random password and changes it every 30 days, with no scheduled downtime and no secret for anyone to leak, paste, or forget. The migration is documented and undramatic: deploy the KDS root key once, create the gMSA, install it on the hosts that run the service, switch the service or task to the gMSA with a blank password, confirm it works, and delete the old account. Microsoft’s own guidance is blunt about the alternative: it does not recommend using on-premises user accounts as service accounts and tells you to convert them to a gMSA or sMSA wherever the service supports it.
In Azure, the equivalent is a managed identity. Microsoft calls manual handling of secrets “a known source of security issues and outages,” and the point of a managed identity is that “credentials aren’t even accessible to you.” For an Entra app that needs a credential, workload identity federation lets a managed identity stand in for the client secret, so there is no secret to rotate or steal. That is the direct answer to the Commvault failure mode: the thing read out of Azure was a stored client secret, and a managed identity is that same account with no stored secret to read.
Not every service supports these. Some vendor apps still demand a plain user account with a typed password, and those accounts stay on a manual cadence. They are the ones worth tracking by name.
Cadence until you get there
For the accounts you cannot yet convert, put them on a schedule instead of on someone’s memory. What I run, and what I would hand my team:
- Rotate every service-account password at least once a year. A gMSA changes its own password every 30 days; a hand-managed secret that has not moved in over a year is already far behind the platform’s own bar.
- Tighten to every 90 days for anything privileged or internet-reachable.
- Rotate off-cycle, immediately, on two triggers: an admin with access to the credential leaves, or you have any reason to think it was exposed. The Commvault customers who rotated were acting on the second one.
- Rotate the account and update every place its password is stored inside the same change window. A rotated password that is not updated in the service, the task, and the config file is just an outage you scheduled for yourself.
These are operational defaults, not a compliance standard. Set the intervals where your risk sits and write them down, because the failure mode here is not a wrong number, it is no number.
When this becomes an incident
Three findings move an account out of the maintenance queue and into a ticket someone works today. An SPN account in a privileged group with a multi-year-old password, kerberoastable and high-value at the same time. A service account whose password is sitting in a plaintext config file or a wiki page. And any account you cannot tie to a current owner or a running service, which is the worst of the three, because you cannot safely rotate what you cannot prove is still in use, and you cannot safely leave it either.
The account that ends up in a breach writeup is never the one you rotated last month. It is the one you found this morning, set in 2019, still running, and still working exactly as configured.
Sources
- Advisory Update on Cyber Threat Activity Targeting Commvault’s SaaS Cloud Application (Metallic) (CISA, 2025-05-22)
- SaaS companies in firing line following Commvault attack (The Register, 2025-05-23)
- Introduction to Active Directory service accounts (Microsoft Learn)
- Secure user-based service accounts in Active Directory (Microsoft Learn)
- Secure group managed service accounts (Microsoft Learn)
- Managed identities for Azure resources overview (Microsoft Learn)
Sources
- Advisory Update on Cyber Threat Activity Targeting Commvault's SaaS Cloud Application (Metallic) | CISA
- SaaS companies in firing line following Commvault attack | The Register
- Introduction to Active Directory service accounts | Microsoft Learn
- Secure user-based service accounts in Active Directory | Microsoft Learn
- Secure group managed service accounts | Microsoft Learn
- Managed identities for Azure resources overview | Microsoft Learn
Share
Related field notes
-
Turn on DNS scavenging without deleting records you still need
Windows DNS zones fill up with A and PTR records for servers you retired years ago. Aging and scavenging cleans them out, but a wrong interval deletes live records. Here is the careful way to enable it.
-
Your backup server is joined to the domain it exists to recover
Joining your backup server to the production Active Directory domain puts your last line of recovery inside the same trust boundary as the systems it protects, so one Domain Admin compromise reaches the backups too. Here is the evidence and what to change.
-
One in ten AD accounts is dormant. Here is the blast radius.
More than one in ten Active Directory user accounts is dormant, per Microsoft. Each one keeps its rights while nobody watches. The math, the queries to find them, and how to reap them without breaking a service.
-
One CERT says it's exploited, Microsoft says it isn't, and you patch anyway
A pre-auth SYSTEM RCE on every domain controller doesn't need an exploitation rumor to earn the top of your patch queue. The interesting part is why the alarm and the data disagree, and why that disagreement shouldn't change your call.
Get the free CVE triage cheat sheet
Subscribe and we'll email you the one-page triage flow for fresh CVEs. Plus the weekly digest.
Subscribe