8 min read

Event-Driven KeyVault Secrets Rotation Management

Justin Yoo

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<IActionResult> 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<bool>(p.Enabled.GetValueOrDefault() == true))
.OrderByDescendingAwait(p => new ValueTask<DateTimeOffset>(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<SecretProperties>();
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.