{"id":16432,"date":"2025-10-16T00:00:00","date_gmt":"2025-10-16T07:00:00","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/ise\/?p=16432"},"modified":"2025-10-16T22:24:53","modified_gmt":"2025-10-17T05:24:53","slug":"managing-azure-keyvault-secrets-with-tagging-strategy","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/ise\/managing-azure-keyvault-secrets-with-tagging-strategy\/","title":{"rendered":"Managing secrets on Azure KeyVault with a Tagging strategy to perform automations"},"content":{"rendered":"<h1>Introduction<\/h1>\n<p>While Azure managed identities are the preferred authentication method to avoid storing secrets, organizations still need effective strategies for managing existing secrets in Azure KeyVault.<\/p>\n<p>For the purpose of managing secrets in KeyVault, there is a need to:<\/p>\n<ul>\n<li>Detail in plain language what the purpose of each secret is so that stakeholders can understand it.<\/li>\n<li>Describe the process used to generate the secret so that we can update the secret as necessary.<\/li>\n<li>Define the rotation strategy for each secret and indicate whether rotation will require downtime.<\/li>\n<\/ul>\n<p>Implementing tagging on KeyVault secrets addresses these requirements effectively.<\/p>\n<ul>\n<li>Users with Azure Key Vault access can view secret tags in the Azure Portal to understand each secret&#8217;s purpose. We can enforce this as a standard via Azure Policy applied on the Key Vault resource.<\/li>\n<li>Bash or PowerShell scripts can retrieve secret tags to generate dynamic metadata views and automate secrets rotation.<\/li>\n<li>Additionally, we can auto-generate documentation like Markdown files for secrets inventory in security plans.<\/li>\n<\/ul>\n<p>If you have an existing Azure KeyVault and you are tagging secrets for the first time, one outcome of carrying out a tagging exercise is that we can identify secrets that are no longer in use. This is an important step because removing unused secrets means we are minimizing the risk of access to a specific resource. We will discuss how we approach this in <a href=\"#removing-unused-secrets\">Removing unused Secrets<\/a>.<\/p>\n<h2>Notes<\/h2>\n<ul>\n<li>This tagging strategy applies only to Azure Key Vault secrets, since other secrets management tools may lack both tagging capabilities and command-line interface support for automation.<\/li>\n<li>This approach using Bash or PowerShell adds maintenance overhead, but enables consistent enforcement and automation.<\/li>\n<\/ul>\n<h2>Tagging Strategy<\/h2>\n<p>Here are the proposed keys for implementing a tagging strategy that you can apply on your Azure KeyVault secrets.<\/p>\n<table>\n<thead>\n<tr>\n<th>Field<\/th>\n<th>Required?<\/th>\n<th>Details<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>purpose<\/td>\n<td>required<\/td>\n<td>Free text, no more than 256 characters, limit set by Azure<\/td>\n<\/tr>\n<tr>\n<td>rotation-policy<\/td>\n<td>required<\/td>\n<td># days\/none, none if not applicable or unknown<\/td>\n<\/tr>\n<tr>\n<td>update-downtime<\/td>\n<td>required<\/td>\n<td>true\/false\/none, true if an update cause app to be down\/restarted, none if not applicable or unknown<\/td>\n<\/tr>\n<tr>\n<td>source-type<\/td>\n<td>required<\/td>\n<td>See Source types below<\/td>\n<\/tr>\n<tr>\n<td>source-other<\/td>\n<td>optional<\/td>\n<td>Free text, no more than 256 characters, limit set by Azure, used when source-type = other<\/td>\n<\/tr>\n<tr>\n<td>source-id<\/td>\n<td>optional<\/td>\n<td>This can be the name of the resource or the full resource Id where secret is for\/used<\/td>\n<\/tr>\n<tr>\n<td>destination-id<\/td>\n<td>optional<\/td>\n<td>This can be the name of the resource or the full resource Id where secret is used<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<h3>Source types<\/h3>\n<p>The source type should inform the process used to generate the secret. Here are some examples for source types. As shown, the following contain links to documentation on Microsoft Learn which shows how a user can regenerate the secret.<\/p>\n<ul>\n<li>storage-blob-sas: Azure Blob Storage Shared Access Signature, see: <a href=\"https:\/\/learn.microsoft.com\/en-us\/azure\/storage\/common\/storage-sas-overview\">Grant limited access to data with shared access signatures (SAS) &#8211; Azure Storage | Microsoft Learn<\/a><\/li>\n<li>storage-connection-string: Azure Storage Key, see: <a href=\"https:\/\/learn.microsoft.com\/en-us\/azure\/storage\/common\/storage-account-keys-manage?tabs=azure-portal#use-azure-key-vault-to-manage-your-access-keys\">Manage account access keys &#8211; Azure Storage | Microsoft Learn<\/a><\/li>\n<li>acr-username: Azure Container Registry username <a href=\"https:\/\/learn.microsoft.com\/en-us\/azure\/container-registry\/container-registry-authentication?tabs=azure-cli#admin-account\">Azure Container Registry Authentication Options Explained &#8211; Azure Container Registry | Microsoft Learn<\/a><\/li>\n<li>acr-password: Azure Container Registry password <a href=\"https:\/\/learn.microsoft.com\/en-us\/azure\/container-registry\/container-registry-authentication?tabs=azure-cli#admin-account\">Azure Container Registry Authentication Options Explained &#8211; Azure Container Registry | Microsoft Learn<\/a><\/li>\n<\/ul>\n<h2>Automation scripts<\/h2>\n<p>The following is an example of a PowerShell script that a user can invoke to generate a view of all Azure KeyVault secrets as a reference. It is required for Azure CLI to be installed and the user is logged into an Azure Subscription.<\/p>\n<pre><code class=\"language-powershell\">&lt;#\r\n.SYNOPSIS\r\n    Generates a view of Azure KeyVault secrets.\r\n\r\n.DESCRIPTION\r\n    You can generate a table view of all secrets specified in a resource group or a filtered view of matching secrets by providing a name with more details.\r\n\r\n.PARAMETER ResourceGroups\r\n    Specifies the resource groups to be processed. This parameter is mandatory and should contain a comma-separated list of resource group names.\r\n\r\n.PARAMETER Name\r\n    Specifies the name of a specific secret to process. If this parameter is provided, the script will focus on this particular secret.\r\n\r\n.PARAMETER GenerateMarkDown\r\n    If this switch is provided, the script will generate a Markdown output of the secrets.\r\n\r\n.EXAMPLE\r\n    .\\script.ps1 -ResourceGroups \"ResourceGroup1,ResourceGroup2\" -GenerateMarkDown\r\n    This example processes all secrets in the specified resource groups and generates a Markdown report.\r\n\r\n.NOTES\r\n    Be sure to login via Azure CLI.\r\n#&gt;\r\nparam(\r\n    [Parameter(Mandatory = $true)][string]$ResourceGroups,  \r\n    [Parameter(Mandatory = $false)][string]$Name,\r\n    [switch]$GenerateMarkDown)\r\n\r\nfunction  CreateSecret {\r\n    param (\r\n        $VaultName,\r\n        $secret,\r\n        [switch]$shouldTruncate,\r\n        [switch]$fullDetail\r\n    )\r\n    $purpose = \"NOT SET\"\r\n    $sourceType = \"NOT SET\"\r\n    $shouldRotate = \"NOT SET\"\r\n    $updateDowntime = \"NOT SET\"\r\n    $usage = \"NOT SET\"\r\n    $rotationPolicy = \"NOT SET\"\r\n\r\n    Write-Host \"processing secret:\" $secret.name \" vault:\" $VaultName\r\n\r\n    if ($secret.tags.\"purpose\") {\r\n\r\n        $purpose = $secret.tags.\"purpose\"\r\n\r\n        if (-not $GenerateMarkDown -and $shouldTruncate -and $purpose.Length -gt 12) {\r\n            $purpose = $purpose.Substring(0, 12) + \"...\"\r\n        }\r\n    }\r\n    if ($secret.tags.\"source-type\") {\r\n        $sourceType = $secret.tags.\"source-type\"\r\n    }\r\n    $versions = az keyvault secret list-versions --name $secret.name --vault-name $VaultName | ConvertFrom-Json\r\n    $secretCreated = [datetime]::Parse($versions[0].attributes.created)\r\n    $activeDays = ([datetime]::Today - $secretCreated).Days\r\n\r\n    if ($secret.tags.\"rotation-policy\") {\r\n        $rotationPolicy = $secret.tags.\"rotation-policy\"\r\n        # test if $secret.tags.\"rotation-policy\" is an int or string\r\n        if ($secret.tags.\"rotation-policy\" -is [int]) {            \r\n            if ($activeDays -gt [int]$secret.tags.\"rotation-policy\") {\r\n                $shouldRotate = \"Yes\"\r\n            }\r\n            else {\r\n                $shouldRotate = \"No\"\r\n            }\r\n\r\n        }\r\n        else {\r\n            $shouldRotate = \"Unknown\"\r\n        }\r\n    }                 \r\n    if ($secret.tags.\"update-downtime\") {\r\n        if ($secret.tags.\"update-downtime\" -eq \"true\") {\r\n            $updateDowntime = \"Yes\"\r\n        }  \r\n        if ($secret.tags.\"update-downtime\" -eq \"false\") {\r\n            $updateDowntime = \"No\"\r\n        }\r\n    }   \r\n    if ($secret.tags.\"source-id\") {\r\n        $usage = $secret.tags.\"source-id\"\r\n        if (-not $GenerateMarkDown -and $shouldTruncate -and $usage.Length -gt 12) {\r\n            $usage = $usage.Substring(0, 12) + \"...\"\r\n        }\r\n        $usage = \"Src: $usage\" \r\n    } \r\n\r\n    if ($secret.tags.\"destination-id\") {\r\n        $destination = $secret.tags.\"destination-id\"\r\n        if (-not $GenerateMarkDown -and $shouldTruncate -and $destination.Length -gt 12) {\r\n            $destination = $destination.Substring(0, 12) + \"...\"\r\n        }\r\n\r\n        if ($usage -eq \"NOT SET\") {\r\n            $usage = \"Dest: $destination\" \r\n        }\r\n        else {\r\n            $usage = \"$usage \/Dest: $destination\" \r\n        }\r\n    } \r\n\r\n    $expires = \"Indefinite\"\r\n    if ($secret.attributes.expires) {\r\n        $expires = $secret.attributes.expires\r\n    }  \r\n\r\n    $item = [PSCustomObject]@{\r\n        Name           = $secret.name\r\n        ShouldRotate   = \"$shouldRotate ($activeDays days)\"\r\n        SourceType     = $sourceType\r\n        DownTime       = $updateDowntime\r\n        Expires        = $expires     \r\n        Purpose        = $purpose\r\n        Usage          = $usage\r\n        RotationPolicy = $rotationPolicy                                       \r\n    }\r\n\r\n    if ($fullDetail) {\r\n        $item | Add-Member -MemberType NoteProperty -Name \"Created\" -Value $secret.attributes.created\r\n        $item | Add-Member -MemberType NoteProperty -Name \"Enabled\" -Value $secret.attributes.enabled\r\n    }\r\n    $item | Add-Member -MemberType NoteProperty -Name \"KeyVaultName\" -Value $VaultName\r\n    return $item\r\n}\r\n\r\n$items = @()\r\n$ResourceGroupNames = $ResourceGroups -split \",\"\r\nforeach ($ResourceGroup in $ResourceGroupNames) {\r\n    $keyVaults = az keyvault list --resource-group $ResourceGroup -o json | ConvertFrom-Json\r\n    foreach ($keyVault in $keyVaults) {\r\n        $secrets = az keyvault secret list --vault-name $keyVault.name -o json | ConvertFrom-Json\r\n        foreach ($secret in $secrets) {\r\n\r\n            if ($secret.name -eq $Name) {\r\n                $item = CreateSecret -VaultName $keyVault.name -secret $secret -fullDetail\r\n                $item | Format-List\r\n                break\r\n            }\r\n            $item = CreateSecret -VaultName $keyVault.name -secret $secret -shouldTruncate            \r\n            $items += $item\r\n        }\r\n    }\r\n}\r\n\r\nif ($GenerateMarkDown) {\r\n    $markdown = \"|Name|What is its purpose?|Where does it live?|How was it generated?|What's the rotation strategy?|Does it cause downtime?|How does the secret get distributed to consumers?|What's the secret's lifespan?|`n\"\r\n    $markdown += \"|---|--------------------|-------------------|---------------------|-----------------------------|-----------------------|-------------------------------------------------|----------------------------|`n\"\r\n    # Name \r\n    # What is its purpose?\r\n    # Where does it live?\r\n    # How was it generated?\r\n    # What's the rotation strategy? \r\n    # Does it cause downtime? \r\n    # How does the secret get distributed to consumers?\r\n    # What\u2019s the secret\u2019s lifespan?\r\n    foreach ($item in $items) {\r\n        $markdown += \"|$($item.Name)|$($item.Purpose)|$($item.KeyVaultName)|$($item.SourceType)|$($item.RotationPolicy)|$($item.DownTime)||$($item.Expires)|`n\"\r\n    }\r\n    $markdown\r\n    return\r\n}\r\n\r\nif (!$Name ) {\r\n    $items | Format-Table -AutoSize\r\n}<\/code><\/pre>\n<p>The following is an example of a PowerShell script that a user can use to tag secrets.<\/p>\n<pre><code class=\"language-powershell\">&lt;#\r\n.SYNOPSIS\r\n    Tags Azure KeyVault secrets with specified metadata.\r\n\r\n.DESCRIPTION\r\n    This script tags Azure KeyVault secrets with metadata such as purpose, rotation policy, update downtime, source type, and more. \r\n    It processes secrets in the specified resource groups and updates the tags accordingly.\r\n\r\n.PARAMETER ResourceGroups\r\n    Specifies the resource groups to be processed. This parameter is mandatory and should contain a comma-separated list of resource group names.\r\n\r\n.PARAMETER Name\r\n    Specifies the name of a specific secret to process. This parameter is mandatory.\r\n\r\n.PARAMETER Purpose\r\n    Specifies the purpose of the secret. This parameter is mandatory.\r\n\r\n.PARAMETER RotationPolicy\r\n    Specifies the rotation policy of the secret. This parameter is mandatory.\r\n\r\n.PARAMETER UpdateDowntime\r\n    Specifies whether updating the secret causes downtime. This parameter is mandatory.\r\n\r\n.PARAMETER SourceType\r\n    Specifies the source type of the secret. This parameter is mandatory.\r\n\r\n.PARAMETER SourceOther\r\n    Specifies additional source information if the source type is 'other'. This parameter is optional.\r\n\r\n.PARAMETER SourceId\r\n    Specifies the name of the resource or the full resource Id where secret is for\/used. This parameter is optional.\r\n\r\n.PARAMETER DestinationId\r\n    Specifies the name of the resource or the full resource Id where secret is for\/used. This parameter is optional.\r\n\r\n.EXAMPLE\r\n    .\\script.ps1 -ResourceGroups \"ResourceGroup1,ResourceGroup2\" -Name \"SecretName\" -Purpose \"ExamplePurpose\" -RotationPolicy \"30\" -UpdateDowntime \"false\" -SourceType \"storage-blob-sas\"\r\n    This example tags the specified secret in the specified resource groups with the provided metadata.\r\n\r\n.NOTES\r\n    Be sure to login via Azure CLI.\r\n#&gt;\r\nparam(\r\n    [Parameter(Mandatory = $true)][string]$ResourceGroups,\r\n    [Parameter(Mandatory = $true)][string]$Name,\r\n    [Parameter(Mandatory = $true)][string]$Purpose,\r\n    [Parameter(Mandatory = $true)][string]$RotationPolicy,\r\n    [Parameter(Mandatory = $true)][string]$UpdateDowntime,\r\n    [Parameter(Mandatory = $true)][string]$SourceType,\r\n    [Parameter(Mandatory = $false)][string]$SourceOther,\r\n    [Parameter(Mandatory = $false)][string]$SourceId,    \r\n    [Parameter(Mandatory = $false)][string]$DestinationId)\r\n\r\n$ResourceGroupNames = $ResourceGroups -split \",\"\r\nforeach ($ResourceGroup in $ResourceGroupNames) {\r\n    $keyVaults = az keyvault list --resource-group $ResourceGroup -o json | ConvertFrom-Json\r\n    foreach ($keyVault in $keyVaults) {\r\n        $secrets = az keyvault secret list --vault-name $keyVault.name -o json | ConvertFrom-Json\r\n        foreach ($secret in $secrets) {\r\n            if ($Name -eq $secret.name) {\r\n                $change = @()\r\n                $change += \"purpose=$Purpose\"\r\n                $change += \"rotation-policy=$RotationPolicy\"\r\n                $change += \"update-downtime=$UpdateDowntime\"\r\n                $change += \"source-type=$SourceType\"\r\n\r\n                if ($SourceOther) {\r\n                    $change += \"source-other=$SourceOther\"\r\n                }\r\n                else {\r\n                    if ($secret.tags.\"source-other\") {\r\n                        $value = $secret.tags.\"source-other\"\r\n                        $change += \"source-other=$value\"\r\n                    }\r\n                }\r\n\r\n                if ($SourceId) {\r\n                    $change += \"source-id=$SourceId\"\r\n                }\r\n                else {\r\n                    if ($secret.tags.\"source-id\") {\r\n                        $value = $secret.tags.\"source-id\"\r\n                        $change += \"source-id=$value\"\r\n                    }\r\n                }\r\n\r\n                if ($DestinationId) {\r\n                    $change += \"destination-id=$DestinationId\"\r\n                }\r\n                else {\r\n                    if ($secret.tags.\"destination-id\") {\r\n                        $value = $secret.tags.\"destination-id\"\r\n                        $change += \"destination-id=$value\"\r\n                    }\r\n                }\r\n\r\n                Write-Host \"Updating $Name\"\r\n                # update secret with new tags\r\n                $log = az keyvault secret set-attributes --vault-name $keyVault.name --name $Name --tags $change\r\n                if ($LASTEXITCODE -ne 0) {\r\n                    throw \"Failed to update secret $Name - $log\" \r\n                }\r\n                else {\r\n                    Write-Host \"Updated tags for secret $Name\"\r\n                }\r\n                return\r\n            }\r\n        }\r\n    }\r\n}<\/code><\/pre>\n<h2>Removing unused Secrets<\/h2>\n<p>During the tagging process, you may identify secrets that are not well known to you or others. It can be helpful to know if those secrets are still being accessed and by whom. This can be done by enabling auditing on Azure KeyVault under Diagnostic setting, choosing the <code>audit<\/code> category group and selecting a destination of your choice to persist the logs.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/ise\/wp-content\/uploads\/sites\/55\/2025\/10\/az-keyvault-diag-setting.png\" alt=\"Azure KeyVault Diagnostic setting\" \/><\/p>\n<p>Once enabled, you can take a few weeks to review the logs. If you are using Log Analytics Workspace, the following Kusto Query Language (KQL) script can help show the name of the secrets being accessed, the user or services accessing the secrets, whether it was successful and when the secrets are being accessed. Be sure to populate the <code>subId<\/code> for Subscription Id, <code>rgName<\/code> for resource group name and <code>vaultName<\/code> for Azure KeyVault name.<\/p>\n<pre><code class=\"language-kql\">let subId = \"\";\r\nlet rgName = \"\";\r\nlet vaultName = \"\";\r\n\r\nlet resourceId = strcat(\"\/SUBSCRIPTIONS\/\", subId, \"\/RESOURCEGROUPS\/\", rgName, \"\/PROVIDERS\/MICROSOFT.KEYVAULT\/VAULTS\/\", vaultName);\r\n\r\nAzureDiagnostics | where \r\nResourceId == resourceId\r\nand Category == 'AuditEvent'\r\nand OperationName  == 'SecretGet' \r\n| project \r\nextract(\"\/secrets\/([^\/]+)\/\", 1, id_s), trustedService_s, identity_claim_oid_g, identity_claim_appid_g, clientInfo_s, CallerIPAddress, ResultType, TimeGenerated<\/code><\/pre>\n<p>These information can help you determine who to reach out to. More importantly, it also identifies secrets that are no longer accessed for a period of time which can mean you can remove those secrets. It is crucial to enable the <code>soft delete<\/code> feature to ensure that secrets can be recovered if needed. One thing to note is that there is a period of time that a secret is soft deleted for. After that period, it is no longer possible to recover the secret.<\/p>\n<h2>Summary<\/h2>\n<p>Managing secrets requires us to implement practices such as secrets rotation, monitor usage etc. With a tagging strategy in place along with automation, we can ensure we are following best practices. I hope this blog post helps you in how we can approach this.<\/p>\n<p><em>Note: the picture that illustrates this article has been generated by AI on <a href=\"https:\/\/www.bing.com\/images\/create\">Bing Image Creator<\/a>.<\/em><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Cloud and Infrastructure teams can manage secrets on Azure KeyVault with a Tagging strategy to perform automations.<\/p>\n","protected":false},"author":124779,"featured_media":16433,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[1,3451],"tags":[3616,3618,3617],"class_list":["post-16432","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-cse","category-ise","tag-azure-keyvault","tag-secrets","tag-tagging"],"acf":[],"blog_post_summary":"<p>Cloud and Infrastructure teams can manage secrets on Azure KeyVault with a Tagging strategy to perform automations.<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/posts\/16432","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/users\/124779"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/comments?post=16432"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/posts\/16432\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/media\/16433"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/media?parent=16432"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/categories?post=16432"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/tags?post=16432"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}