0
votes

TL;DR: Without List permission Asp.Net Core 2.2 Web App deployed in Azure fails with Error - Microsoft.Azure.KeyVault.Models.KeyVaultErrorException: Operation returned an invalid status code 'Forbidden' at startup when using AzureServiceTokenProvider

I am working on Asp.Net Core 2.2 Web App and I am aware how Azure Key Vault works and how Web App deployed in Azure can access Keys, Secrets and Certificates from key valut.

Below is my current configuration:

I have created an Azure Key Vault to store subscription information of all my clients:

Azure Key Vault with multiple secrets

Then I created Azure Web App and created Identity for it:

Web app with Identity created

Later in Azure Key Vault Access Policies I granted this App Get and List secret permission.

Azure Secret Access Policy

I didn't want to hard code any secrets in my code thus I used AzureServiceTokenProvider to connect and get the secrets, below is my Program.cs file code:

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Azure.KeyVault;
using Microsoft.Azure.Services.AppAuthentication;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Configuration.AzureKeyVault;
using Microsoft.Extensions.Logging;
using NLog.Common;
using NLog.Web;

namespace AzureSecretsTest
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var logger = NLogBuilder.ConfigureNLog("nlog.config").GetCurrentClassLogger();
            try
            {
                InternalLogger.LogFile = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "logs", "nlog-internals.txt");
                var host = CreateWebHostBuilder(args).Build();
                host.Run();
            }
            catch (Exception ex)
            {
                logger.Error(ex, "Stopped program because of exception");
                throw;
            }
            finally
            {
                // Ensure to flush and stop internal timers/threads before application-exit (Avoid segmentation fault on Linux)
                NLog.LogManager.Shutdown();
            }
        }

        public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
                .ConfigureAppConfiguration((ctx, builder) =>
                {
                    //https://anthonychu.ca/post/secrets-aspnet-core-key-vault-msi/
                    var keyVaultEndpoint = Environment.GetEnvironmentVariable("KEYVAULT_ENDPOINT");
                    if (!string.IsNullOrEmpty(keyVaultEndpoint))
                    {
                        var azureServiceTokenProvider = new AzureServiceTokenProvider();
                        var keyVaultClient = new KeyVaultClient(new KeyVaultClient.AuthenticationCallback(azureServiceTokenProvider.KeyVaultTokenCallback));
                        builder.AddAzureKeyVault(keyVaultEndpoint, keyVaultClient, new DefaultKeyVaultSecretManager());
                    }
                })
                .UseStartup<Startup>();
    }
}

And below is my simple Startup.cs file with code to access the Secrets from Azure Key Vault:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace AzureSecretsTest
{
    public class Startup
    {
        // This method gets called by the runtime. Use this method to add services to the container.
        // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
        public void ConfigureServices(IServiceCollection services)
        {
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env, IConfiguration configuration)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.Run(async (context) =>
            {
                string configValue = configuration["OneOfTheSecretKey"];
                StringBuilder sb = new StringBuilder();
                var children = configuration.GetChildren();
                sb.AppendLine($"Is null or whitespace: {string.IsNullOrWhiteSpace(configValue)}, Value: '{configValue}'");
                foreach (IConfigurationSection item in children)
                {
                    sb.AppendLine($"Key: {item.Key}, Value: {item.Value}");
                }
                await context.Response.WriteAsync(sb.ToString());
            });
        }
    }
}

Everything works fine as long as I have granted "List" permission. However by granting "List" permission I noticed that entire Secret list can be accessed. This exposes all other client subscription info which I am not too happy about. I can go and create one Key Vault per each client but that seems to be overkill.

It is possible that I am making a silly mistake and I am not seeing it or it is quite possible that you can't remove "List" permission. Either way I would appreciate if someone with more knowledge can shed light on whether can I use AzureServiceTokenProvider without granting List permission or not?

Update: 1

Found out that there is already issue logged in GitHub for this: Handle No List Permission for Secrets and Azure Key Vault with no List permissions on Secrets fails

