Shore Up
A locksmith walking down a row of identical doors, fitting a fresh lock to each one and writing the new key code for each door into a single ledger that is then locked in a small safe.
WindowsSecurity

Rotate Local Admin Passwords Across Machines with PowerShell

Ketan Aagja9 min read
5.0· 1 rating

Before you run this

This guide gives you a PowerShell script that connects to a list of Windows machines over WinRM and sets a brand-new random password on the built-in local Administrator account of each one, then writes the results to a CSV so you have the new credentials. Its purpose is to kill shared, static, "same password everywhere" local admin accounts.

A few things you need to know before it goes anywhere near production:

  • It changes credentials, and the change is irreversible. Once the script runs against a machine, the old password is gone. There is no "undo" that recovers the previous password — the only way back is to set a new known password. If the script rotates a password and you lose the CSV, you have locked yourself out of that account.
  • It needs elevated rights on the targets. You run it from a normal (non-admin) PowerShell console, but the credential you supply must have local administrator rights on every target machine (typically a domain account that is a member of local Administrators, or a Domain Admin). The targets must have WinRM/PowerShell remoting enabled and reachable.
  • It writes plaintext passwords to a file. That CSV is as sensitive as the passwords themselves. Protect it and treat it as a temporary hand-off to a proper secret store, not a permanent record. This plaintext-storage problem is exactly why Microsoft built LAPS (more on that below).
  • Test it first. Read the script, then run it in dry-run mode (the default) against one test VM. Only flip it to live after you have confirmed it reaches the machine and finds the account. Never paste-and-run it across your whole fleet blind.

Assumptions: a Windows Active Directory domain, Windows PowerShell 5.1 on both the admin workstation and the targets, WinRM enabled (default port 5985, encrypted within the domain via Kerberos), and the target account is the built-in Administrator. I do not cover workgroup/standalone machines, where local-account remoting has extra hurdles.

Use Windows LAPS if you possibly can

Before writing a line of your own code: the standard, supported, well-established answer to this problem is Windows LAPS (Local Administrator Password Solution), now built into current Windows and Windows Server and also available as the older "Microsoft LAPS" MSI for down-level systems. LAPS rotates each machine's local admin password on a schedule and stores it encrypted in Active Directory (or Entra ID), with proper access control and audit — no CSV of plaintext passwords sitting on your desk.

If LAPS is deployed, you don't need a rotation script at all. You manage it through Group Policy and the LAPS PowerShell module — for example Get-LapsADPassword to read a machine's current password and cmdlets to expire it so the client rotates on its next policy cycle. I'm not going to reproduce the exact syntax here from memory; check Microsoft Learn for the "Windows LAPS PowerShell" reference and the Windows LAPS deployment guide for the current cmdlet names and parameters.

The script below is for the in-between case: you don't have LAPS yet, or you have a set of machines outside its scope, and you need to get off shared static passwords today.

The script

Save this as Rotate-LocalAdmin.ps1. It defaults to a dry run that changes nothing.

#Requires -Version 5.1

# ---- Settings you edit ----
$ComputerListPath = "C:\path\to\computers.txt"   # one hostname per line
$LocalAdminName   = "Administrator"              # target account; see note on renamed built-in accounts
$OutputCsv        = "C:\path\to\rotation-results.csv"
$PasswordLength   = 20
$DryRun           = $true   # SAFE DEFAULT: reports what it would do, changes nothing.
                            # Set to $false only when you are ready to actually rotate.

# Credential with LOCAL ADMIN rights on every target machine
$Cred = Get-Credential -Message "Account with admin rights on the target machines"

# .NET password generator (present in Windows PowerShell 5.1 / .NET Framework)
Add-Type -AssemblyName System.Web
function New-RandomPassword {
    param([int]$Length = 20)
    # second argument = number of non-alphanumeric characters
    [System.Web.Security.Membership]::GeneratePassword($Length, 4)
}

$computers = Get-Content -Path $ComputerListPath

