Event-Driven KeyVault Secrets Rotation Management
Published Mar 07 2021 04:00 PM 4,237 Views

In my previous post, I discussed how all secrets in Azure Key Vault could automatically manage their versions to get disabled. While that approach was surely useful, it sometimes seems overkilling to iterate against all secrets at once. What if you can manage only a certain secret when the secret gets a new version updated? That could be more cost-effective. As an Azure Key Vault instance publishes events through Azure EventGrid, by capturing this event, you can manage version rotation.


Throughout this post, I'm going to discuss how to handle events published through Azure EventGrid and manage secret rotations using Azure Logic Apps and Azure Functions when a new secret version is created.


You can download the sample code from this GitHub repository.


Events from Azure Key Vault


Azure Key Vault publishes events to Azure EventGrid. Whenever a new secret version is added, it always raises an event. Therefore, processing this event doesn't have to iterate all secrets but focuses on the specific secret, making our lives easier. Here's the high-level end-to-end workflow architecture using Azure Key Vault, Azure EventGrid, Azure Logic Apps and Azure Functions.


Overall E2E Process Architecture


Like I mentioned in my previous post, using Azure Logic Apps as an event handler doesn't require the event delivery authentication. But if you prefer explicit authentication, please refer to my another blog post.


There are two ways to integrate Azure Key Vault with Azure Logic App as an event handler. One uses the EventGrid trigger through the connector, and the other uses the HTTP trigger like a regular HTTP API call. While the former generates a dependency on the connector, the latter works both instances independently, which is my preferred approach.


First of all, create an Azure Logic App instance and add the HTTP trigger.


Logic Apps HTTP Trigger


Once save the Logic App workflow, you will get the endpoint URL, which will be used as the event handler webhook. Go to the Azure Key Vault instance's Events blade and click the + Event Subscription button.


Event Subscription Button


You will be asked to create an EventGrid subscription instance. Enter Event Subscription Details Name, Event Schema, System Topic Name, Event Type, Endpoint Type and Endpoint URL.


Event Subscription Details


  • In the Event Subscription Details session, choose Cloud Event Schema v1.0 because it's the standard spec of CNCF and it's convenient for heterogeneous systems integration.
  • Enter the Event Grid Topic name to the System Topic Name field.
  • Choose only the Secret New Version Created event in the Filter to Event Types dropdown.
  • Choose Webhook and enter the endpoint URL copied from the Logic App HTTP trigger.


You've completed the very basic pipeline between Azure Key Vault, Azure EventGrid and Azure Logic Apps to handle events. If you create a new version of a particular secret, it generates an event captured by the Logic App instance. Confirm that the Microsoft.KeyVault.SecretNewVersionCreated event type has been captured.


Event Captured by Logic App


The actual event data as a JSON payload looks like this:


Event Data Payload in Logic App


There is the attribute called ObjectName in the data attribute, which is the secret name. You need to send this value to Azure Functions to process the secret version rotation management. Let's implement the function logic.


Version Rotation Management against Specific Secret via Azure Functions


There are not many differences from my previous post. However, this implementation time will become simpler because it doesn't have to iterate all the secrets at once but look after a specific one. First of all create a new HTTP Trigger.

    func new --name DisableSecretHttpTrigger --template HttpTrigger --language C#


