SCRIPT LIBRARY · POWERSHELL
Create a Dedicated Local Admin Account for Windows LAPS to Manage
Roll out a named local admin account with a random password nobody knows, ready for Windows LAPS to take over, and optionally retire the built-in Administrator.
- What it does
- Makes sure a named local account exists, is enabled, and is in the local Administrators group, creating it with a random 32-character password that's never stored or shown. Can also disable the built-in Administrator.
- Requires
- Windows PowerShell 5.1 (Microsoft.PowerShell.LocalAccounts module, built in)
- A Windows LAPS policy that names the same account
- Permissions
- Local administrator or SYSTEM on the target PC
- Runs on
- Windows 10/11, Windows Server 2016+
- Tested
- Parse-checked and dry-run with mocked LocalAccounts cmdlets in PowerShell 7.4
Part 2 of the thread Local admin, done right
Plenty of shops don't want LAPS managing the built-in Administrator. It's the account every attacker tries first, its SID always ends in 500, and disabling it is on most hardening checklists. The usual answer is a separate local admin account that LAPS manages instead.
The catch is getting that account onto every machine without inventing a password for it. The first version of this post used net user ... /add from a package, which works but leaves you choosing between a blank password and one typed into the script. Neither is great. This script creates the account with a long random password generated on the device, one that's never written down, logged, or returned. Nobody knows it, and nobody needs to, because the moment your LAPS policy applies, LAPS sets its own and backs it up.
One thing first: if your machines run Windows 11 24H2 or Windows Server 2025, Windows LAPS can create and manage the account for you (automatic account management). If that's you, set that in the LAPS policy and you may not need this script at all. For everything older, this is the piece that fills the gap.
<#
.SYNOPSIS
Creates (or repairs) a dedicated local admin account with a random, never-seen password, ready for Windows LAPS to manage.
.DESCRIPTION
Makes sure a named local account exists, is enabled, and is in the local Administrators group.
When it creates the account, it gives it a long random password generated on the device and never
written, logged, or returned. Nobody knows it, and nobody needs to: point your Windows LAPS policy's
AdministratorAccountName at this account and LAPS takes over the password from there.
Optionally disables the built-in Administrator (RID 500). Safe to run again; it only fixes what's off.
.PARAMETER Name
The account name. 20 characters max, no \ / [ ] : ; | = , + * ? < > @ or quotes.
.PARAMETER Description
Description shown on the account.
.PARAMETER DisableBuiltInAdministrator
Also disable the built-in Administrator account.
.EXAMPLE
.\Initialize-LapsAdminAccount.ps1 -Name lapsadmin -WhatIf
.EXAMPLE
.\Initialize-LapsAdminAccount.ps1 -Name lapsadmin -DisableBuiltInAdministrator
#>
[CmdletBinding(SupportsShouldProcess)]
param(
[Parameter(Mandatory)]
[ValidateLength(1, 20)]
[ValidatePattern('^[^\\/\[\]:;|=,+*?<>@"]+$')]
[string]$Name,
[ValidateLength(0, 48)][string]$Description = 'Local admin managed by Windows LAPS',
[switch]$DisableBuiltInAdministrator
)
function New-RandomSecureString {
param([int]$Length = 32)
$chars = [char[]]'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789!#%+-=?@_'
$bytes = New-Object byte[] $Length
$rng = [System.Security.Cryptography.RandomNumberGenerator]::Create()
$rng.GetBytes($bytes); $rng.Dispose()
$secure = New-Object System.Security.SecureString
foreach ($b in $bytes) { $secure.AppendChar($chars[$b % $chars.Length]) }
$secure.MakeReadOnly()
$secure
}
function Write-Step {
param([string]$Step, [string]$Status, [string]$Detail = '')
[pscustomobject]@{ ComputerName = $env:COMPUTERNAME; Account = $Name; Step = $Step; Status = $Status; Detail = $Detail }
if ($Status -eq 'Failed') { $script:failed = $true }
}
$failed = $false
$adminsGroup = Get-LocalGroup -SID 'S-1-5-32-544' # "Administrators" in any language
# 1. The account itself.
$user = Get-LocalUser -Name $Name -ErrorAction SilentlyContinue
if (-not $user) {
if ($PSCmdlet.ShouldProcess($Name, 'Create local account with a random password')) {
try {
$user = New-LocalUser -Name $Name -Password (New-RandomSecureString) -Description $Description -PasswordNeverExpires -AccountNeverExpires -ErrorAction Stop
Write-Step -Step 'Create account' -Status 'Done'
}
catch { Write-Step -Step 'Create account' -Status 'Failed' -Detail $_.Exception.Message }
}
else {
Write-Step -Step 'Create account' -Status 'WhatIf'
Write-Step -Step 'Add to Administrators' -Status 'WhatIf'
}
}
else {
Write-Step -Step 'Create account' -Status 'AlreadyExists'
if (-not $user.Enabled -and $PSCmdlet.ShouldProcess($Name, 'Enable account')) {
try { Enable-LocalUser -Name $Name -ErrorAction Stop; Write-Step -Step 'Enable account' -Status 'Done' }
catch { Write-Step -Step 'Enable account' -Status 'Failed' -Detail $_.Exception.Message }
}
}
# 2. Administrators membership. Add-LocalGroupMember tells us if it's already there, which dodges
# Get-LocalGroupMember's habit of failing on groups that contain orphaned SIDs.
if ($user -and $PSCmdlet.ShouldProcess($adminsGroup.Name, "Add $Name")) {
try {
Add-LocalGroupMember -Group $adminsGroup -Member $user -ErrorAction Stop
Write-Step -Step 'Add to Administrators' -Status 'Done'
}
catch [Microsoft.PowerShell.Commands.MemberExistsException] { Write-Step -Step 'Add to Administrators' -Status 'AlreadyMember' }
catch { Write-Step -Step 'Add to Administrators' -Status 'Failed' -Detail $_.Exception.Message }
}
# 3. Optionally switch off the built-in Administrator, but never the account we just set up.
if ($DisableBuiltInAdministrator) {
$builtIn = Get-LocalUser | Where-Object { $_.SID.Value -match '^S-1-5-21-.+-500$' }
if ($builtIn.Name -eq $Name) {
Write-Step -Step 'Disable built-in Administrator' -Status 'Skipped' -Detail 'That is the account you named.'
}
elseif (-not $builtIn.Enabled) {
Write-Step -Step 'Disable built-in Administrator' -Status 'AlreadyDisabled'
}
elseif ($PSCmdlet.ShouldProcess($builtIn.Name, 'Disable built-in Administrator')) {
try { Disable-LocalUser -SID $builtIn.SID -ErrorAction Stop; Write-Step -Step 'Disable built-in Administrator' -Status 'Done' }
catch { Write-Step -Step 'Disable built-in Administrator' -Status 'Failed' -Detail $_.Exception.Message }
}
}
exit ([int]$failed)
Parameters
| Parameter | Type | Default | What it's for |
|---|---|---|---|
-Name | string | — | Required. The account name, up to 20 characters. Use exactly the name your LAPS policy's AdministratorAccountName setting uses. |
-Description | string | Local admin managed by Windows LAPS | Description shown on the account, 48 characters max. |
-DisableBuiltInAdministrator | switch | — | Also disable the built-in Administrator (the RID 500 account). Skipped if it's the account you named. |
-WhatIf | switch | — | Shows each step it would take. |
Run it
Create the account on this machine.
.\Initialize-LapsAdminAccount.ps1 -Name lapsadminCreate it and disable the built-in Administrator in the same run.
.\Initialize-LapsAdminAccount.ps1 -Name lapsadmin -DisableBuiltInAdministratorAs a ConfigMgr package program.
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\Initialize-LapsAdminAccount.ps1 -Name lapsadmin -DisableBuiltInAdministratorPreview first.
.\Initialize-LapsAdminAccount.ps1 -Name lapsadmin -DisableBuiltInAdministrator -WhatIfWhat you'll see
ComputerName Account Step Status Detail
------------ ------- ---- ------ ------
PC-0142 lapsadmin Create account Done
PC-0142 lapsadmin Add to Administrators Done
PC-0142 lapsadmin Disable built-in Administrator Done
PC-0187 lapsadmin Create account AlreadyExists
PC-0187 lapsadmin Add to Administrators AlreadyMember
PC-0187 lapsadmin Disable built-in Administrator AlreadyDisabled
How it works
- Find Administrators by SID.
S-1-5-32-544is the local Administrators group on every Windows install, whatever language it's in. - Create the account if it's missing. The password is built from cryptographically random bytes straight into a
SecureString. It never exists as a normal string, so it can't end up in a transcript or a log. If the account already exists but is disabled, it gets enabled. - Add it to Administrators. If it's already there, that's reported as
AlreadyMember, not an error. - Optionally disable the built-in Administrator, found by the
-500at the end of its SID so a renamed one is still caught. It will never disable the account you just named. - Report every step as an object, and exit 1 if any step failed, so ConfigMgr shows it as a failure.
It's safe to run again and again. A second run on a machine that's already right changes nothing.
Packaging it
$SiteCode = 'ABC'
$SourceShare = '\\sccm01\Sources\Scripts'
$DPGroup = 'All DPs'
$Name = 'LAPS - Local Admin Account'
$source = Join-Path $SourceShare 'Initialize-LapsAdminAccount'
New-Item -ItemType Directory -Path $source -Force | Out-Null
Copy-Item .\Initialize-LapsAdminAccount.ps1 -Destination $source
Import-Module (Join-Path $env:SMS_ADMIN_UI_PATH '..\ConfigurationManager.psd1')
Push-Location "$($SiteCode):\"
New-CMPackage -Name $Name -Path $source | Out-Null
New-CMProgram -PackageName $Name -StandardProgramName 'Create account' -CommandLine 'powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\Initialize-LapsAdminAccount.ps1 -Name lapsadmin' -ProgramRunType WhetherOrNotUserIsLoggedOn -RunMode RunWithAdministrativeRights -RunType Hidden | Out-Null
Start-CMContentDistribution -PackageName $Name -DistributionPointGroupName $DPGroup
Pop-Location
Add a second program with -DisableBuiltInAdministrator on the end, and deploy that one only after LAPS is confirmed working.
Take it further
- Use a configuration baseline instead. Wrap the script as a ConfigMgr compliance setting with remediation, and it'll quietly put the account back if someone deletes it.
- Force the first rotation. Follow it with Invoke-LapsPasswordRotation.ps1 so LAPS takes over the new account straight away instead of on its next cycle.
- Pick a boring name.
lapsadminis fine for a blog post. In real life, something that doesn't shout "admin" makes an attacker's password-spraying list a little less accurate.
Things that'll trip you up
- Match the name in the LAPS policy exactly. LAPS only manages the account whose name matches AdministratorAccountName. A typo means the account sits there with a random password nobody knows, and LAPS keeps managing the built-in one. Check with Invoke-LapsPasswordRotation.ps1 -WhatIf, or check the Account field from Get-LapsADPassword (or Local administrator password recovery in the Entra admin center).
- Get LAPS working before you disable the built-in account. If you flip -DisableBuiltInAdministrator on a machine where LAPS isn't managing the new account yet, you've got no local admin you can actually sign in with. Deploy the account first, confirm passwords are showing up in AD or Entra ID, then run it again with the switch.
- Other policies can undo the group membership. Restricted Groups in Group Policy, or an Intune "Local users and groups" policy set to replace members, will pull your account out of Administrators on the next refresh. Add the account to those policies instead of fighting them.
- Get-LocalGroupMember chokes on orphaned SIDs. If the Administrators group contains a SID for a deleted domain account, Get-LocalGroupMember throws instead of listing members. That's why the script just tries Add-LocalGroupMember and treats "already a member" as success.
- Create a Local Administrator Account with PowerShellScript Library