Powershell script to set Azure key vault key auto rotation policy
One of the pen-testing results, as well as the Azure security advisor recommendation, is to have an expiry date for the azure key vault keys. Although not all keys can be rotated at all times because they may be associated with one or the other option depending on how they are used in any Azure implementation, the CISO will always make a strong push to have these implemented with Auto-rotation enabled.
The key vault key auto-rotation policy was introduced by Azure and was in preview for quite some time before it went GA in 2021. We’ve been waiting for Hashicorp to include this feature in one of the upcoming Terraform releases ever since, and we’re still waiting.
I couldn’t find any official roadmap that documents it; the best I could find was a request on Github: https://github.com/hashicorp/terraform-provider-azurerm/issues/14471
User “aristovo,” who contributes to the Terraform Azure provider codebase, has essentially stated that he is waiting for another fix to be implemented in a pull request: https://github.com/hashicorp/terraform-provider-azurerm/issues/14471#issuecomment-1348662672 … but that PR has been closed, and they appear to be arguing about how to proceed.
I didn’t want to wait long enough and probably find other ways using powershell scripts, so I wrote a script that can help perform this for all keyvaults in an Azure Tenant. I’d like to thank everyone who explained how this is done on the internet, and the use of a JSON template to manage the policy settings is actually in the official Microsoft documentation (https://learn.microsoft.com/en-us/azure/key-vault/keys/how-to-configure-key-rotation). So without delaying let me get the script listed below:
The JSON file which is used to configure the setting of the policy which will be set is as below:
rotationpolicy.json
{
"lifetimeActions": [
{
"trigger": {
"timeAfterCreate": "P75D",
"timeBeforeExpiry": null
},
"action": {
"type": "Rotate"
}
},
{
"trigger": {
"timeBeforeExpiry": "P20D"
},
"action": {
"type": "Notify"
}
}
],
"attributes": {
"expiryTime": "P90D"
}
}
The main file which has the logic and script is as below:
Set-KeyVaultKeyRotationPolicy.ps1
function Set-KeyAutoRotationPolicy {
<#
.SYNOPSIS
Used to set KeyVault Key Auto Rotation policies
<<<<<<<<KNOWN ISSUES>>>>>>>
If Set-AzKeyVaultKeyRotationPolicy fails on line 185, it will still add the entry for the failed Key into the output file of newly created rotation policies, with an invalid Rotation_Policy_Id
To be fixed in next version.
<<<<<<<</KNOWN ISSUES>>>>>>>
.NOTES
Name: Set-KeyAutoRotationPolicy
Author: Karthik Sankaran, Max Allanson
Version: 1.0
DateCreated: 2023-Jan-25
.INPUTS
Mandatory
RotationPolicyDirectory - Directory where the rotationpolicy.json file is located, also where the output log files will be created
Optional
excludedSubscriptions - String array of subscriptions you want to exclude, defaults to none
excludedKeyvaults - String array of keyvaults you want to exclude, defaults to none
excludedKeys - String array of keys you want to exclude, defaults to none
RetrieveKeysOnly - Boolean to retrieve keys only or set rotation policy, defaults to true for safety
WhatIf - Boolean to run script in WhatIf mode, defaults to true for safety
.EXAMPLE
Use PowerShell dot sourcing to run the script, e.g.
Run this in PowerShell: ". 'C:\{ScriptLocation}\Set-KeyVaultKeyRotationPolicy.ps1'"
The dot notation will import the script into the current PowerShell window scope and make it's emebdeded functions available to be called
Then call the function with the required parameters, e.g. Set-KeyAutoRotationPolicy -RotationPolicyDirectory "C:\KeyRotation\"
.EXAMPLE
Example using all parameters
Set-KeyAutoRotationPolicy -WhatIf $true -RetrieveKeysOnly $false `
-RotationPolicyDirectory "C:\SetKeyVaultKeyRotation\" `
-excludedKeys "Name-Of-KV-Key" -excludedKeyvaults "Name-Of-KeyVault" -excludedSubscriptions "Name-Of-Subscription"
.EXAMPLE
Example using multiple values for the excludedKeys string[] array parameters
Set-KeyAutoRotationPolicy -RotationPolicyDirectory "C:\SetKeyVaultKeyRotation\" `
-excludedKeys "Name-Of-KV-Key","Name-Of-KV-Key2","Name-Of-KV-Key3"
.EXAMPLE
Example just using mandatory parameters
Set-KeyAutoRotationPolicy -RotationPolicyDirectory "C:\KeyRotation\"
#>
param (
[string[]]$excludedSubscriptions, #String array of subscriptions you want to exclude
[string[]]$excludedKeyvaults, #String array of keyvaults you want to exclude
[string[]]$excludedKeys, #String array of keys you want to exclude
[Parameter(mandatory=$true)]
[string]$RotationPolicyDirectory, #Directory of rotation polic to be applied to keys, and logs output location
[bool]$RetrieveKeysOnly = $true, #Will only retrieve key details, not set any new rotation policies
[bool]$WhatIf = $true #Runs Set-KeyRotationPolicy in WhatIf mode so you can see potential key's that would be created
)
if(-Not(Test-Path $RotationPolicyDirectory\rotationpolicy.json)){
Write-Warning "Rotation policy file not found in directory: $RotationPolicyDirectory"
exit
}
if($RetrieveKeysOnly -eq $true){
Write-Warning "Retrieveing Keys Only - Will not create new Key Policies!"
Start-Sleep -s 2
}
if($WhatIf -eq $false){
Write-Warning "You set WhatIf to false. New rotation policies will be created against keys if RetrieveKeysOnly parameter is also false"
$ConfirmProceed = Read-Host "Type Yes to proceed"
if($ConfirmProceed -ne "Yes"){
Write-Warning "Aborting as input did not equal Yes"
exit
}
}
$CurrentDateTime = (Get-Date -Format "dd-MM-yyyy-HH-mm")
$azSubs = Get-AzSubscription | Where-Object {$_.Name -NotIn $excludedSubscriptions}
$KeyVaultKeyArray = @()
$ForbiddenKeyVaults = @()
$ForbiddenGetKeyRotationPolicy = @()
$IPUnauthorisedGetKeyRotationPolicy = @()
$OtherErrorGetKeyRotationPolicy = @()
$SetKeyRotationPolicyErrors = @()
$ExistingKeyvaultRotationPolicies = @{}
# Define array variables
$azKvkRPAlreadyExistsDetails = New-Object System.Collections.ArrayList # variable to hold vm details in arraylist
$azKvkRPNewlyCreatedDetails = New-Object System.Collections.ArrayList # variable to hold vm details in arraylist
$azKvkRPDetail = [ordered]@{} # show the list in the order objects were added
$KeyVaultKeyArray = @()
# loop through all subscriptions
foreach ( $azSub in $azSubs ) {
# for each subscription, do a Set-AzContext to switch the subscription specific resources going foward
Write-Host "Iterating through Keyvaults in subscription: " $azSub.Name
Set-AzContext -Subscription $azSub | Out-Null
$azSubName = $azSub.Name
# Get all the KeyVaults in that returned $azSubs variable
try {
$azKVs = Get-AzKeyVault -ErrorAction Stop
}
catch {
Write-Warning "Error retrieving KeyVaults for subscription: $($azSub.Name), check log for full details"
$_.Exception.Message | Out-File $RotationPolicyDirectory\Get-AzKeyVaultErrors$CurrentDateTime.txt -Append
}
#Write-Host "Subscription: " $azSub.Name
# loop through each of the key vault
foreach ( $azKv in $azKVs ) {
if($excludedKeyvaults -notcontains $azKv.VaultName){
#Loop through keys only for the keyvaults whose Sku is 'Standard'
if ($szKv.Sku -ne 'Premium'){
Write-Host "Iterating through keys in Keyvault: " $azKv.VaultName
# Get the keys present in the keyvault and loop through them
try{
if($azKVKeys = Get-AzKeyVaultKey -VaultName $azKv.VaultName -ErrorAction Stop){
Write-Host "Found keys in KeyVault: $($azKv.VaultName)" -ForegroundColor Green
ForEach($KVKey in $azKVKeys){
Write-Host "Found key: $($KVKey.name)" -ForegroundColor Green
}
}
$KeyVaultKeyArray += $azKVKeys
}
catch{
Clear-Variable -Name azKVKeys -ErrorAction SilentlyContinue
if($PSItem.Exception.Message -match "Code: Forbidden"){
$ForbiddenKeyVaults += $azKv.VaultName
}
"KeyVault: $($azKv.VaultName)`n" + $PSItem.Exception.Message + "`n------------------------`n`n" | Out-File $RotationPolicyDirectory\Get-AzKeyVaultKeyErrors$CurrentDateTime.txt -Append
}
if($RetrieveKeysOnly -eq $false){
# loop through each of the keys if not part of excluded keys array RetrieveKeysOnly
foreach ($azKvk in $azKVKeys) {
if($excludedKeys -notcontains $azKvk.Name){
try {
if(($azKvkRotationPolicy = Get-AzKeyVaultKeyRotationPolicy -VaultName $azKv.VaultName -Name $azKvk.Name -ErrorAction Stop).Id){
Write-Host "Existing KeyVault Rotation Policy for $($azKvk.Name) found, ID: " $azKvkRotationPolicy.Id
$ExistingKeyvaultRotationPolicies.Add("$($azKvk.Name)", "$($azKvkRotationPolicy.id)")
}
}
catch{
if($PSItem.Exception.Message -match "Client address is not authorized"){
#Write-Warning "Get Key Rotation Policy for $($azKvk.Name) can not be accessed, client IP unauthorised"
$IPUnauthorisedGetKeyRotationPolicy += $azKvk.Name
"$($azKvk.Name)`n" + $_.Exception.Message | Out-File $RotationPolicyDirectory\Get-AzKeyVaultKeyRotationPolicyFirewallErrors$CurrentDateTime.txt -Append
}
elseif($PSItem.Exception.Message -match "keys getrotationpolicy"){
#Write-Warning "Get Key Rotation Policy for $($azKvk.Name) forbidden, client does not have getrotationpolicy permission"
$ForbiddenGetKeyRotationPolicy += $azKvk.Name
"$($azKvk.Name)`n" + $_.Exception.Message | Out-File $RotationPolicyDirectory\Get-AzKeyVaultKeyRotationPolicyUnauthorizedErrors$CurrentDateTime.txt -Append
}
else{
#Write-Warning "Get Key Rotation Policy for $($azKvk.Name) other error, check logs for full details"
$OtherErrorGetKeyRotationPolicy += $azKvk.Name
"$($azKvk.Name)`n" + $_.Exception.Message | Out-File $RotationPolicyDirectory\Get-AzKeyVaultKeyRotationPolicyOtherErrors$CurrentDateTime.txt -Append
}
}
# If a keyvault rotation policy does not exist, the .Id will be null for this specific variable $azKvkRotationPolicy
# Set the Rotation policy defined in the rotationpolicy.JSON file
if ($azKvkRotationPolicy.Id) {
$azKvkRP = Get-AzKeyVaultKeyRotationPolicy -VaultName $azKv.VaultName -Name $azKvk.Name
$azKvkRPDetail.'Subscription_Name' = $azSub.Name
$azKvkRPDetail.'KVName' = $azKv.VaultName
$azKvkRPDetail.'KVKeyName' = $azKvk.Name
$azKvkRPDetail."KV_Sku" = $szKv.Sku
$azKvkRPDetail.'Rotation_Policy_Id' = $azKvkRP.Id
$azKvkRPDetail."Rotation_Policy_CreatedOn" = $azKvkRP.CreatedOn
#Write-Host "Key Rotation Policy already exists:`n" + "$($azSub.Name)`n" + "$($azKv.VaultName)`n" + "$($azKv.Sku)`n" + "$($azKvk.Name)`n" + "$($azKvkRP.Id)`n" + "$($azKvkRP.CreatedOn)`n"
$azKvkRPAlreadyExistsDetails.Add((New-object PSObject -Property $azKvkRPDetail)) | Out-Null
}
else {
try {
if($WhatIf -eq $true){
$azKvkRP = Set-AzKeyVaultKeyRotationPolicy -VaultName $azKv.VaultName -Name $azKvk.Name -PolicyPath $RotationPolicyDirectory\rotationpolicy.json -WhatIf -ErrorAction Stop
}
elseif($WhatIf -eq $false){
$azKvkRP = Set-AzKeyVaultKeyRotationPolicy -VaultName $azKv.VaultName -Name $azKvk.Name -PolicyPath $RotationPolicyDirectory\rotationpolicy.json -ErrorAction Stop
}
}
catch {
Write-Warning "Error Setting Key Vault Rotation Policy for $($azKvk.Name)"
"$($azKvk.Name)`n" + $_.Exception.Message | Out-File $RotationPolicyDirectory\Set-AzKeyVaultKeyRotationPolicyErrors$CurrentDateTime.txt -Append
$SetKeyRotationPolicyErrors += $azKvk.Name
}
$azKvkRPDetail.'Subscription_Name' = $azSub.Name
$azKvkRPDetail.'KVName' = $azKv.VaultName
$azKvkRPDetail.'KVKeyName' = $azKvk.Name
$azKvkRPDetail."KV_Sku" = $szKv.Sku
$azKvkRPDetail.'Rotation_Policy_Id' = $azKvkRP.Id
$azKvkRPDetail."Rotation_Policy_CreatedOn" = $azKvkRP.CreatedOn
Write-Host "Created new Key rotation policy:`n" + "$($azSub.Name)`n" + "$($azKv.VaultName)`n" + "$($azKv.Sku)`n" + "$($azKvk.Name)`n" + "$($azKvkRP.Id)`n" + "$azKvkRP.CreatedOn`n" -ForegroundColor Green
$azKvkRPNewlyCreatedDetails.Add((New-object PSObject -Property $azKvkRPDetail)) | Out-Null
}
}
else {
Write-Warning "$($azKvk.Name) is in the excluded keys array, skipping"
}
Clear-Variable -Name azKvkRotationPolicy -ErrorAction SilentlyContinue
}
}
}
}
else {
Write-Warning "$($azKv.VaultName) is in excluded vaults array, skipping"
}
}
}
$azKvkRPAlreadyExistsDetails | Export-Csv -Path "$RotationPolicyDirectory\kevault-existing-rotation-policy-details.csv" -NoTypeInformation -Encoding UTF8 -Delimiter ',' #Export to CSV
$azKvkRPNewlyCreatedDetails | Export-Csv -Path "$RotationPolicyDirectory\kevault-new-rotation-policy-details.csv" -NoTypeInformation -Encoding UTF8 -Delimiter ',' #Export to CSV
if($ForbiddenKeyVaults){
Write-Host "`n`n"
Write-Warning "Forbidden access to following KeyVaults, please review log file for more info:"
$ForbiddenKeyVaults
}
if($ForbiddenGetKeyRotationPolicy){
Write-Host "`n`n"
Write-Warning "`n`nForbidden access to following key rotation policies, please review log file for more info:"
$ForbiddenGetKeyRotationPolicy
}
if($IPUnauthorisedGetKeyRotationPolicy){
Write-Host "`n`n"
Write-Warning "Client IP unauthorised to following key rotation policies, please review log file for more info:"
$IPUnauthorisedGetKeyRotationPolicy
}
if($OtherErrorGetKeyRotationPolicy){
Write-Host "`n`n"
Write-Warning "Other errors when getting following key rotation policies, please review log file for more info:"
$OtherErrorGetKeyRotationPolicy
}
if($RetrieveKeysOnly -eq $false){
Write-Host "`n`nExisting KeyVault Key Rotation Policies:"
$ExistingKeyvaultRotationPolicies
}
if($azKvkRPNewlyCreatedDetails){
Write-Host "`n`nNew KeyVault Key Rotation Policies:"
$azKvkRPNewlyCreatedDetails
}
Write-Host "`n`nOutputting file KeyVaultsKeys.txt containing all keys that were accessible"
Write-Host "`n`n---Please scroll up to review all output of errors, existing and new rotation policies---" -ForegroundColor Green
Write-Host "`n`n---Please check logs for full script output details---" -ForegroundColor Green
$KeyVaultKeyArray | Out-File $RotationPolicyDirectory\KeyVaultsKeys.txt
}