Update: 2 Based on Joey's answer this is the final working code:

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Azure.KeyVault;
using Microsoft.Azure.Services.AppAuthentication;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Configuration.AzureKeyVault;
using Microsoft.Extensions.Logging;
using NLog.Common;
using NLog.Web;

namespace AzureSecretsTest
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var logger = NLogBuilder.ConfigureNLog("nlog.config").GetCurrentClassLogger();
            try
            {
                InternalLogger.LogFile = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "logs", "nlog-internals.txt");
                var host = CreateWebHostBuilder(args).Build();
                host.Run();
            }
            catch (Exception ex)
            {
                logger.Error(ex, "Stopped program because of exception");
                throw;
            }
            finally
            {
                // Ensure to flush and stop internal timers/threads before application-exit (Avoid segmentation fault on Linux)
                NLog.LogManager.Shutdown();
            }
        }

        public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
                .ConfigureAppConfiguration((ctx, builder) =>
                {
                    //https://anthonychu.ca/post/secrets-aspnet-core-key-vault-msi/
                    //var keyVaultEndpoint = Environment.GetEnvironmentVariable("KEYVAULT_ENDPOINT");
                    //if (!string.IsNullOrEmpty(keyVaultEndpoint))
                    //{
                    //    var azureServiceTokenProvider = new AzureServiceTokenProvider();
                    //    var keyVaultClient = new KeyVaultClient(new KeyVaultClient.AuthenticationCallback(azureServiceTokenProvider.KeyVaultTokenCallback));
                    //    keyVaultClient.GetSecretAsync(keyVaultEndpoint, "").GetAwaiter().GetResult();
                    //    //builder.AddAzureKeyVault(keyVaultEndpoint, keyVaultClient, new DefaultKeyVaultSecretManager());
                    //}
                })
                .UseStartup<Startup>();
    }
}

And

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Azure.KeyVault;
using Microsoft.Azure.Services.AppAuthentication;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace AzureSecretsTest
{
    public class Startup
    {
        // This method gets called by the runtime. Use this method to add services to the container.
        // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
        public void ConfigureServices(IServiceCollection services)
        {
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env, IConfiguration configuration)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.Run(async (context) =>
            {
                var keyVaultEndpoint = Environment.GetEnvironmentVariable("KEYVAULT_ENDPOINT");
                StringBuilder sb = new StringBuilder();
                if (!string.IsNullOrEmpty(keyVaultEndpoint))
                {
                    var azureServiceTokenProvider = new AzureServiceTokenProvider();
                    var keyVaultClient = new KeyVaultClient(new KeyVaultClient.AuthenticationCallback(azureServiceTokenProvider.KeyVaultTokenCallback));
                    var secret = await keyVaultClient.GetSecretAsync(keyVaultEndpoint, "OneOfTheSecretKey");
                    sb.AppendLine($"Is null or whitespace: {string.IsNullOrWhiteSpace(secret.Value)}, Value: '{secret.Value}'");
                }

                //string configValue = configuration["OneOfTheSecretKey"];
                //var children = configuration.GetChildren();

                //sb.AppendLine($"Is null or whitespace: {string.IsNullOrWhiteSpace(configValue)}, Value: '{configValue}'");

                //foreach (IConfigurationSection item in children)
                //{
                //    sb.AppendLine($"Key: {item.Key}, Value: {item.Value}");
                //}
                await context.Response.WriteAsync(sb.ToString());
            });
        }
    }
}
1
The problem here is that using the AddAzureKeyVault extension load all your secrets from key vault to make it available in your app. You can either retrieve manually the secret or have a key vault per customerThomas

1 Answers

1
votes

As Thomas said, when you use AddAzureKeyVault extension to add the KeyVaultProvider. At this point, the middleware has enough information to go and pull all the KeyVault data. We can immediately start pulling secret values using the Configuration API.

So, if you want to get the specific secret to keep security, you could use the following code.

var azureServiceTokenProvider = new AzureServiceTokenProvider();

var keyVaultClient = new KeyVaultClient(new KeyVaultClient.AuthenticationCallback(azureServiceTokenProvider.KeyVaultTokenCallback));

var scret = keyVaultClient.GetSecretAsync(keyvaultEndpoint, SecretName).GetAwaiter().GetResult();