AzureWebJobsStorage, the secret you don't need in your Function App.

Published on Wednesday, September 15, 2021

If you are using Azure Functions chances are you are using the setting AzureWebJobsStorage in your Function App configuration. And it is quite likely that the value of this setting which is a secret is stored in a non-secured way directly in your Function App configuration, available to anyone who has access to this configuration. But do not worry, we will see in this article how we can make your Function App more secure by removing this secret.

But first, let's start at the beginning.

What is this AzureWebJobsStorage setting?

As explained in the documentation, Azure Functions "rely on Azure Storage for operations such as managing triggers and logging function executions" which explains why you must associate a storage account to your Function App when you create one.

By default when you create a Function App with its storage account from Azure Portal, the setting AzureWebJobsStorage is automatically created in the Function App configuration and its value contains the secret connection string of the storage account. Thanks to that it will allow your Function App to have access to this storage and to work properly.

Why AzureWebJobsStorage poses a security risk?

App settings of your Function App are stored encrypted in Azure so having secrets in a Function App configuration in Azure does not seem a big security threat. Yet, secrets in Azure application settings will be available to anyone who has access to the configuration screen of your Function App (or to Kudu) which does not seem a great idea. Moreover in the application settings of a Function App, there is no proper access monitoring, alerting, and auditing as you would have in an Azure Key Vault. So your secret is not really "safe" there.

To avoid having someone gaining access to your storage account without you knowing, you probably do not want your storage account connection string to stay in a Function App configuration on Azure Portal.

What can we do about it?

A solution could be to store the AzureWebJobsStorage secret value in an Azure Key Vault and use a Key Vault reference to link the secret in Key Vault to the AzureWebJobsStorage setting like on the example below.

Another solution that is far more interesting I think (as it does not require any secret) is to assign the Storage Blob Data Owner role to your Function App identity and to replace the AzureWebJobsStorage connection string setting by the setting AzureWebJobsStorage__accountName that only contains the name of the storage account and no secret value at all.

If you want more details about connecting to the storage with the Function App identity you can find it here. There is no point for me to paraphrase the documentation just to explain how you can set this up. However, I can show you how to implement that using Infrastructure as Code.

🗨 If you have read my article about connecting to an Azure SQL Database using Azure AD to authenticate instead of a secret connection, you probably know that I am not a big fan of secrets when we can avoid using them. From a security perspective, I think it is always a gain to remove the need for secrets while ensuring a resource can only be accessed by authorized people/applications.

How to configure a Function App to work with its storage account without a secret connection string?

To do that, I will use Pulumi which is an Infrastructure as Code platform that uses programming languages instead of DSL to deploy infrastructure. As I am usually programming in C# for my application code, I will use C# as well for my infrastructure code.

What resources do we need to create?

We need to create 3 different Azure resources:

  • a consumption App Service Plan
  • a Function App
  • a Storage Account

A resource group will also be created to contain these resources. And we will also need to assign the Storage Blob Data Owner role to the Function App, so to create a Role Assignment "resource".

What the infrastructure code looks like?

The infrastructure code looks like standard C# code, but it describes the Azure resources we need using the Azure Native provider for Pulumi.

Declaring a resource group is quite easy. Here we use C# string interpolation to build the resource group name from the project name and the stack name (two Pulumi notions that correspond to the name of the project and the environment):

var resourceGroup = new ResourceGroup($"rg-{Deployment.Instance.ProjectName}-{Deployment.Instance.StackName}");

To declare the resource group in which we want to create the storage account, we can use the property name of variable resourceGroup previously declared. We can see that for some arguments like the SKU names, Pulumi has types to help us choose between different possible values instead of specifying a magic string. It is not always the case but it is pretty handy when such things are available.

var storageAccount = new StorageAccount($"stnosecretfun{Deployment.Instance.StackName}", new StorageAccountArgs
{
    ResourceGroupName = resourceGroup.Name,
    Sku = new SkuArgs
    {
        Name = SkuName.Standard_LRS
    },
    Kind = Kind.StorageV2
});

This is the way of declaring a consumption App Service Plan:

var appServicePlan = new AppServicePlan($"plan-{Deployment.Instance.ProjectName}-{Deployment.Instance.StackName}", new AppServicePlanArgs
{
    ResourceGroupName = resourceGroup.Name,
    Kind = "Windows",
    Sku = new SkuDescriptionArgs
    {
        Tier = "Dynamic",
        Name = "Y1"
    }
});

In Azure APIs, a FunctionApp is just a WebApp of a special kind "FunctionApp". You can notice that we enabled the System Managed Identity on the Function App by setting the Identity property. And as expected we added an app setting AzureWebJobsStorage__accountName whose value is the name of the storage account.

