Long story, short (2 days later)
While implementing an Azure Function that is designed to fetch secrets from Azure KeyVault, I ran into a funny and odd issue. I am not able to explain why and what is going on, but I have tried every trick a google search can conjure, at least until page 30 in the search results. It was by coincidence I came across some of the parameters in the DefaultAzureCredentialOptions class that got me going, at least locally.
The idea, as far as I have understood, is that whenever you invoke the Azure.Identity.DefaultAureCredential class, it provides a flow for attempting authentication using one of the following credentials, in listed order:
- EnvironmentCredential
- WorkloadIdentityCredential
- ManagedIdentityCredential
- AzureDeveloperCliCredential
- SharedTokenCacheCredential
- VisualStudioCredential
- VisualStudioCodeCredential
- AzureCliCredential
- AzurePowerShellCredential
- InteractiveBrowserCredential
I suspect that since I have deployed my Azure Function using the Managed Identity setting to a Systems Assigned identity, like this:
AND the fact that ManagedIdentityCredential is before VisualStudioCredential in the authentication flow, it fails, since it is unable to authenticate the managed identity – which is the main principle of the design – none other than the service can assume the identity of the service.
See more detail here: https://learn.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview
Snip
- System-assigned. Some Azure resources, such as virtual machines allow you to enable a managed identity directly on the resource. When you enable a system-assigned managed identity:
- A service principal of a special type is created in Azure AD for the identity. The service principal is tied to the lifecycle of that Azure resource. When the Azure resource is deleted, Azure automatically deletes the service principal for you.
- By design, only that Azure resource can use this identity to request tokens from Azure AD.
- You authorize the managed identity to have access to one or more services.
- The name of the system-assigned service principal is always the same as the name of the Azure resource it is created for. For a deployment slot, the name of its system-assigned identity is
<app-name>/slots/<slot-name>
.
Love rears it’s ugly head
Having assigned the proper permissions in the Azure KeyVault, you are able to connect using your credentials in Visual Studio to said KeyVault. A code example of that could look like this:
public static string GetSecret( string keyvault, string secret )
{
var kvUri = $"https://{keyvault}.vault.azure.net";
var creds = new DefaultAzureCredential();
var client = new SecretClient(new Uri(kvUri), creds);
var secret = client.GetSecretAsync(secret).Result.Value.Value;
return secret;
}
(link to NuGet: NuGet Gallery | Azure.Security.KeyVault.Secrets 4.5.0)
Usually this works, and I have no other explanation than having deployed the solution to a live running App Service is what breaks this otherwise elegant piece of code. The above listed code does not work for me.
Workaround
You can instantiate the DefaultAzureCredential class using a constructor that takes a DefaultAzureCredentialOptions object as a parameter and this object has a great number of attributes that are of interest. You can actively remove items in the authentication flow and you can specify the tenant id, if you have access to multiple tenants.
The code that resolved the issue locally looks something like this. (I can probably just do without the ManagedIdentity, will test)
public static string GetSecret( string keyvault, string secret )
{
var kvUri = $"https://{keyvault}.vault.azure.net";
var creds = new DefaultAzureCredential(
new DefaultAzureCredentialOptions() {
TenantId = "<INSERT TENANT ID HERE>"
, ExcludeAzureCliCredential = true
, ExcludeAzurePowerShellCredential = true
, ExcludeSharedTokenCacheCredential = true
, ExcludeVisualStudioCodeCredential = true
, ExcludeEnvironmentCredential = true
, ExcludeManagedIdentityCredential = true
});
var client = new SecretClient(new Uri(kvUri), creds);
var secret = client.GetSecretAsync(secret).Result.Value.Value;
return secret;
}
I am not sure this will work when I deploy the solution, but I will probably create a test on environment (local debug or running prod)
HTH