Simon Vedder

Cloud Engineer · Zürich

Scheduled automation · Bicep or Terraform

AzureVMCredentialRotation

Rotate local admin passwords and SSH keys on the Azure VMs that Windows LAPS cannot reach. Stored in Key Vault, renewed on a schedule, and optionally replaced within hours of somebody reading one.

licenseMITPowerShell7.2+galleryv0.4.1deployBicep or Terraformruns inAzure Automation

pwsh · one deployment, then it runs itself
# Everything: automation account, runbook, schedule, roles, audit trail.
az deployment sub create -l switzerlandnorth -f deploy/main.bicep -p keyVaultName=kv-credentials keyVaultResourceGroupName=rg-vault
# Opt a machine in. Nothing is touched until you do.
az vm update --ids $VM --set tags.CredentialRotation=enabled
[Info] 3 credential(s) within 14 days of expiry
[Info] jump-01    LocalAdmin  rotated, expires 2026-12-07
[Info] dmz-web-02  SSHKey      rotated, expires 2026-12-07
Rotated 3, skipped 1 (powered off, retried next run)

Quick start

Something leaked. Rotate it now.

The schedule is for the ninety-nine days nothing happens. On the hundredth you run the same module yourself, against the machines you name, from your own workstation. No deployment involved, and it works whether or not the orchestrator is running.

Needs Key Vault Secrets Officer on the vault and Virtual Machine Contributor on the machines · Prerequisites

  1. Install the module and sign in

    Install-Module AzureVMCredentialRotation
    Connect-AzAccount
  2. One machine, right now

    No expiry check. You named it, that is the reason.

    Invoke-CredentialRotation -VaultName kv-credentials -VMName jump-01
  3. A blast radius rather than a machine

    Everything in a resource group, with the incident number in the audit record. -TriggeredBy is free text, so the timeline writes itself.

    $vms = Get-AzVM -ResourceGroupName rg-dmz
    Invoke-CredentialRotation -VaultName kv-credentials -VM $vms -TriggeredBy 'INC-4471'
  4. Hold a machine you are still investigating

    Tagged machines are skipped by the orchestrator until you take the tag off.

    az vm update --ids $VM --set tags.CredentialRotationHold=true

For the other ninety-nine days, deploy the orchestrator

What it does

What it does

Reaches what LAPS cannot

Linux, workgroup Windows, and machines with no line of sight to a domain controller. Passwords and SSH keys both.

The expiry date is the trigger

No queue and no event pipeline. A scheduled pass asks which credentials are near expiry and acts on the answer.

Rotation after use

The vault's audit log says who read a secret; reading one moves its expiry forward, so the next pass replaces it.

Vault first

A failure in between leaves a credential that is in the vault and about to work, not one that works and is nowhere.

Opt-in by tag

The first run over an untagged estate does nothing at all, and dry-run is the default the deployment ships with.

An audit trail

One row per attempt in a custom table: secret versions before and after, who read what, and what keeps being skipped.

Proof

What was actually run

Every row below was executed against live Azure VMs in a lab, both from a workstation and through the deployed runbook. Nothing here is inferred from the code.

ImagePasswordSSH key
Windows Server 2022, MarketplaceVerified—
Windows Server 2022, CIS Level 1Verified—
Windows Server 2022, CIS Level 2Verified—
Ubuntu 24.04, MarketplaceVerifiedVerified
Ubuntu 24.04, CIS Level 1VerifiedVerified
Ubuntu 24.04, CIS STIGVerifiedVerified

No CIS Level 2 image is published for Ubuntu, so the STIG build stands in for it. STIG enforces a sixty-day maximum password age, so set validityDays below that or the guest and the vault disagree. Both deployment paths were run against a live tenant and rotated real credentials, and a machine in a second subscription was rotated from an automation account in the first. Throughput was measured rather than estimated: about three seconds to reconcile a machine with nothing due, about thirty-five to replace one credential, which is roughly three thousand machines or three hundred credentials inside Azure Automation's three-hour job limit.

Credential access and rotation · Azure Monitor workbook · sample data
The Credential access and rotation workbook: exposure windows with who read which secret and how long it stayed valid, rotation activity over time by trigger reason, and the machines that keep being skipped

FAQ

Questions people ask first

Can I try one machine before deploying anything?

Yes, and it is the right first move — the same commands as after a breach, with -WhatIf on the front. It tells you what it would replace and changes nothing. The secret is created in the vault if it is not there yet, so this is also how a machine is onboarded by hand. What you do not get without the orchestrator is the loop: no retry for a machine that was powered off, and no replacement after somebody reads a credential.

What does it need in my tenant?

An Automation Account with a system-assigned identity, an existing Key Vault with RBAC authorisation, and a Log Analytics workspace for the audit trail. Name resource groups rather than letting the role fall back to the whole subscription — Virtual Machine Contributor includes installing extensions, which is code execution as SYSTEM or root on every VM in scope.

Can it lock me out?

The order is designed against it: the new credential is written to the vault before it is applied to the machine, so a failure in between leaves a credential that is in the vault and about to work. There is also a hold tag that stops rotation for one machine without untagging it.

Does a rotation change anything besides the credential?

On Windows, no: security policy, audit policy, group membership and the RDP setting were identical before and after a rotation on a CIS Level 2 image. On Linux, VMAccess writes /etc/sudoers.d/waagent granting passwordless sudo on every rotation. Stock images already grant that, so nothing changes — but on a machine where somebody removed it deliberately, it comes back, and there is no switch for it.

How many machines can one schedule handle?

Two thousand machines on ninety-day validity come due at around twenty-two a day, so steady state is not the problem. The pass that does not fit is the first one over a large estate, when every credential is missing at once — onboard in batches and let the schedule catch up. The measured figures are above.

How do I reach machines in another subscription?

Name resource groups by resource ID or whole subscriptions by ID, and the deployment assigns the role where the machines are while telling the runbook which subscriptions to walk. The identity still has to reach them; naming one it cannot see produces a permission error rather than an empty result.

Bicep or Terraform?

Either, and both have been deployed against a live tenant and rotated real credentials. Bicep is the default and the one the deploy button uses; it imports the module from the PowerShell Gallery. Terraform publishes a flattened runbook and needs no Gallery access.

Rotating credentials is the easy half

The hard half is knowing which machines still hold one, who reads them, and what breaks when they change. I run that review as the first step and stay for the deployment.

Get in touch