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

SCRIPT LIBRARY · POWERSHELL

Create a Local Administrator Account with PowerShell

A one-off local admin account done properly, with a hidden password prompt, a language-proof group lookup, and an expiry date for the temporary ones.

AT A GLANCENew-LocalAdminAccount.ps1
What it does
Creates a local account, adds it to the local Administrators group by SID, and returns the new account. Prompts for the password twice if you don't pass one, and can set the account to expire on a date.
Requires
  • Windows PowerShell 5.1 (Microsoft.PowerShell.LocalAccounts module, built in)
Permissions
An elevated PowerShell session on the machine
Runs on
Windows 10/11, Windows Server 2016+
Tested
Parse-checked and dry-run with mocked LocalAccounts cmdlets in PowerShell 7.4

Part 1 of the thread Local admin, done right

Every so often you need a local admin account on one specific machine. A lab VM that isn't domain-joined. A standalone server in a rack somewhere. A vendor who needs to install their thing on Thursday and should not still have access on Friday.

The original version of this post was a four-line snippet: prompt for a password, New-LocalUser, Add-LocalGroupMember -Group "Administrators". It works on an English install, most of the time. This version checks you're elevated, confirms the password by asking twice, finds the Administrators group by SID so it works on a French or German install, won't clobber an existing account, and can set an expiry date so temporary accounts clean themselves up.

One honest caveat: this is for one-offs. If you're thinking about running it on every machine you manage, stop and use Windows LAPS instead. A shared local admin password across a fleet is the thing attackers hope to find, and LAPS gives every machine its own password, rotates it, and keeps it in AD or Entra ID. There's a fleet-friendly way to set up a LAPS-managed account in this post.

New-LocalAdminAccount.ps1Download
<#
.SYNOPSIS
    Creates a local administrator account on this computer, with the password typed in securely and an optional expiry date.
.DESCRIPTION
    For the one-off cases: a lab box, a standalone server, a temporary account for a vendor. Creates the
    account, adds it to the local Administrators group by SID (so it works on non-English Windows), and
    returns the new account. If you don't pass -Password, it prompts twice and makes sure both match.
    The password is never written to disk or shown. For fleet-wide admin accounts, use Windows LAPS.
.PARAMETER Name
    The new account name. 20 characters max.
.PARAMETER Password
    The password as a SecureString. Leave it off to be prompted.
.PARAMETER FullName
    Display name for the account.
.PARAMETER Description
    Description shown on the account.
.PARAMETER AccountExpires
    Date the account stops working. Leave it off for an account that never expires.
.EXAMPLE
    .\New-LocalAdminAccount.ps1 -Name labadmin
.EXAMPLE
    .\New-LocalAdminAccount.ps1 -Name vendor-tmp -FullName 'Vendor support' -AccountExpires (Get-Date).AddDays(3)
#>
[CmdletBinding(SupportsShouldProcess)]
param(
    [Parameter(Mandatory)]
    [ValidateLength(1, 20)]
    [ValidatePattern('^[^\\/\[\]:;|=,+*?<>@"]+$')]
    [string]$Name,
    [securestring]$Password,
    [string]$FullName = '',
    [ValidateLength(0, 48)][string]$Description = 'Local administrator',
    [datetime]$AccountExpires
)

