Hands-Off Admin Password Rotation for MSPs: PS

Reading Time: 14 minutes
🔐 The Never-Ending Password Talk
After working previously at an MSP that had automated password rotation for computers and servers at our ready, it made the process of remote support so much faster and less annoying, I knew I wanted to replicate it. No Excel sheets to open, no password managers and MFA codes to get through. They had a separate application for it. When I moved to a new employer without it I figured out how to do it with PowerShell.
The passwords are generated in memory and not logged on the Windows machines, instead the RMM agent monitors the script output in memory and records it to the corresponding endpoint’s RMM attribute value of the particular RMM solution. In these scripts provided below, they are currently set for Action1, but commented out notes at the bottom explain how to implement with ConnectWise Automate or Datto RMM.
💭 Key Considerations Before Scripting
Before diving into automation, here are essential things to keep in mind when designing password rotation scripts:
- Passwords should be rotated on a regular schedule.
- Passwords must be complex and randomly generated.
- You should always have access to the updated password.
- Passwords should never be stored on the endpoint where they can be retrieved by attackers.
🛠️ Safe for RMM Integration
I spent time testing scripts previously for Datto RMM, which can easily schedule jobs and execute PowerShell scripts across endpoints. When the job runs, the script is temporarily copied to the endpoint and executed from a path like:
C:\ProgramData\CentraStage\Packages\d0b49e6e-9eb6-48bf-8e28-99271b329f93#\command.ps1The GUID in the path is unique for each execution. The generated password is not stored on the endpoint. After the script runs, the output (including the new password) is accessible via the “StdOut” link in Datto RMM, allowing you to copy and paste it into the target system using tools like Splashtop or Web Remote.
While the password is briefly available in memory, it’s never saved to disk—providing a decent security trade-off. If malware is already present and capable of reading memory, you’re compromised regardless.
🔄 Administrator Password Rotation Script
Here is a script to rotate the Administrator password for either a Domain or Local account named "Administrator". On a domain controller or Windows client endpoint, the built-in Administrator account already exists and should never be deleted, so this script just aims to rotate its password.
<#
.DESCRIPTION
This script will randomize a 16-character password that will consist of Upper-case, Lower-case, Numerals, and Symbols and apply it to the domain or local user account in Windows named Administrator.
.NOTES
SETTINGS:
Username : Administrator
PasswordLength : 16
CharacterSet : Upper-case, Lower-case, Numerals, and Symbols
RMM Target : Action1 Custom Attribute "Administrator Password"
Script Name : _Set RMM Value - Password Rotation - Administrator - v2.7
Version : 2.7
Author : WinReflection (www.winreflection.com), in collaboration with ChatGPT.
RMMs : Action1 (Custom Attributes)
Requirements:
- Administrator or SYSTEM privileges.
- Local admin rights.
- On domain controllers: ActiveDirectory module available.
- Action1 agent with Action1-Set-CustomAttribute cmdlet.
- PowerShell 5.1 or Windows Management Framework 5.1 on older operating systems such as Windows Server 2012/2012 R2 and Windows 8/8.1 to provide the required cmdlets (for example, Get-LocalUser and Set-LocalUser).
CHANGELOG:
v2.7 - Added a runtime check to verify that PowerShell 5.1 or newer is available, and exit with a clear message if the requirement is not met. Simplified logic to rotate the existing "Administrator" account without attempting to create it if missing.
v2.6 - Documented the requirement for PowerShell 5.1 / Windows Management Framework 5.1 on older systems such as Windows Server 2012/2012 R2 and Windows 8/8.1.
v2.5 - Added explicit detection of the local "Administrator" account and safer handling when it already exists.
v2.0 - Standardized formatting, added header, SETTINGS, and RMM notes. Logic unchanged.
v1.x - Initial implementation for local and domain "Administrator" account rotation and Action1 attribute update.
#>
# =====================================================================
# REGION: Config.
# =====================================================================
$ErrorActionPreference = 'Stop'
$Username = 'Administrator' # Account name to rotate.
# =====================================================================
# REGION: PowerShell Version Check.
# =====================================================================
# Verify that the current PowerShell version is 5.1 or newer.
$psMajor = $PSVersionTable.PSVersion.Major
$psMinor = $PSVersionTable.PSVersion.Minor
if (
($psMajor -lt 5) -or
($psMajor -eq 5 -and $psMinor -lt 1)
) {
Write-Output "PowerShell 5.1 or newer is required for this script. Current version: $($PSVersionTable.PSVersion)."
return
}
# =====================================================================
# REGION: Functions.
# =====================================================================
function New-RandomPassword {
<#
.DESCRIPTION
Generates a random password.
.PARAMETER Length
Length of the generated password.
#>
param(
[int]$Length = 16
)
# Use a single flat character set to keep it simple and avoid infinite loops.
$chars = 'ABCDEFGHJKMNOPQRSTUVWXYZabcdefghjkmnopqrstuvwxyz0123456789!#$%&*+=?@'.ToCharArray()
$max = $chars.Length
-join (1..$Length | ForEach-Object {
$chars[(Get-Random -Min 0 -Max $max)]
})
}
# =====================================================================
# REGION: Environment Detection.
# =====================================================================
# Detect whether this is a domain controller (ProductType = 2).
$IsDomainController = $false
try {
$os = Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction Stop
if ($os.ProductType -eq 2) {
$IsDomainController = $true
}
}
catch {
# If detection fails, assume a non-domain controller rather than hanging.
$IsDomainController = $false
}
# =====================================================================
# REGION: Main Logic.
# =====================================================================
# Generate the new password and convert it to a secure string.
$Passwd = New-RandomPassword -Length 16
$PasswdSecStr = ConvertTo-SecureString $Passwd -AsPlainText -Force
if (-not $IsDomainController) {
# --------------------------------------------------------------
# Non-domain controller: Use the local "Administrator" account.
# --------------------------------------------------------------
# Try to detect whether the local "Administrator" account already exists.
$LocalAdministrator = $null
try {
$LocalAdministrator = Get-LocalUser -Name $Username -ErrorAction Stop
}
catch [Microsoft.PowerShell.Commands.UserNotFoundException] {
# The local "Administrator" account does not exist.
Write-Output "The local 'Administrator' account was not found. No changes were made."
return
}
catch {
# If Get-LocalUser fails for another reason, report the error and exit.
Write-Output "Failed to query the local 'Administrator' account. Error: $($_.Exception.Message)"
return
}
if ($null -ne $LocalAdministrator) {
# The local "Administrator" account exists, so rotate the password.
try {
# On newer systems, set the password and mark it as never expiring.
Set-LocalUser -Name $Username -Password $PasswdSecStr -PasswordNeverExpires $true
}
catch {
# On older systems without PasswordNeverExpires support, only set the password.
Set-LocalUser -Name $Username -Password $PasswdSecStr
}
}
# --------------------------------------------------------------
# RMM Integration: Action1 custom attribute.
# --------------------------------------------------------------
Action1-Set-CustomAttribute "Administrator Password" "$Passwd"
# ConnectWise Automate: Write-Output $Passwd.
# Datto RMM: Add UDF application code here to store $Passwd (not included in this script).
}
else {
# --------------------------------------------------------------
# Domain controller: Use the domain "Administrator" account.
# --------------------------------------------------------------
# Import the ActiveDirectory module if it is not already loaded.
if (-not (Get-Module ActiveDirectory -ErrorAction SilentlyContinue)) {
Import-Module ActiveDirectory -ErrorAction Stop -Verbose:$false | Out-Null
}
try {
# Verify that the domain "Administrator" account exists.
$null = Get-ADUser -Identity $Username -ErrorAction Stop
}
catch [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException] {
# The domain "Administrator" account does not exist.
Write-Output "The domain 'Administrator' account was not found. No changes were made."
return
}
catch {
# If Get-ADUser fails for another reason, report the error and exit.
Write-Output "Failed to query the domain 'Administrator' account. Error: $($_.Exception.Message)"
return
}
try {
# Rotate the password on the existing domain user account.
Set-ADAccountPassword -Identity $Username -Reset -NewPassword $PasswdSecStr
Set-ADUser -Identity $Username -PasswordNeverExpires $true
}
catch {
Write-Output "Failed to rotate the password for the domain 'Administrator' account. Error: $($_.Exception.Message)"
return
}
# --------------------------------------------------------------
# RMM Integration: Action1 custom attribute.
# --------------------------------------------------------------
Action1-Set-CustomAttribute "Administrator Password" "$Passwd"
# ConnectWise Automate: Write-Output $Passwd.
# Datto RMM: Add UDF application code here to store $Passwd (not included in this script).
}🔄 Support Password Rotation Script
This script is different in that it aims to provide password rotation for either a Domain or Local account named "Support". However, it also works as an onboarding script for new Windows endpoints to create the Support account on either a Windows client, DC, or member server so that you can get access to Admin rights fast.
<#
.DESCRIPTION
This script will randomize a 16-character password that will consist of Upper-case, Lower-case, Numerals, and Symbols and apply it to the domain or local user account in Windows named Support.
.NOTES
SETTINGS:
Username : Support
PasswordLength : 16
CharacterSet : Upper-case, Lower-case, Numerals, and Symbols
RMM Target : Action1 Custom Attribute "Support Password"
Script Name : _Set RMM Value - Password Rotation - Support - v2.7
Version : 2.7
Author : WinReflection (www.winreflection.com), in collaboration with ChatGPT.
RMMs : Action1 (Custom Attributes)
Requirements:
- Administrator or SYSTEM privileges.
- Local admin rights.
- On domain controllers: ActiveDirectory module available.
- Action1 agent with Action1-Set-CustomAttribute cmdlet.
- PowerShell 5.1 or Windows Management Framework 5.1 on older operating systems such as Windows Server 2012/2012 R2 and Windows 8/8.1 to provide the required cmdlets (for example, Get-LocalUser and Set-LocalUser).
CHANGELOG:
v2.7 - Added a runtime check to verify that PowerShell 5.1 or newer is available, and exit with a clear message if the requirement is not met.
v2.6 - Documented the requirement for PowerShell 5.1 / Windows Management Framework 5.1 on older systems such as Windows Server 2012/2012 R2 and Windows 8/8.1.
v2.5 - Added explicit detection of the local "Support" account and safer handling when it already exists, including a fallback path for UserExistsException during creation.
v2.0 - Standardized formatting, added header, SETTINGS, and RMM notes. Logic unchanged.
v1.x - Initial implementation for local and domain "Support" account rotation and Action1 attribute update.
#>
# =====================================================================
# REGION: Config.
# =====================================================================
$ErrorActionPreference = 'Stop'
$Username = 'Support' # Account name to rotate.
# =====================================================================
# REGION: PowerShell Version Check.
# =====================================================================
# Verify that the current PowerShell version is 5.1 or newer.
$psMajor = $PSVersionTable.PSVersion.Major
$psMinor = $PSVersionTable.PSVersion.Minor
if (
($psMajor -lt 5) -or
($psMajor -eq 5 -and $psMinor -lt 1)
) {
Write-Output "PowerShell 5.1 or newer is required for this script. Current version: $($PSVersionTable.PSVersion)."
return
}
# =====================================================================
# REGION: Functions.
# =====================================================================
function New-RandomPassword {
<#
.DESCRIPTION
Generates a random password.
.PARAMETER Length
Length of the generated password.
#>
param(
[int]$Length = 16
)
# Use a single flat character set to keep it simple and avoid infinite loops.
$chars = 'ABCDEFGHJKMNOPQRSTUVWXYZabcdefghjkmnopqrstuvwxyz0123456789!#$%&*+=?@'.ToCharArray()
$max = $chars.Length
-join (1..$Length | ForEach-Object {
$chars[(Get-Random -Min 0 -Max $max)]
})
}
# =====================================================================
# REGION: Environment Detection.
# =====================================================================
# Detect whether this is a domain controller (ProductType = 2).
$IsDomainController = $false
try {
$os = Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction Stop
if ($os.ProductType -eq 2) {
$IsDomainController = $true
}
}
catch {
# If detection fails, assume a non-domain controller rather than hanging.
$IsDomainController = $false
}
# =====================================================================
# REGION: Main Logic.
# =====================================================================
# Generate the new password and convert it to a secure string.
$Passwd = New-RandomPassword -Length 16
$PasswdSecStr = ConvertTo-SecureString $Passwd -AsPlainText -Force
if (-not $IsDomainController) {
# --------------------------------------------------------------
# Non-domain controller: Use the local "Support" account.
# --------------------------------------------------------------
# Try to detect whether the local "Support" account already exists.
$LocalSupport = $null
try {
$LocalSupport = Get-LocalUser -Name $Username -ErrorAction Stop
}
catch [Microsoft.PowerShell.Commands.UserNotFoundException] {
# The local "Support" account does not exist.
$LocalSupport = $null
}
catch {
# If Get-LocalUser fails for another reason, leave $LocalSupport as $null and rely on New-LocalUser.
$LocalSupport = $null
}
if ($null -ne $LocalSupport) {
# The local "Support" account exists, so rotate the password.
try {
# On newer systems, set the password and mark it as never expiring.
Set-LocalUser -Name $Username -Password $PasswdSecStr -PasswordNeverExpires $true
}
catch {
# On older systems without PasswordNeverExpires support, only set the password.
Set-LocalUser -Name $Username -Password $PasswdSecStr
}
}
else {
# The local "Support" account does not exist, so attempt to create it.
try {
# Create the local admin account.
New-LocalUser -Name $Username -Password $PasswdSecStr -FullName $Username | Out-Null
# Attempt to set the password to never expire if the platform supports it.
try {
Set-LocalUser -Name $Username -PasswordNeverExpires $true
}
catch {
# Ignore this step if PasswordNeverExpires is not supported.
}
# Add the account to the local Administrators group.
Add-LocalGroupMember -Group 'Administrators' -Member $Username | Out-Null
}
catch [Microsoft.PowerShell.Commands.UserExistsException] {
# If the user already exists at creation time, fall back to rotating the password.
try {
# On newer systems, set the password and mark it as never expiring.
Set-LocalUser -Name $Username -Password $PasswdSecStr -PasswordNeverExpires $true
}
catch {
# On older systems without PasswordNeverExpires support, only set the password.
Set-LocalUser -Name $Username -Password $PasswdSecStr
}
}
}
# --------------------------------------------------------------
# RMM Integration: Action1 custom attribute.
# --------------------------------------------------------------
Action1-Set-CustomAttribute "Support Password" "$Passwd"
# ConnectWise Automate: Write-Output $Passwd.
# Datto RMM: Add UDF application code here to store $Passwd (not included in this script).
}
else {
# --------------------------------------------------------------
# Domain controller: Use the domain "Support" account.
# --------------------------------------------------------------
# Import the ActiveDirectory module if it is not already loaded.
if (-not (Get-Module ActiveDirectory -ErrorAction SilentlyContinue)) {
Import-Module ActiveDirectory -ErrorAction Stop -Verbose:$false | Out-Null
}
try {
# Rotate the password on the existing domain user account.
$null = Get-ADUser -Identity $Username -ErrorAction Stop
Set-ADAccountPassword -Identity $Username -Reset -NewPassword $PasswdSecStr
Set-ADUser -Identity $Username -PasswordNeverExpires $true
}
catch [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException] {
# Create the domain user if it does not already exist.
New-ADUser -Name $Username `
-SamAccountName $Username `
-AccountPassword $PasswdSecStr `
-Enabled $true `
-PasswordNeverExpires $true | Out-Null
# Add the account to the desired group, such as Administrators or Domain Admins.
Add-ADGroupMember -Identity 'Administrators' -Members $Username -ErrorAction Stop
}
# --------------------------------------------------------------
# RMM Integration: Action1 custom attribute.
# --------------------------------------------------------------
Action1-Set-CustomAttribute "Support Password" "$Passwd"
# ConnectWise Automate: Write-Output $Passwd.
# Datto RMM: Add UDF application code here to store $Passwd (not included in this script).
}These scripts have been tested but perhaps I missed something. In any case, they’re provided for you to paste into ChatGPT or other AIs if you wish to improve upon them, but this should be a great starting point.
✅ Conclusion
This approach allows you to automate password changes without manual intervention. If someone leaves your MSP or gains unauthorized access, the password will rotate on the next schedule. You can even set it to rotate hourly for extra peace of mind.
🌿 Final Thoughts
Have ideas for improving this process or using a different tool? I’d love to hear how you’re handling password rotation in your environment.
My name is Dex, author at WinReflection.
I am a Christian, conservative, problem-solver, and truth-seeker who is not afraid to share about important or controversial issues—silence leads to death. There’s more to life than the worldly status quo, and that’s why many are sad and depressed—they’re suffocating. Truth and purpose can bring fresh air into one’s life, and that’s my mission. For those that care, here is my script/command laboratory.
📖 John 3:16: For God so loved the world that He gave His one and only Son, that whoever believes in Him shall not perish but have eternal life.


Leave a Reply
Want to join the discussion?Feel free to contribute!