A new HTTP trigger has been generated with the default template. Now, update the HttpTrigger binding settings. Remove the GET method and put the routing URL to secrets/{name}/disable/{count:int?} (line #5). Notice that the routing URL contains placeholders like {name} and {count:int?}, which are substituted with parameters of string name and int? count respectively (line #6).


    public static class DisableSecretHttpTrigger
        public static async Task Run(
            [HttpTrigger(AuthorizationLevel.Function, "POST", Route = "secrets/{name}/disable/{count:int?}")] HttpRequest req,
            string name, int? count,
            ILogger log)


Get the two values from the environment variables. One is the endpoint URL to the Azure Key Vault instance, and the other is the tenant ID where the Key Vault instance is hosted.


            // Get the KeyVault URI
            var uri = Environment.GetEnvironmentVariable("KeyVault__Uri");

            // Get the tenant ID where the KeyVault lives
            var tenantId = Environment.GetEnvironmentVariable("KeyVault__TenantId");


Next, instantiate the SecretClient object that can access the Key Vault instance. While instantiating, give the authentication options with the DefaultAzureCredentialOptions object. If your log-in account is bound with multiple tenants, you should explicitly specify the tenant ID; otherwise, you will get the authentication error (line #4-6).


            // Set the tenant ID, in case your account has multiple tenants logged in
            var options = new DefaultAzureCredentialOptions()
                SharedTokenCacheTenantId = tenantId,
                VisualStudioTenantId = tenantId,
                VisualStudioCodeTenantId = tenantId,
            var client = new SecretClient(new Uri(uri), new DefaultAzureCredential(options));


As you already know the secret name, populate all the versions of the given secrets. Of course, you don't need inactive versions. Therefore, use the WhereAwait clause to filter them out (line #5). Additionally, use the OrderByDescendingAwait clause to sort all the active versions in the reverse-chronological order (line #6).


            // Get the all versions of the given secret
            // Filter only enabled versions
            // Sort by the created date in a reverse order
            var versions = await client.GetPropertiesOfSecretVersionsAsync(name)
                                    .WhereAwait(p => new ValueTask(p.Enabled.GetValueOrDefault() == true))
                                    .OrderByDescendingAwait(p => new ValueTask(p.CreatedOn.GetValueOrDefault()))


If there is no version enabled, end the function by returning the AcceptedResult instance.


            // Do nothing if there is no version enabled
            if (!versions.Any())
                return new AcceptedResult();


As you need at least two versions enabled for rotation if there is no count value given, set the value to 2 as the default.


            if (!count.HasValue)
                count = 2;


If the number of secret versions enabled is less than the count value, complete the processing and return the AcceptedResult instance.


            // Do nothing if there is only given number of versions enabled
            if (versions.Count < count.Value + 1)
                return new AcceptedResult();


Let's disable the remaining versions. Skip as many as the count value of the versions (line #2). Set the Enabled value to false (line #7), the update them (line #9).


            // Disable all versions except the first (latest) given number of versions
            var candidates = versions.Skip(count.Value).ToList();
            var results = new List();
            foreach (var candidate in candidates)
                candidate.Enabled = false;

                var response = await client.UpdateSecretPropertiesAsync(candidate).ConfigureAwait(false);



Finally, return the processed result as a response.


            var res = new ContentResult()
                Content = JsonConvert.SerializeObject(results, Formatting.Indented),
                ContentType = "application/json",
                StatusCode = (int)HttpStatusCode.OK,

            return res;


The implementation of the Azure Functions side is over. Let's integrate it with Azure Logic Apps.


Integration of Azure Logic Apps with Azure Functions


Add both HTTP action and Response action to the Logic App instance previously generated. Make sure that you call the Azure Functions app through the HTTP action, with the ObjectName value and 2 as the routing parameters.


Additional Actions to Logic Apps


Now, you've got the integration workflow completed from Azure Key Vault to Azure Functions via Azure EventGrid and Logic Apps. Let's run the workflow.


End-to-End Test – Adding New Secret Version to Azure Key Vault


In order to run the integrated workflow, you need to create a new version of the Azure Key Vault Secrets.


List of Azure Key Vault Secrets


Add a new version of the secret.


Adding a New Version of Secret


You will see the new version added.


Result of the New Version of Secret Added


When a new secret version is added, it publishes an event to EventGrid, and the Logic App captures the event. Can you confirm the ObjectName value and the secret version are the same as the one on the Azure Key Vault instance?


Logic App Run Result


Once you complete the end-to-end integration workflow, you will be able to see that all versions except the latest two have been disabled.


Secret Versions Disabled


So far, we've implemented a new logic that captures an event published when a new secret version is added to Azure Key Vault instance, and process the rotation management against the specific secret, using Azure EventGrid, Azure Logic Apps and Azure Functions. It would be handy if you have a similar use case and implement this sort of event-driven workflow process.

This article was originally published on Dev Kimchi.

Version history
Last update:
‎Mar 04 2021 11:04 PM
Updated by: