Handle secrets in Azure functions configuration

Problem

T

here are moments when you must include connection strings in your Azure function, such as for a ServiceBusTrigger. While developing locally, local.settings.json serves as a proper configuration solution. However, upon deployment to Azure, it becomes necessary to use Azure Functions configuration. This scenario presents a challenge due to configurations being stored in plain text, thereby posing a significant security risk. Anyone with access to the Azure portal can view these secret values. The Azure team acknowledges this issue and there are several open GitHub issues addressing it as of the writing of this article. This has always been a concern for me, and just last week, I discovered a solution I would like to share with you.
Preferred architecture
The best way to keep secrets in Azure safe is by using Azure Key Vault. I like to keep things organized with Azure App Configuration, which offers a service-centric approach to manage application settings and feature flags. It simplifies the deployment process and keeps configurations separate from the code. Plus, it’s cool because you can link up secrets from the Key Vault. So, what I’m aiming to do is set up my function to grab its connection strings right from Azure App Configuration, which in turn pulls the real secrets from Azure Key Vault.

Here is the configuration in Azure app configuration:

Solution
Usually you connect to Azure app configuration at runtime and load requested values into your IConfiguration using ConfigurationBuilder. You can do the same in Azure functions, but you are not able to use these values in several triggers. They need to be embedded right at the function level. Luckily there is new feature that will allow us to reference the secret value. It is still in preview at the time of writing this article.
Now, let’s dive into the config values in our Azure App Configuration. We’re aiming to pull the ConnectionStringFromAppConfig first. To achieve it, we will use special syntax used for referencing value from App configuration.
@Microsoft.AppConfiguration(Endpoint=https://app-config-functions-secret-demo.azconfig.io; Key=ConnectionStringFromAppConfig)
  • Endpoint=endpoint; Endpoint is the required part of the reference string. The value for Endpoint should have the url of your App Configuration resource.
  • Key=keyName; Key forms the required part of the reference string. Value for Key should be the name of the Key that you want to assign to the App setting.
  • Label=label; The Label part is optional in reference string. Label should be the value of Label for the Key specified in Key.

We’ll use RBAC and managed identity to set up authorization for our function to access the configuration and secret. This involves adding the role of App configuration data reader to Azure app configuration and Key vault secret user to Key vault. This setup works perfectly for a System Assigned Managed Identity. However, for a User Assigned Managed Identity, we need to specify which identity the application should use. This is done by adjusting the function’s keyVaultReferenceIdentity property to the resource ID of the user-assigned identity.

userAssignedIdentityResourceId=$(az identity show -g functions-secret-config-demo -n function-identity --query id -o tsv)
az functionapp update --resource-group functions-secret-config-demo --name function-secrets-demo --set keyVaultReferenceIdentity=${userAssignedIdentityResourceId}
Conclusion

And there we have it – our values are now successfully loaded in the application, and we’ve securely moved our secrets to the ideal spot: Azure Key Vault. You’ll notice the Source column pointing to the App configuration reference. A green tick icon is your sign of success, showing that we’ve successfully retrieved the value.

Current configuration inside the application:
{
    "deployment_branch": "master",
    "SCM_TRACE_LEVEL": "Verbose",
    "SCM_COMMAND_IDLE_TIMEOUT": "60",
    "SCM_LOGSTREAM_TIMEOUT": "7200",
    "SCM_BUILD_ARGS": "",
    "FUNCTIONS_RUNTIME_SCALE_MONITORING_ENABLED": "0",
    "ScmType": "None",
    "AzureWebJobsStorage": "DefaultEndpointsProtocol=https;AccountName=functionssecretconf881a;AccountKey=someky;EndpointSuffix=core.windows.net",
    "CONNECTION_STRING__KEY_VAULT": "super secret connection string",
    "SCM_USE_LIBGIT2SHARP_REPOSITORY": "0",
    "CONNECTION_STRING__APP_CONFIG": "my connections string taken from app onfiguration",
    "WEBSITE_SLOT_NAME": "Production",
    "WEBSITE_AUTH_LOGOUT_PATH": "/.auth/logout",
    "WEBSITE_AUTH_AUTO_AAD": "False",
    "REMOTEDEBUGGINGVERSION": "16.0.33328.57",
    "FUNCTIONS_EXTENSION_VERSION": "~4",
    "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
    "WEBSITE_SITE_NAME": "function-secrets-demo",
    "WEBSITE_AUTH_ENABLED": "False",
    "WEBSITE_USE_PLACEHOLDER_DOTNETISOLATED": "1",
    "CONNECTION_STRING__PLAINTEXT": "my plaintext connection string"
}