var functionApp = new WebApp($"func-nosecret-{Deployment.Instance.StackName}", new WebAppArgs
{
    Kind = "FunctionApp",
    ResourceGroupName = resourceGroup.Name,
    ServerFarmId = appServicePlan.Id,
    Identity = new ManagedServiceIdentityArgs
    {
        Type = Pulumi.AzureNative.Web.ManagedServiceIdentityType.SystemAssigned
    },
    SiteConfig = new SiteConfigArgs
    {
        AppSettings = new[]
        {
            new NameValuePairArgs
            {
                Name = "runtime",
                Value = "dotnet",
            },
            new NameValuePairArgs
            {
                Name = "FUNCTIONS_WORKER_RUNTIME",
                Value = "dotnet",
            },
            new NameValuePairArgs
            {
                Name = "FUNCTIONS_EXTENSION_VERSION",
                Value = "~4"
            },
            new NameValuePairArgs
            {
                Name = "AzureWebJobsStorage__accountName",
                Value = storageAccount.Name
            }
        },
    },
});

The last thing to do is to assign the role Storage Blob Data Owner to the Function App. To find the resource id I needed I looked at this page of the Microsoft documentation but I hope that in the future Pulumi will provide an enum or something like that with the possible values to make that easier to find and assign.

var storageBlobDataOwnerRole = new RoleAssignment("storageBlobDataOwner", new RoleAssignmentArgs
{
    PrincipalId = functionApp.Identity.Apply(i => i.PrincipalId),
    PrincipalType = PrincipalType.ServicePrincipal,
    RoleDefinitionId = "/providers/Microsoft.Authorization/roleDefinitions/b7e6dc6d-f1e8-4753-8033-0f276bb0955b",
    Scope = storageAccount.Id
});

🗨 Because the Azure provider is auto-generated from the Azure Resource Manager APIs, it is not always obvious to figure out what object and properties we should use to implement the infrastructure we want. Hopefully, because we are using a programming language in an IDE (C# in Visual Studio in my case), IntelliSense can help us.

And that's just it, with this code the Function App will go well without needing the secret connection string of the storage in the AzureWebJobsStorage setting. Everything works fine thanks to the Managed Identity, the assignment of the correct role, and the setting AzureWebJobsStorage__accountName. One thing we could do is to remove the AzureWebJobsStorage__accountName setting from the configuration to observe that without it, the Function App will not work properly: for instance, we would not be able to create function keys as a storage is needed to store them.

You can find all this code on this GitHub repository if you want to test it by yourself. You will also find an HttpTrigger Azure Function created from the templates in Visual Studio that I only used to have something deployed on my Function App.

How can we remove the AzureWebJobsStorage secret setting using Terraform?

I am a big fan of Pulumi's approach to build and deploy infrastructure but as Terraform is pretty popular for doing Infrastructure as Code, I thought it might be a good idea to explain how to solve the same issue using Terraform instead of Pulumi.

Well, in fact, you can't. 🤔 Currently there is no way to remove AzureWebJobsStorage secret using Terraform. Indeed as you can see in the Terraform documentation of a Function App resource azurerm_function_app, the AzureWebJobsStorage setting is automatically filled based on the storage_account_name and storage_account_access_key parameters which are required. So not only you can't use the Managed Identity access to storage as we did in Pulumi but also you can't use a key vault reference for the AzureWebJobsStorage setting (see the documentation screenshot below).

The only possibility right now is to use an ARM template in your Terraform project thanks to the azurerm_resource_group_template_deployment resource but that defeats the whole point of using Terraform especially for a major resource like a Function App.

In the GitHub repository of the Terraform provider for Azure RM there are 2 issues relative to this problem (do not hesitate to vote for them):

  • the 8977 issue that aims at supporting a KeyVault reference for the AzureWebJobsStorage setting
  • the 13240 issue that aims at supporting the Managed Identity access to storage

I guess this will be implemented someday in Terraform provider for Azure RM, so that may not be a big deal but that clearly shows the limitations of Terraform providers.

🗨 Because Terraform provider for Azure RM is manually implemented using the Azure SDK, it does not match Azure APIs hence not all new resources and features are available (they have to be implemented in the provider as new features in Azure are released and it can take time). It's something that is not a problem with Pulumi Azure Native provider as the SDKs are generated automatically from the Azure API specifications which makes Pulumi Azure Native provider always up-to-date.

Final thoughts

I hope that after reading this article if you are working on a Function App with an AzureWebJobsStorage setting you will take the time to replace it with an access to storage through the Managed Identity of your Function. One question you could ask me is why this is not the default behavior when creating a new Function App instead of using the AzureWebJobsStorage. And that would be an excellent question... for the Azure Functions team 😀.

If you did not know Pulumi before reading this article, I hope it made you want to give it a try. Happy learning.