function Test-IsElevated {
    $principal = [Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()
    $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
}

function Compare-SecureString {
    param([securestring]$First, [securestring]$Second)
    $a = [System.Net.NetworkCredential]::new('', $First).Password
    $b = [System.Net.NetworkCredential]::new('', $Second).Password
    try { $a -ceq $b } finally { $a = $null; $b = $null }
}

if (-not (Test-IsElevated)) { throw 'Run this from an elevated PowerShell session.' }
if (Get-LocalUser -Name $Name -ErrorAction SilentlyContinue) { throw "A local account named '$Name' already exists." }
if ($PSBoundParameters.ContainsKey('AccountExpires') -and $AccountExpires -le (Get-Date)) { throw 'AccountExpires is in the past.' }

if (-not $Password) {
    $Password = Read-Host -AsSecureString -Prompt "Password for $Name"
    $confirm  = Read-Host -AsSecureString -Prompt 'Type it again'
    if (-not (Compare-SecureString -First $Password -Second $confirm)) { throw "The passwords didn't match. Nothing was created." }
}
if ($Password.Length -lt 14) { Write-Warning 'That password is under 14 characters. Fine for a lab, not for anything that matters.' }

$userArgs = @{
    Name        = $Name
    Password    = $Password
    Description = $Description
    ErrorAction = 'Stop'
}
if ($FullName) { $userArgs.FullName = $FullName }
if ($PSBoundParameters.ContainsKey('AccountExpires')) { $userArgs.AccountExpires = $AccountExpires } else { $userArgs.AccountNeverExpires = $true }

if (-not $PSCmdlet.ShouldProcess($Name, 'Create local account and add it to Administrators')) { return }

$user = New-LocalUser @userArgs
Write-Verbose "Created $Name ($($user.SID))"

try {
    Add-LocalGroupMember -SID 'S-1-5-32-544' -Member $user -ErrorAction Stop
}
catch {
    Write-Warning "Created $Name but couldn't add it to Administrators: $($_.Exception.Message)"
}

Get-LocalUser -Name $Name | Select-Object Name, FullName, Enabled, AccountExpires, PasswordLastSet, SID

Parameters

ParameterTypeDefaultWhat it's for
-Namestring—Required. The new account name, up to 20 characters.
-Passwordsecurestring—The password. Leave it off and you'll be prompted twice, with the typing hidden.
-FullNamestring—Display name, e.g. 'Vendor support'.
-DescriptionstringLocal administratorDescription shown on the account, 48 characters max.
-AccountExpiresdatetime—When the account stops working. Leave it off for an account that never expires.
-WhatIfswitch—Runs the checks and shows what it would create.

Run it

A lab admin account, prompting for the password.

.\New-LocalAdminAccount.ps1 -Name labadmin

A temporary account for a vendor that expires in three days.

.\New-LocalAdminAccount.ps1 -Name vendor-tmp -FullName 'Vendor support' -AccountExpires (Get-Date).AddDays(3)

On a remote machine, with the password passed through as a SecureString.

$pw = Read-Host -AsSecureString 'Password'; Invoke-Command -ComputerName SRV-LAB01 -FilePath .\New-LocalAdminAccount.ps1 -ArgumentList 'labadmin', $pw

What you'll see

Example outputvalues are illustrative
Name            : vendor-tmp
FullName        : Vendor support
Enabled         : True
AccountExpires  : 10/2/2026 5:00:00 PM
PasswordLastSet : 9/29/2026 5:00:12 PM
SID             : S-1-5-21-1004336348-1177238915-682003330-1003

How it works

  1. Check the basics before touching anything. Is the session elevated? Does an account with that name already exist? Is the expiry date in the future? Any "no" stops the script with a clear message.
  2. Get the password safely. If you didn't pass -Password, it prompts twice with Read-Host -AsSecureString and compares the two. If they don't match, nothing is created. Short passwords get a warning.
  3. Create the account. New-LocalUser with either -AccountExpires or -AccountNeverExpires, depending on what you asked for.
  4. Add it to Administrators by SID. If that step fails, you get a warning rather than a half-silent success, and the account is left in place so you can fix the membership by hand.
  5. Return the account, so you can see what was created and pipe it somewhere if you want.

Take it further

  • Several machines at once. Invoke-Command -ComputerName with -FilePath and the SecureString in -ArgumentList works fine. PowerShell remoting carries SecureStrings over encrypted.
  • Clean up expired accounts. Get-LocalUser | Where-Object { $_.AccountExpires -and $_.AccountExpires -lt (Get-Date) } finds the leftovers.
  • Going fleet-wide? Use Initialize-LapsAdminAccount.ps1 and Windows LAPS instead of a password you have to remember.

Things that'll trip you up

  • Don't use this for the whole fleet. The moment the same admin password is on more than a handful of machines, one compromised box is all of them. Windows LAPS is free, built into current Windows, and solves exactly this.
  • "Administrators" isn't always called Administrators. On non-English Windows it's Administrateurs, Administratoren, and so on, and scripts that use the name just fail. The SID S-1-5-32-544 is the same everywhere, so that's what the script uses.
  • PowerShell 7 and the LocalAccounts cmdlets. On some PowerShell 7 builds the LocalAccounts cmdlets throw an odd TelemetryAPI type error. Run the script from Windows PowerShell 5.1, or run Import-Module Microsoft.PowerShell.LocalAccounts -UseWindowsPowerShell first.
  • Expiry stops new sign-ins, not existing sessions. An expired account can't sign in again, but a session that's already open keeps going. For a vendor account, sign them out or reboot when the job's done. And delete the account afterwards; expired accounts pile up.