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:
Then I created Azure Web App and created Identity for it:
Later in Azure Key Vault Access Policies I granted this App Get and List secret permission.
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());
});
}
}
}
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 customer – Thomas