Being able to change Azure VM admin password from Azure Portal easily is very convenient. This feature relies on Azure Agent and VM extension, and it allows us to reset the configuration of RDP/SSH and the local administrator password. Or even to create a new account if we don’t remember the user name. Very useful.

This also unlocks the possibility to change local administrator passwords for our Azure VMs programmatically. And that can be useful if we are not using LAPS or a similar solution. Users tend to enter the same user name and password for all VM resources in Azure. That is convenient but not safe. After changing passwords, you can save them into the Key Vault or any password manager of your choice. This is great for one-time change and changing passwords regularly based on a schedule.

This article will show you how to use Azure Automation Runbook to automatically change VM passwords and save new passwords to Azure Key Vault.

Prepare the test environment

Before we start, I will assume that you already have VMs, Automation Account, and Key Vault. If you want to build these resources quickly in a new test resource group, check out this PowerShell script that I created: Create-DemoLab.ps1.

Identify VMs for which we want to change the password

Running this script to all VMs in our tenant is not a good idea. Here are a few things to consider:

Domain Controllers will not have a local administrator. Trying to change it via VM extension will fail.

Migrated VMs will have a historical password from on-premises/another cloud. This account user name will not be filled up in the OS Profile of Azure VM. But if you know the user name, you can still change it via the Azure VM extension. Keep that in mind since this example is not working with that possibility.

Changing passwords for VMs that are off will not work. The demo script will skip VMs that are not running, but you can easily change that to bring them on before changing the password.

If you have any automation or services that depend on using a local administrator password, you will need to point them to the Key Vault

VMs using different authentication methods or hardened VMs are also not good candidates

Now that we know which VMs we want to exclude, we can enable this for VMs based on a specific Azure Tag, Resource Group, or group membership. We can also pre-fill VM names in Key Vault as Secret names and let the script change only for VMs where KeyVault Secret names match VM names.

We are making it simple for this demo, and I will do it for all VMs with the tag VMPasswordReset = Yes.

You can use this PowerShell to assign this tag to your VMs:

$VMs = @(VM1, VM2, VM3, VM4)
$Tag = @{VMPasswordReset = Yes}
foreach ($VMName in $VMs) {
$VM = Get-AzResource ResourceType Microsoft.Compute/VirtualMachines Name $VMName
Update-AzTag ResourceId $VM.ResourceId Tag $Tag Operation Merge
}

Automate password rotation with PowerShell and Key Vault

Our script will be simple, and it can run from anywhere. It’s up to you if you want to use it from within your client PC or Windows or Linux Server, or from Automation Account, Azure Function, Azure DevOps, or if you want to create a GitHub workflow for it.

I will use the Azure automation account for this demo since that is free and the most straightforward to set up. If you need help setting this up in any other environment, let me know on Twitter, and I’ll do my best to help you.

Configure Automation Account

We need Automation Account with System-Assigned Managed Identity that has access to VMs (VM Contributor Role) and Key Vault (Secrets: Set).

Create PowerShell Runbook

Create a new PowerShell runbook and copy the following code. It is necessary to change Automation Account Name, Subscription Name, and Tag name variables.

Write-Output Connecting …
# Ensures you do not inherit an AzContext in your runbook
Disable-AzContextAutosave Scope Process
# Connect to Azure with system-assigned managed identity
$AzureContext = (Connect-AzAccount Identity).context
# Set and store context
$AzureContext = Set-AzContext SubscriptionName $AzureContext.Subscription DefaultProfile $AzureContext
#Define variables:
Write-Output Defining variables …
#Fill in the name of your Key Vault:
$KeyVaultName = KV-Test
#Fill in the name of your Subscription where Key Vault is:
$KeyVaultSubscriptionName = SUB-Test
$SecretsHolder = @()
Write-Output Getting the list of all subscriptions….
$Subscriptions = Get-AzSubscription | Where-Object Name -notlike Access to Azure Active Directory
Write-Output Start Subscription loop…
foreach ($Subscription in $Subscriptions) {
$VMs = @()
Write-Output Selecting Subscription $($Subscription.Name)
Get-AzSubscription SubscriptionName $Subscription.Name | Set-AZContext
$VMs += Get-AzResource ResourceType Microsoft.Compute/VirtualMachines TagName VMPasswordReset TagValue Yes
IF (!$VMs) {
Write-Output No VMs with enabled password reset in this subscription.
} else {
Write-Output VMs found. Staring the VM loop…
}
foreach ($VM in $VMs) {
Write-Output Processing VM $($VM.Name)
$VMStatus = Get-AzVM Name $VM.Name ResourceGroupName $VM.ResourceGroupName Status
if ($(($VMStatus.Statuses | Where-Object Code -like *PowerState*).DisplayStatus) -eq VM running) {
#Get the VM info
$VMInfo = Get-AzVM Name $VM.Name ResourceGroupName $VM.ResourceGroupName
#Generate a new password
$CharArray = !@#$%^&*0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz!@#$%^&*.tochararray()
$Password = ConvertTo-SecureString (($CharArray | Get-Random Count 18) -join ) AsPlainText Force
#Secret Name will be in the format of “VM Name – Admin User Name”
$KeyVaultSecretName = $VMinfo.Name + + $VMInfo.OSProfile.AdminUsername
$VMReset = @{
VMName = $VMInfo.Name
ResourceGroupName = $VMInfo.ResourceGroupName
Location = $VMInfo.Location
Credential = New-Object System.Management.Automation.PSCredential ($VMInfo.OSProfile.AdminUsername, $Password)
}
Write-Output Resetting password for VM $($VM.Name)
Set-AzVMAccessExtension @VMReset typeHandlerVersion 2.0 Name VMAccessAgent
#Add secret to object array
$secret = new-object TypeName psobject
$secret | Add-Member MemberType NoteProperty Name SecretName Value $KeyVaultSecretName
$secret | Add-Member MemberType NoteProperty Name SecretValue Value $Password
$SecretsHolder += $secret
} else {
Write-Output VM ($VM.Name) is not running. Skipping…
}
} #End of VMs in Subscription loop
} # End of Subscriptions loop
if ((Get-AzContext).Subscription.Name -ne $KeyVaultSubscriptionName) {
Write-Output Switching to Subscription where the KeyVault is…
Get-AzSubscription SubscriptionName $KeyVaultSubscriptionName | Set-AZContext
}
#Save password to KeyVault
if ($SecretsHolder) {
Write-Output Saving passwords to KeyVault…
foreach ($s in $SecretsHolder) {
Write-Output Saving secret $($s.SecretName)
Set-AzKeyVaultSecret VaultName $KeyVaultName SecretName $s.SecretName SecretValue $s.SecretValue
}
}
Write-Output Runbook completed.

Runbook will switch between subscriptions in your tenant and find all VMs with specified Tag and status “VM running”.

Passwords will then be saved to Key Vault, where the secret name will be in format “VMName-LocalAdminUserName” :

You can see the history of all passwords:

And the actual password:

One last step is to link this Runbook to Schedule:

What to consider before running this in production

This Key Vault will contain sensitive information. It’s a good idea to check RBAC roles and Access Policies to limit its access. You can quickly adapt this method and store passwords in your other favorite password manager.

Using this workflow based on Tags as shown in this demo can be risky. Tags can be easily lost or replaced. Targeting VMs based on Group membership or KeyVault Secrets will provide more control on who can add/remove VMs from the list.

Automation Account is pretty good at keeping logs about previous runs. I would still consider implementing logging and alerting into your PowerShell script.

Read the full article here: https://azureis.fun/posts/Rotate-Azure-VM-admin-password-with-PowerShell-and-KeyVault/