Start to finish - Secure function app in Azure

I'm creating an application that will run using the Azure Functions runtime hosted in an Azure Web app. The application will call a remote API, which is secured by a API token and will use Azure Blob storage, which is secured using an account key that is part of a connection string.

This application will use a Key Vault to store the API token and the blob storage connection string. Listed below is a summary of an ARM template that can create the secure infrastructure for the code.

{
  "resources": [
  {
    "type": "Microsoft.Storage/storageAccounts",
    "apiVersion": "2019-04-01",
    "name": "[variables('appInsightsName')]"
  },
  {
    "type": "Microsoft.Web/serverfarms",
    "apiVersion": "2016-09-01",
    "name": "[variables('hostingPlanName')]"
  },
  {
    "apiVersion": "2015-08-01",
    "type": "Microsoft.Web/sites",
    "name": "[variables('functionAppName')]",
    "kind": "functionapp",
    "identity": {
      "type": "SystemAssigned"
    },
    "resources": [
      {
        "apiVersion": "2015-08-01",
        "name": "appsettings",
        "type": "config",
        "dependsOn": [
          "[resourceId('Microsoft.Web/Sites', variables('functionAppName'))]",
          "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]"
        ],
        "properties": {
          "AzureWebJobsStorage": "[concat('@Microsoft.KeyVault(SecretUri=', reference(variables('kvSecretStorageConnectionStringResourceId')).secretUriWithVersion, ')')]",
        }
      }
    ]
  },
  {
    "type": "Microsoft.KeyVault/vaults",
    "apiVersion": "2016-10-01",
    "name": "[variables('keyvaultName')]",
    "accessPolicies": [
      {
        "tenantId": "[reference(concat('Microsoft.Web/sites/', variables('functionAppName')), '2019-08-01', 'Full').identity.tenantId]",
        "objectId": "[reference(concat('Microsoft.Web/sites/', variables('functionAppName')), '2019-08-01', 'Full').identity.principalId]",
        "permissions": {
          "secrets": [ "get" ]
        }
      }
    ],
  },
  "resources":[
    "type":"secrets",
    "apiVersion":"2016-10-01",
    "name":"[variables('kvSecretStorageConnectionStringName')]",
    "dependsOn":[
      "[resourceId('Microsoft.KeyVault/vaults/', variables('keyVaultName'))]",
      "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]"
    ],
    "properties":{
      "value": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storageAccountName'), ';AccountKey=', listKeys(variables('storageAccountid'),'2018-02-01').keys[0].value)]"
    }
  ]
}

Template notes:

  • Creates a storage account
  • Creates a hosting plan
  • Creates a key vault
  • Creates a key vault secret containing the connection string for the storage account
  • Creates a Function App with a system-managed identity
  • Adds an access policy to the Key Vault for the Function App's identity, allowing it to read secrets
  • Sets appSettings of the function app containing a Key Vault reference for the AzureWebJobsStorage property

If you view the configuration of the Function App in the portal, you will see that the setting is sourced from Key Vault.

The setup is for the API token would follow the same pattern as the connection string, but would require a parameter since the value is not generated within the template.

The code for the function app is straight-forward. Simply use environment variables to get the connection string:

string storageConnectionString = Environment.GetEnvironmentVariable("AzureWebJobsStorage");

And, use a local.settings.json file to set the values on the dev machine:

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "FUNCTIONS_WORKER_RUNTIME": "dotnet"
  }
}