Net Core 6 Minimal Hosting Model --> How to get Azure KeyVault key inside ConfigureServices

1.2k Views Asked by At

I have a .NET Core 6 Worker App which I've configured to use Azure KayVault (newbie to KeyVault)

When registering services, I need to get a key from the vault but I'm finding that the connection to the keyVault service has not yet started, so a typical chicken aand egg situation.

How can I access a key when registering a service in ConfigureServices?

The code sample below shows I need to grab the connection string for using LiteDB, but I will have other use cases that requires a similar solution:

IHost host = Host.CreateDefaultBuilder(args)
.ConfigureServices(services =>
{
    var configuration = new ConfigurationBuilder()
            .AddJsonFile("appsettings.json")
            .Build();

    // See notes in appsettings.josn file.
    // See https://learn.microsoft.com/en-us/azure/azure-monitor/app/worker-service
    var aiOptions = new ApplicationInsightsServiceOptions();
    aiOptions.ConnectionString = configuration["APPINSIGHTS_CONNECTIONSTRING"];
    aiOptions.EnableQuickPulseMetricStream = configuration.GetSection("ApplicationInsights").GetValue<bool>("EnableQuickPulseMetricStream");
    aiOptions.EnableEventCounterCollectionModule = configuration.GetSection("ApplicationInsights").GetValue<bool>("EnableEventCounterCollectionModule");
    aiOptions.EnableAppServicesHeartbeatTelemetryModule = configuration.GetSection("ApplicationInsights").GetValue<bool>("EnableAppServicesHeartbeatTelemetryModule");
    aiOptions.EnableAzureInstanceMetadataTelemetryModule = configuration.GetSection("ApplicationInsights").GetValue<bool>("EnableAzureInstanceMetadataTelemetryModule");
    aiOptions.EnableDependencyTrackingTelemetryModule = configuration.GetSection("ApplicationInsights").GetValue<bool>("EnableDependencyTrackingTelemetryModule");
    aiOptions.EnableEventCounterCollectionModule = configuration.GetSection("ApplicationInsights").GetValue<bool>("EnableEventCounterCollectionModule");
    aiOptions.EnableAdaptiveSampling = configuration.GetSection("ApplicationInsights").GetValue<bool>("EnableAdaptiveSampling");
    aiOptions.EnableHeartbeat = configuration.GetSection("ApplicationInsights").GetValue<bool>("EnableHeartbeat");
    aiOptions.AddAutoCollectedMetricExtractor = configuration.GetSection("ApplicationInsights").GetValue<bool>("AddAutoCollectedMetricExtractor");

    services.AddApplicationInsightsTelemetryWorkerService(aiOptions);

    // ------------------------
    // ----- Lite DB START-----
    // ------------------------

    // *This doesnt work becuase the keyvault client hasnt started up yet!!!*
    var connectionString = configuration.GetSection("LiteDB").GetValue<string>("ConnectionString");

    // HOW DO I GET THE KEY VAUT KEY HERE???
    services.AddSingleton<ILiteDatabase, LiteDatabase>(x => new LiteDatabase(connectionString));

    // -----------------------
    // ----- Lite DB END-----
    // -----------------------

    // Repository used for our own logging events throughout the business logic code base.
    services.AddTransient<ILogExtension, LogExtension>();
    // Add the Background Services
    services.AddHostedService<AzureSignalRService>();
})
.ConfigureAppConfiguration((context, config) => // Azure KeyVault Configuration
{
    if (context.HostingEnvironment.IsDevelopment() | context.HostingEnvironment.IsProduction())
    {
        // See https://learn.microsoft.com/en-us/aspnet/core/security/key-vault-configuration?view=aspnetcore-6.0
        // See https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/key-vault-configuration/samples/3.x/SampleApp/Startup.cs
        var root = config.Build();
        using var x509Store = new X509Store(StoreLocation.CurrentUser);
        x509Store.Open(OpenFlags.ReadOnly);

        var x509Certificate = x509Store.Certificates
        .Find(
            X509FindType.FindByThumbprint,
            root["AzureADCertThumbprint"],
            validOnly: false)
        .OfType<X509Certificate2>()
        .Single();

        config.AddAzureKeyVault(
        new Uri($"https://{root["KeyVaultName"]}.vault.azure.net/"),
        new ClientCertificateCredential(
            root["AzureADDirectoryId"],
            root["AzureADApplicationId"],
            x509Certificate));
    }
})
.Build();
2

