AzureVMCredentialRotation
One deployment, then it runs itself
Subscription scope, dry-run by default. The same deployment configures the runbook through eighteen CR_ automation variables, which is also what the Terraform path sets — a test compares the two sets on every push.
az deployment sub create \
--location switzerlandnorth \
--template-file deploy/main.bicep \
--parameters keyVaultName=kv-credentials \
keyVaultResourceGroupName=rg-vault \
targetResourceGroupNames='["rg-workloads"]'
# Then opt machines in, one tag at a time.
az vm update --ids $VM --set tags.CredentialRotation=enabledOne deployment creates
- An Automation Account with a system-assigned identity and the runbook published
- A schedule, and the role assignments it needs: Secrets Officer on the vault, Virtual Machine Contributor on the scopes you name
- A Log Analytics workspace, a custom table, and the workbook below: who read which credential, how long it stayed valid, and what is stuck
- Nothing on your VMs. Scope is opt-in by tag, so the first run over an untagged estate does nothing at all
Feature flags rather than stacked modules: deployObservability for the audit trail (on), enableRotateOnAccess for rotation after use (off), dryRun for whether anything is actually replaced (on, deliberately). Name resource groups or whole subscriptions anywhere in the tenant and the roles are assigned where the machines are.
Under the hood
The expiry date is the only signal
That one idea removes the queue, the orchestrator and the second code path a rotation system usually grows. A scheduled run asks two questions and acts on the answers.
Which credentials are near expiry?
Every secret inside the threshold, on a machine the orchestrator picked by tag. Opt-in on purpose: a loop that read "no secret exists for this VM" as "rotate it" would change the local administrator password of every machine it can see on its first run.
Which did a human read since the last run?
The orchestrator queries the Key Vault audit log and moves that secret's expiry date forward by the grace period. The first question picks it up on the next pass. That is the whole of rotation-after-use — no queue, no second code path.
Replace it, then record it
Written to the vault first and applied to the guest second, so a failure in between leaves a credential that is in the vault and about to work rather than one that works and is nowhere. One row per attempt goes to a custom table — secret versions before and after, never credential material.