1
votes

I am following the directions at https://docs.microsoft.com/en-us/azure/app-service/app-service-key-vault-references

Essentially, I am attempting to protect the storage connection string used for AzureWebJobsDashboard and AzureWebJobsStorage behind an Azure Key vault secret. I cannot use my injected KeyVault service to fetch it because my service container has not been built yet. So I found (through the link above) I could express this intent using a "@Microsoft.KeyVault()" expression in configuration. Here is an example where I moved the configuration to inline code to keep it terse:

.ConfigureHostConfiguration(configurationBuilder =>
{
    configurationBuilder
        .AddConfiguration(configuration)
        .AddInMemoryCollection(new Dictionary<string, string>
        {
            ["ConnectionStrings:AzureWebJobsDashboard"] = "@Microsoft.KeyVault(SecretUri=https://host.vault.azure.net/secrets/secret-name/ec545689445a40b199c0e0a956f16fca)",
            ["ConnectionStrings:AzureWebJobsStorage"] = "@Microsoft.KeyVault(SecretUri=https://host.vault.azure.net/secrets/secret-name/ec545689445a40b199c0e0a956f16fca)",
        });
})

If I run this, I get:

FormatException: No valid combination of account information found.

If I change the configuration values from the special annotation to the copied secret value from Key Vault (the blue copy button under the 'Show Secret Value' button), everything just works. This confirms to me the connection string I use is correct.

Also, I manually used KeyVaultClient w/AzureServiceTokenProvider to verify the process should work when running locally in Visual Studio, even before the host has been built. I am able to get the secret just fine. This tells me I have sufficient privileges to get the secret.

So now I am left wondering if this is even supported. There are pages which imply this is possible however, such as https://medium.com/statuscode/getting-key-vault-secrets-in-azure-functions-37620fd20a0b. At least for Azure Functions. I am using Azure Web Jobs which gets deployed as a console application with an ASP.NET Core service, and I cannot find an example with that configuration.

Can anybody clarify if what I am doing is supported? And if not, what is the advisable process for getting connection strings stored in Azure Key Vault before the Azure Web Jobs host has been built?

Thanks

1

1 Answers

1
votes

I have gone through a lot of online resources and everything seems to indicate that the special decorated @Microsoft.KeyVault setting only works when the value lives in AppSettings on the Azure Portal, not in local configuration. Somebody please let me know if that is an incorrect assessment.

So to solve this problem, I came up with a solution which in all honesty, feels a little hacky because I am depending on the fact that the connection string is not read/cached from local configuration until the host is ran (not during build). Basically, the idea is to build a configuration provider for which I can set a value after the host has been built. For example:

public class DelayedConfigurationSource : IConfigurationSource
{
    private IConfigurationProvider Provider { get; } = new DelayedConfigurationProvider();

    public IConfigurationProvider Build(IConfigurationBuilder builder) => Provider;

    public void Set(string key, string value) => Provider.Set(key, value);

    private class DelayedConfigurationProvider : ConfigurationProvider
    {
        public override void Set(string key, string value)
        {
            base.Set(key, value);
            OnReload();
        }
    }
}

A reference to this type gets added during host builder construction:

var delayedConfigurationSource = new DelayedConfigurationSource();

var hostBuilder = new HostBuilder()
    .ConfigureHostConfiguration(configurationBuilder =>
    {
        configurationBuilder
            .AddConfiguration(configuration)
            .Add(delayedConfigurationSource);
    })
    ...

And just make sure to set the configuration before running the host:

var host = hostBuilder.Build();

using (host)
{
    var secretProvider = host.Services.GetRequiredService<ISecretProvider>();
    var secret = await secretProvider.YourCodeToGetSecretAsync().ConfigureAwait(false);

    delayedConfigurationSource.Set("ConnectionStrings:AzureWebJobsStorage", secret.Value);

    await host.RunAsync().ConfigureAwait(false);
}

If there is a more intuitive way to accomplish this, please let me know. If not, the connection string design is plain silly.