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.
- KeyVault Secrets Rotation Management
- Event-Driven KeyVault Secrets Rotation Management
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.
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.
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.
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
.
- 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 theFilter 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.
The actual event data as a JSON payload looks like this:
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.
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.
Add a new version of the secret.
You will see the new version 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?
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.
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.