I'm building Azure Function in python triggered with Event Grid events, which should be able to gather secret from Kay Vault.
I added system-assigned managed identity to my Function App, and then I was able to pick my App in Key Vault access policies. I gave it permissions like below:
(I was trying different combinations at this one)
Also I provided new app setting with reference to mentioned key vault.
Unfortunetly when i try to check this value from code I'm unable to get this.
logging.info(os.environ)
When I add another app setting, just with plaintext it works great.
I will be grateful for any ideas what else can be done.
After hours of tests I found proper way to resolve this one.
In case of problems with Key Vault Reference make sure that App Function used for Azure Function is based on proper Hosting Plan.
Functions on 'Consumption Plan' are unaable to use Key Vault Reference. Same code on 'App Service Plan' works correctly.
https://learn.microsoft.com/en-us/azure/app-service/app-service-key-vault-references
I couldn't figure out what you want to get with os.environ. I test with function It could work for me.
In function to retrieve key-valut if you already set it in appsettings, you could use Environment.GetEnvironmentVariable("secrest name", EnvironmentVariableTarget.Process) to implement it.
You can use the following helper to get the values
namespace AccessKeyVault
{
public static class GetKeyVaultValues
{
[FunctionName("GetKeyVaultValues")]
public static async Task<HttpResponseMessage> Run([HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)]HttpRequestMessage req, TraceWriter log)
{
log.Info("C# HTTP trigger function processed a request.");
string linkKeyVaultUrl = $"https://keyVaultname.vault.azure.net/secrets/";
string keyvaultKey = $"KeyVaultKey";
var secretURL = linkKeyVaultUrl + keyvaultKey;
//Get token from managed service principal
var azureServiceTokenProvider = new AzureServiceTokenProvider();
var kvClient = new KeyVaultClient(new KeyVaultClient.AuthenticationCallback(azureServiceTokenProvider.KeyVaultTokenCallback));
try
{
var clientIdRecord = await kvClient.GetSecretAsync(secretURL).ConfigureAwait(false);
string KeyvaultValue = clientIdRecord.Value;
return req.CreateErrorResponse(HttpStatusCode.OK, "Key vault secret value is : " + KeyvaultValue);
}
catch (System.Exception ex)
{
return req.CreateResponse(HttpStatusCode.BadRequest, "Key vault value request is not successfull");
}
}
}
}
Related
I have a simple azure function (Authorization level set to Anonymous) with an HTTP trigger
public static async Task<IActionResult> Run(HttpRequest req, ILogger log)
{
return new OkObjectResult("This is a response from secured function.");
}
I want to secure it with Managed Identities, so I turned on system-assigned identity
And in my AD enterprise app registration, I can see created a system-assigned identity so I copied its Application ID value
and for testing purposes, I want to trigger it from another azure function
public static async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req,
ILogger log)
{
var clientID = {application / client ID of system identity}
var azureServiceTokenProvider = new AzureServiceTokenProvider();
var accessToken = await azureServiceTokenProvider.GetAccessTokenAsync(clientID);
// Call secured azure function
using (var client = new HttpClient())
{
client.BaseAddress = new Uri("https://my-secured-function.azurewebsites.net");
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
var response = await client.GetAsync("/api/HttpTrigger1");
if (response.IsSuccessStatusCode)
{
var result = await response.Content.ReadAsStringAsync();
return new OkObjectResult(result);
}
else
{
return new OkObjectResult("Error - " + response.StatusCode);
}
}
}
The code works, it generates & sends a token within the HTTP request. However, the "secured" azure function is still publicly available.
The question is how can I protect the "secured" azure function, so it can be triggered only with an HTTP request with a generated token using managed identities.
The System Assigned Managed Identity you enabled here only gives an identity to your app, through a Service Principal. This is not blocking any access unlike the Firewall/IP Restrictions (that you could use but I assume that you want to rely on Identity only here).
What you are looking for is basically Authentication with Azure AD. From there, you could use the Managed Identity (if this is suitable) of the caller to authenticate against your app through the AAD. That also works with any Service Principal or users.
You need to click "Authentication" on the left panel of your Function app. Then add Microsoft AD as an identity provider.
Add the necessary settings(you can let Azure create an App registration or use the managed identity which you have already created. With this step, you will lock your Azure function app so it is triggered only if a valid AD token is provided to it.
When I inject IConfiguration in a function, it does not find any keys that only live in my "Azure App Configuration".
I have a functionApp (V3) that accesses App Configuration using the DefaultAzureCredential. I am running this locally in debug hence the need for a default credential. I also have multiple Tenants so I had to set the VisualStudioTenantId and SharedTokenCacheTenantId on DefaultAzureCredentialOptions. My Visual studio user was also given the role "App Configuration Data Reader" to be able to debug.
When connecting to App configuration I get no errors.
Editedto add: I have setup AppConfiguration to authenticate with AzureAD.
See code below:
public override async void ConfigureAppConfiguration(IFunctionsConfigurationBuilder builder)
{
var credOptions = new DefaultAzureCredentialOptions();
var tenantId = Environment.GetEnvironmentVariable("Tenant_Id");
credOptions.VisualStudioTenantId = tenantId;
credOptions.SharedTokenCacheTenantId = tenantId;
var cred = new DefaultAzureCredential(credOptions);
/*Works but requires SharedTokenCacheTenantId*/
var secretClient = new SecretClient(new Uri(vaultURI), cred);
var secret = await secretClient.GetSecretAsync("<secret name>");
/*Works but where are my keys when I try to access them?*/
builder.ConfigurationBuilder.AddAzureAppConfiguration(options =>
{
options.Connect(new Uri(appConfigURI), cred);
}).Build(); //Should I be building this??
}
In my function
public FunctName(IConfiguration configuration)
{
_configuration = configuration;
}
And when I access the property
var prop = _configuration["PropertyName"];
There is an example function app that uses IFunctionsConfigurationBuilder here https://github.com/Azure/AppConfiguration/blob/main/examples/DotNetCore/AzureFunction/FunctionApp/Startup.cs . I would recommend taking a look and seeing if there are any missing pieces.
The title mentions "using DefaultAzureCredential on local". Does that mean that this works as expected if you use a connection string?
Notice the async void ConfigureAppConfiguration. This caused my ConfigureAppConfiguration to not execute synchronously, causing configure to add my App Configuration before it was populated.
I have a Azure SQL Db with encrypted columns (Always Encrypted with Azure KeyVault). I can access this db from SSMS and I can see the decrypted data.
I also have a web app made with .Net Core 5.0 which is deployed to Azure App Service. The app service has Managed Identity turned on and Key Vault that has enc/dec keys for that SQL Db has access policy setting to permit this app service to decrypt the data.
The web app works with managed identity as I can see that not encrypted data is retrieved without any issue.
Also, connection string does include Column Encryption Setting=enabled;. Here's the connection string:
Server=tcp:server.database.windows.net,1433;Database=somedb;Column Encryption Setting=enabled;
The problem is I can't find ANY samples with this kind of set up. I found some and I understand I need to register SqlColumnEncryptionAzureKeyVaultProvider. Here's my code to obtain SqlConnection:
internal static class AzureSqlConnection
{
private static bool _isInitialized;
private static void InitKeyVaultProvider(ILogger logger)
{
/*
* from here - https://github.com/dotnet/SqlClient/blob/master/release-notes/add-ons/AzureKeyVaultProvider/1.2/1.2.0.md
* and - https://github.com/dotnet/SqlClient/blob/master/doc/samples/AzureKeyVaultProviderExample.cs
*
*/
try
{
// Initialize AKV provider
SqlColumnEncryptionAzureKeyVaultProvider sqlColumnEncryptionAzureKeyVaultProvider =
new SqlColumnEncryptionAzureKeyVaultProvider(AzureActiveDirectoryAuthenticationCallback);
// Register AKV provider
SqlConnection.RegisterColumnEncryptionKeyStoreProviders(
new Dictionary<string, SqlColumnEncryptionKeyStoreProvider>(1, StringComparer.OrdinalIgnoreCase)
{
{SqlColumnEncryptionAzureKeyVaultProvider.ProviderName, sqlColumnEncryptionAzureKeyVaultProvider}
});
_isInitialized = true;
}
catch (Exception ex)
{
logger.LogError(ex, "Could not register SqlColumnEncryptionAzureKeyVaultProvider");
throw;
}
}
internal static async Task<SqlConnection> GetSqlConnection(string connectionString, ILogger logger)
{
if (!_isInitialized) InitKeyVaultProvider(logger);
try
{
SqlConnection conn = new SqlConnection(connectionString);
/*
* This is Managed Identity (not Always Encrypted)
* https://learn.microsoft.com/en-us/azure/app-service/app-service-web-tutorial-connect-msi#modify-aspnet-core
*
*/
#if !DEBUG
conn.AccessToken = await new AzureServiceTokenProvider().GetAccessTokenAsync("https://database.windows.net/");
logger.LogInformation($"token: {conn.AccessToken}");
#endif
await conn.OpenAsync();
return conn;
}
catch (Exception ex)
{
logger.LogError(ex, "Could not establish a connection to SQL Server");
throw;
}
}
private static async Task<string> AzureActiveDirectoryAuthenticationCallback(string authority, string resource, string scope)
{
return await new AzureServiceTokenProvider().GetAccessTokenAsync("https://database.windows.net/");
//AuthenticationContext? authContext = new AuthenticationContext(authority);
//ClientCredential clientCred = new ClientCredential(s_clientId, s_clientSecret);
//AuthenticationResult result = await authContext.AcquireTokenAsync(resource, clientCred);
//if (result == null)
//{
// throw new InvalidOperationException($"Failed to retrieve an access token for {resource}");
//}
//return result.AccessToken;
}
}
This code does not throw any exceptions, and it works for non-encrypted queries. But for encrypted queries I get the following error:
Failed to decrypt a column encryption key. Invalid key store provider name: 'AZURE_KEY_VAULT'. A key store provider name must denote either a system key store provider or a registered custom key store provider. Valid system key store provider names are: 'MSSQL_CERTIFICATE_STORE', 'MSSQL_CNG_STORE', 'MSSQL_CSP_PROVIDER'. Valid (currently registered) custom key store provider names are: . Please verify key store provider information in column master key definitions in the database, and verify all custom key store providers used in your application are registered properly. Failed to decrypt a column encryption key. Invalid key store provider name: 'AZURE_KEY_VAULT'. A key store provider name must denote either a system key store provider or a registered custom key store provider. Valid system key store provider names are: 'MSSQL_CERTIFICATE_STORE', 'MSSQL_CNG_STORE', 'MSSQL_CSP_PROVIDER'. Valid (currently registered) custom key store provider names are: . Please verify key store provider information in column master key definitions in the database, and verify all custom key store providers used in your application are registered properly.
It seems like the key vault provider is not registered.
What should I do to make it work to query encrypted data?
packages used
<PackageReference Include="Microsoft.Azure.Services.AppAuthentication" Version="1.6.0" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="2.1.0" />
<PackageReference Include="Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider" Version="1.2.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="5.0.0" />
I was able to use this code that uses provides a TokenCredential to the SqlColumnEncryption provider. DefaultAzureCredential returns a managed identity when deployed as an App Service:
SqlColumnEncryptionAzureKeyVaultProvider azureKeyVaultProvider = new SqlColumnEncryptionAzureKeyVaultProvider(new DefaultAzureCredential());
Dictionary<string, SqlColumnEncryptionKeyStoreProvider> providers = new Dictionary<string, SqlColumnEncryptionKeyStoreProvider>
{
{ SqlColumnEncryptionAzureKeyVaultProvider.ProviderName, azureKeyVaultProvider }
};
SqlConnection.RegisterColumnEncryptionKeyStoreProviders(providers);
Call it from your startup.Configure method.
Turns out, it is impossible to read decrypted data in .NET 5 when MSI is used. There is a bug in MS packages and the app service is never authorized.
You have to use Service Principal. This works like a charm!
Update
I have to thank to MS engineers that offered a working solution:
public static async Task<string> KeyVaultAuthenticationCallback(string authority, string resource, string scope)
{
return await Task.Run(() => new ManagedIdentityCredential().GetToken(new TokenRequestContext(new string [] {"https://vault.azure.net/.default"})).Token);
/********************** Alternatively, to use User Assigned Managed Identity ****************/
// var clientId = {clientId_of_UserAssigned_Identity};
// return await Task.Run(() => new ManagedIdentityCredential(clientId).GetToken(new TokenRequestContext(new string [] {"https://vault.azure.net/.default"})).Token);
}
It's not a problem with .NET 5. You've taken the example authentication callback for Azure Key Vault and changed it to be specific to Azure SQL DB instead of the AKV resource. You need to adjust your callback to get a valid AKV token. This is just one way to get a token using the Azure.Core and Azure.Identity libraries:
private static async Task<string> AzureActiveDirectoryAuthenticationCallback(string authority, string resource, string scope)
{
return await Task.Run(() => new ManagedIdentityCredential().GetToken(new TokenRequestContext(new string [] {"https://vault.azure.net/.default"})).Token);
}
Answer for Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider v3
SqlColumnEncryptionAzureKeyVaultProvider azureKeyVaultProvider = new(new ManagedIdentityCredential());
Dictionary<string, SqlColumnEncryptionKeyStoreProvider> providers = new()
{
[SqlColumnEncryptionAzureKeyVaultProvider.ProviderName] = azureKeyVaultProvider
};
SqlConnection.RegisterColumnEncryptionKeyStoreProviders(providers);
Taken from microsoft/sql-server-samples after almost losing my mind.
Apart from that, it's required to:
Assign app service managed identity Key Vault Crypto User role in Key Vault's IAM tab.
Grant app service managed identity DB user VIEW ANY COLUMN MASTER KEY DEFINITION and VIEW ANY COLUMN ENCRYPTION KEY DEFINITION permissions:
GRANT VIEW ANY COLUMN MASTER KEY DEFINITION to [app-service-user]
GRANT VIEW ANY COLUMN ENCRYPTION KEY DEFINITION to [app-service-user]
Allow app service access through Key Vault's firewall (i.e. adding app service outbound IPs to Key Vault's firewall rules).
I am attempting to create an Azure Function using .NET Core to call to the YouTube API to retrieve some metrics on my videos.
Before calling the API I need to Authenticate with Google in a server to server method since this function will run daily with NO user interaction.
I've followed a number of examples (https://developers.google.com/api-client-library/dotnet/guide/aaa_oauth) and I'm having no luck getting properly authenticated when running from Azure.
Is this possible? And can anyone point me to an example of this working?
For server-to-server interactions you need a service account, which is an account that belongs to your application instead of to an individual end-user. Your application calls Google APIs on behalf of the service account, and user consent is not required.
public class Program
{
// A known public activity.
private static String ACTIVITY_ID = "z12gtjhq3qn2xxl2o224exwiqruvtda0i";
public static void Main(string[] args)
{
Console.WriteLine("Plus API - Service Account");
Console.WriteLine("==========================");
String serviceAccountEmail = "SERVICE_ACCOUNT_EMAIL_HERE";
var certificate = new X509Certificate2(#"key.p12", "notasecret", X509KeyStorageFlags.Exportable);
ServiceAccountCredential credential = new ServiceAccountCredential(new ServiceAccountCredential.Initializer(serviceAccountEmail)
{
Scopes = new[] { PlusService.Scope.PlusMe }
}.FromCertificate(certificate));
// Create the service.
var service = new PlusService(new BaseClientService.Initializer()
{
HttpClientInitializer = credential,
ApplicationName = "Plus API Sample",
});
Activity activity = service.Activities.Get(ACTIVITY_ID).Execute();
Console.WriteLine(" Activity: " + activity.Object.Content);
Console.WriteLine(" Video: " + activity.Object.Attachments[0].Url);
Console.WriteLine("Press any key to continue...");
Console.ReadKey();
}
}
The above sample code creates a ServiceAccountCredential. The required scopes are set and there is a call to FromCertificate, which loads the private key from the given X509Certificate2. As in all other samples code, the credential is set as HttpClientInitializer.
For more details about service account flow you could refer to this article.
I need to get access to Key Vault and Service Bus from code, using a Service Principle for authentication.
I can use the following code to access Service Bus, which works as expected - when I enable to Service Principle in the Access Policies I can pull the list of topics:
var credentials = SdkContext.AzureCredentialsFactory.FromServicePrincipal(APPID, APPSECRET, TENANTID, AzureEnvironment.AzureGlobalCloud);
var serviceBusManager = ServiceBusManager.Authenticate(credentials, SUBSCRIPTIONID);
var serviceBusNamespace = serviceBusManager.Namespaces.List().SingleOrDefault(n => n.Name == "SERVICEBUSNAMESPACE");
var topics = serviceBusNamespace.Topics.ListAsync().GetAwaiter().GetResult();
However, I also need to get some information from Key Vault and I was trying to establish a common way to authenticate.
METHOD 1
Similar to the above, I tried this code to access KeyVault:
var credentials = SdkContext.AzureCredentialsFactory.FromServicePrincipal(APPID, APPSECRET, TENANTID, AzureEnvironment.AzureGlobalCloud);
var kvManager = new KeyVaultClient(credentials);
var secret = kvManager.GetSecretAsync("https://VAULTNAMESPACE.vault.azure.net", "SECRETNAME").GetAwaiter().GetResult().Value;
I get the the following error:
Microsoft.Azure.KeyVault.Models.KeyVaultErrorException: 'Operation
returned an invalid status code 'Unauthorized''
METHOD 2
This code does work for Key Vault however (showing I have correct permissions):
string GetSecret()
{
var client = new KeyVaultClient(GetAccessToken);
var secret = client.GetSecretAsync("https://VAULTNAMESPACE.vault.azure.net", "SECRETNAME").GetAwaiter().GetResult();
return secret;
}
private static async Task<string> GetAccessToken(string authority, string resource, string scope)
{
var context = new AuthenticationContext("https://login.windows.net/" + tenantId);
var credential = new ClientCredential(appId, appSecret);
var tokenResult = await context.AcquireTokenAsync("https://vault.azure.net", credential);
return tokenResult.AccessToken;
}
But, again, it's a very KeyVault specific way to Authenticate and I was hoping to establish a common mechanism using SdkContext.AzureCredentialsFactory. Any reason why I'd be getting an Unauthorized exception with the code above connecting to Key Vault? (all is set up correctly in Azure).
Thanks for any tips!
When you use SdkContext.AzureCredentialsFactory.FromServicePrincipal to authenticate, it will use https://management.azure.com/ as its Resource Uri.
While Azure Key Vault has its own authorization system and its Resource Uri is https://vault.azure.net, so you may get the Unauthorized error message.
So, you could use Method2 to get access to Azure Key Vault with right Resource Uri.
For more details, you could refer to this article.