$results = foreach ($computer in $computers) {
    try {
        if ($DryRun) {
            # Only confirm the machine is reachable and the account exists.
            $enabled = Invoke-Command -ComputerName $computer -Credential $Cred -ScriptBlock {
                param($User) (Get-LocalUser -Name $User).Enabled
            } -ArgumentList $LocalAdminName -ErrorAction Stop

            [pscustomobject]@{ Computer=$computer; Account=$LocalAdminName; Password=""
                RotatedAt=""; Status="DRYRUN reachable, account enabled=$enabled" }
        }
        else {
            $newPassword = New-RandomPassword -Length $PasswordLength
            $secure      = ConvertTo-SecureString $newPassword -AsPlainText -Force

            Invoke-Command -ComputerName $computer -Credential $Cred -ScriptBlock {
                param($User, $Pwd) Set-LocalUser -Name $User -Password $Pwd
            } -ArgumentList $LocalAdminName, $secure -ErrorAction Stop

            [pscustomobject]@{ Computer=$computer; Account=$LocalAdminName; Password=$newPassword
                RotatedAt=(Get-Date).ToString("s"); Status="Success" }
        }
    }
    catch {
        [pscustomobject]@{ Computer=$computer; Account=$LocalAdminName; Password=""
            RotatedAt=""; Status="Failed: $($_.Exception.Message)" }
    }
}

$results | Format-Table -AutoSize
if (-not $DryRun) {
    $results | Export-Csv -Path $OutputCsv -NoTypeInformation -Encoding UTF8
}

Replace C:\path\to\... and computers.txt with your real paths, and put your target hostnames in that text file, one per line.

A few notes on the non-obvious bits:

  • Set-LocalUser and Get-LocalUser come from the Microsoft.PowerShell.LocalAccounts module, present on Windows 10 / Server 2016 and later. They run on the target inside the Invoke-Command script block.
  • I pass the new password as a SecureString into the remote session; the WinRM channel itself is encrypted within the domain.
  • GeneratePassword is a long-standing .NET Framework method. If you run this under PowerShell 7, that assembly may not be present — stay on Windows PowerShell 5.1 for this script, or swap in your own generator.

If your built-in Administrator has been renamed, $LocalAdminName = "Administrator" won't match. Find the real name — the built-in account always ends in SID -500:

Get-LocalUser | Where-Object SID -Like '*-500' | Select-Object Name, SID

Verify it worked

First, read the output table (and the CSV) — every row should say Success. Investigate anything that says Failed:; the message tells you whether it was unreachable, an access-denied, or a missing account.

Then actually prove a new password works by using it. Take one row from the CSV and test-authenticate with the new credential:

$u = "$env:COMPUTERNAME_or_target\Administrator"   # DOMAIN not needed; local account form is  HOST\Administrator
$p = ConvertTo-SecureString "PASTE-NEW-PASSWORD-HERE" -AsPlainText -Force
$test = [System.Management.Automation.PSCredential]::new("TARGETHOST\$LocalAdminName", $p)

Invoke-Command -ComputerName "TARGETHOST" -Credential $test -ScriptBlock { whoami }

If that returns targethost\administrator, the rotation took. (Local-account remoting can be blocked by remote-UAC token filtering; if the built-in Administrator is disabled or blocked, verify instead by signing in at the console or via Enter-PSSession with a domain admin and checking the account's PasswordLastSet with Get-LocalUser ... | Select PasswordLastSet.)

Undo, and the part you must not skip

There is no rollback to the old password — it no longer exists. If a rotation caused a problem, the fix is to set a new known value the same way, by running the live path against that one machine.

The real follow-through is the CSV. Right after the run:

  1. Move each machine's password into your password manager or secret vault.
  2. Delete the CSV and empty it from any backup or recycle path.
  3. While it exists, lock it down with NTFS permissions to just yourself.

And treat this script as a bridge. Rotating once is good; rotating on a schedule with encrypted at-rest storage and access control is the goal — which brings you right back to deploying Windows LAPS. Do that next.

Written by
Ketan Aagja

Runs enterprise networks and security for a living, and writes Shore Up to turn two decades of hands-on Linux, Windows and mail-server work into guides you can actually use.

More about the author →

Was this article helpful?

Tap a star — no sign-in needed.

Average 5.0 from 1 reader