Event-Driven KeyVault Secrets Rotation Management

Published 03-07-2021 04:00 PM 2,761 Views
Microsoft

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
    {
        [FunctionName("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()))
                                    .ToListAsync()
                                    .ConfigureAwait(false);

 

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();
            results.AddRange(versions.Take(count.Value));
            foreach (var candidate in candidates)
            {
                candidate.Enabled = false;

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

                results.Add(response.Value);
            }

 

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.

2 Comments
Microsoft

@Brandon Hurlburt Thanks for pointing to the sample code! It is similar to mine, but this post has different approach from the sample code you linked. The solution in this post doesn't event need to restart the function app to refresh the secret token cache either.

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