There are 2 best solutions below

0
On

ConfigureAppConfiguration runs before ConfigureServices so when you configure your services, the configuration is already loaded. Logically iy should looks like that:

IHost host = Host.CreateDefaultBuilder(args)    
.ConfigureAppConfiguration((context, config) =>
{
  config.AddAzureKeyVault(...);
})
.ConfigureServices(services =>
{
  ... 
})
.Build();

In your ConfigureServices method, you are recreating the configuration only including appsettings.json file so it will never get the secrets from key vault configuration. There is another overload that accept the HostbuilderContext

.ConfigureServices((context, services) =>
{
  // Get the configuration
  var configuration = context.Configuration;
  ...
})
1
On

The way around this was to create a service collection extension class and point to this within Program.cs, rather then registering the service within ConfigureServices.

Any code that requires resolving KeyVault keys when fetching the properties from appsettings.json is now working. It seems pot luck for me that this works, I can only assume that the service extension somehow knows to go back and get any missing keys after the first stages of startup.

Program.cs

IHost host = Host.CreateDefaultBuilder(args)
.ConfigureServices(services =>
{
    var configuration = new ConfigurationBuilder()
            .AddJsonFile("appsettings.json")
            .Build();

    // ------------------------
    // ----- Lite DB START-----
    // ------------------------

    // ----- BAD -----
    // *This doesnt work becuase the keyvault client hasnt started up yet!!!*
    var connectionString = configuration.GetSection("LiteDB").GetValue<string>("Password");
    // Simply returns the native value in app settings, rather than the actual key value stored in Azure KeyVault
    Console.WriteLine(connectionString); 
    // ---------------------------------

    // ----- Solution -----
    // Add the IService Collection Extension, see seperate class further down...
    services.AddDatabase();

    // -----------------------
    // ----- Lite DB END-----
    // -----------------------

    // Repository used for our own logging events throughout the business logic code base.
    services.AddTransient<ILogExtension, LogExtension>();
    // Add the Background Services
    services.AddHostedService<AzureSignalRService>();
})
.ConfigureAppConfiguration((context, config) => // Azure KeyVault Configuration
{
    if (context.HostingEnvironment.IsDevelopment() | context.HostingEnvironment.IsProduction())
    {
        // See https://learn.microsoft.com/en-us/aspnet/core/security/key-vault-configuration?view=aspnetcore-6.0
        // See https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/key-vault-configuration/samples/3.x/SampleApp/Startup.cs
        var root = config.Build();
        using var x509Store = new X509Store(StoreLocation.CurrentUser);
        x509Store.Open(OpenFlags.ReadOnly);

        var x509Certificate = x509Store.Certificates
        .Find(
            X509FindType.FindByThumbprint,
            root["AzureADCertThumbprint"],
            validOnly: false)
        .OfType<X509Certificate2>()
        .Single();

        config.AddAzureKeyVault(
        new Uri($"https://{root["KeyVaultName"]}.vault.azure.net/"),
        new ClientCertificateCredential(
            root["AzureADDirectoryId"],
            root["AzureADApplicationId"],
            x509Certificate));
    }
})
.Build();

Seperate IServiceCollection Class:

internal static class ServiceCollectionDatabaseExtensions
{
    public static IServiceCollection AddDatabase(this IServiceCollection services)
    {

        var config = services.BuildServiceProvider().GetService<IConfiguration>();
        var password = config!.GetSection("LiteDB").GetValue<string>("Password");
        Console.WriteLine(password); // This now returns the correct key vaue pulled from Azure Key Vault

        var connectionString = "Filename=C:\'database.db;Connection=shared;Password=" + password;
        Console.WriteLine(connectionString);
        
        services.AddSingleton<ILiteDatabase, LiteDatabase>(x => new LiteDatabase(connectionString));

        // Other services here...

        return services;
    }
}