Wes Ellis./ a personal notebook
Technology. Stories. Side projects.
A few things worth writing down.
← Back to Script Library

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.

AT A GLANCEInitialize-LapsAdminAccount.ps1
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.

Initialize-LapsAdminAccount.ps1Download
<#
.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

ParameterTypeDefaultWhat it's for
-Namestring—Required. The account name, up to 20 characters. Use exactly the name your LAPS policy's AdministratorAccountName setting uses.
-DescriptionstringLocal admin managed by Windows LAPSDescription shown on the account, 48 characters max.
-DisableBuiltInAdministratorswitch—Also disable the built-in Administrator (the RID 500 account). Skipped if it's the account you named.
-WhatIfswitch—Shows each step it would take.

Run it

Create the account on this machine.

.\Initialize-LapsAdminAccount.ps1 -Name lapsadmin

Create it and disable the built-in Administrator in the same run.

.\Initialize-LapsAdminAccount.ps1 -Name lapsadmin -DisableBuiltInAdministrator

As a ConfigMgr package program.

powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\Initialize-LapsAdminAccount.ps1 -Name lapsadmin -DisableBuiltInAdministrator

Preview first.

.\Initialize-LapsAdminAccount.ps1 -Name lapsadmin -DisableBuiltInAdministrator -WhatIf

What you'll see

Example outputvalues are illustrative
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

  1. Find Administrators by SID. S-1-5-32-544 is the local Administrators group on every Windows install, whatever language it's in.
  2. 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.
  3. Add it to Administrators. If it's already there, that's reported as AlreadyMember, not an error.
  4. Optionally disable the built-in Administrator, found by the -500 at the end of its SID so a renamed one is still caught. It will never disable the account you just named.
  5. 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. lapsadmin is 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.