How to secure Azure Functions with Azure AD, Key Vault and VNETs | by René Bremer | Towards Data Science

Secure Azure Functions with Azure AD, Key Vault and VNETs. Then connect to Azure SQL using firewall rules and Managed Identity of Function.
Azure Functions is a popular tool to create small snippets of code that can execute simple tasks. Azure Functions can be triggered using queue triggers, HTTP triggers or time triggers. A typical pattern of an Azure Function is as follows:
Init: Retrieve state from Storage Account
Request: Endpoint is called by another application/user
Processing: Data is processed using other Azure resources
Response: Result is replied to caller
Pattern is depicted below, in which data is retrieved from Azure SQL and returned to application/user.
In this blog, it is discussed how to secure Azure Functions. In this, Azure AD, Managed Identities, Key Vault, VNET and firewall rules are used. To create dedicated and isolated Azure Functions, it can also be decided to create a separate App Service Environment (ASE). However, an ASE can be difficult to manage and more costs are involved. Therefore, an ASE is not used in this blog and a regular App Service Plan is taken. Structure is as follows:
B0. Init: Secure Storage account of Azure Function
B1/B3. Request-response: Secure access to Azure Function
B2. Process data: Secure access to Azure SQL from Azure Functions
C. Conclusion
For details how to secure your Azure Data Factory pipeline, see this blog. Deploying a custom docker image as Azure Functions to run a Selenium web scraper is discussed in this blog.
An Azure Function always has a supporting storage account attached. On this storage account, the code can be found on the file share and logging is written to the blob storage. In this blog, the security account is secured as follows:
Managed Identity (MI) of Azure Function is enabled and this MI is used to authenticate to an Azure Key Vault to get/set secrets
Storage keys are stored in a key vault rather than app settings which is the default. Also, the Azure Function keys are stored in this key vault
VNET integraton is enabled to Azure Function such that all outbound traffic flows through this VNET
NSG is added that only allows outbound traffic to ports 443, 1433 and destination Azure WestEurope
Only VNET of Azure Function can access the Key vault using firewall rules.
(Optional) Only VNET of Azure Function can access the supporting storage account using firewall rules. Notice this scenario is offically not supported, see here and can break your app. When using this feature, it shall be explicitly tested after deployment in production if the app is still working, and if not, the app shall be redeployed.
Securing the storage account in the Azure Function pattern is depicted below.
The code of this step by found using the following below, script can also be found on my github.
# B0_1. Login Clear-AzContext -Scope CurrentUser -Force                       Connect-AzAccount# B0_2. Variables, take same id in all B0/B1B3/B2 scripts$id = "3405" $rg = "test-funappsec" + $id + "-rg"                       $loc = "westeurope"                       $funname = "test-funappsec" + $id + "-func"$funstor = "testfunappsec" + $id + "stor"                       $akv = "test-funappsec" + $id + "-akv"$funplan = "test-funappsec" + $id + "-plan"$vnet = "test-funappsec" + $id + "-vnet"                       $nsg = "test-funappsec" + $id + "-nsg"                       $subnet = "azurefunction"                       $addressrange = "10.200.0.0"# B0_3. Create resource group                       az group create -n $rg -l $loc                                               # B0_4. Create Storage account                       az storage account create -n $funstor -g $rg --sku Standard_LRS                                               # B0_5. Create VNET                       az network vnet create -g $rg -n $vnet --address-prefix $addressrange/16 -l $loc                                               # B0_6. create NSG                       az network nsg create -g $rg -n $nsg                                               # B0_7. Create firewall rules # Only allow outbound to WE storage, port 443 and Azure WEaz network nsg rule create -g $rg --nsg-name $nsg -n allow_we_stor_sql --priority 100 --source-address-prefixes VirtualNetwork --source-port-ranges '*' --destination-address-prefixes Storage.WestEurope --destination-port-ranges '443' '1433' --access Allow --protocol '*' --description "Allow storage West Europe 443" --direction Outbound                       az network nsg rule create -g $rg --nsg-name $nsg -n allow_azure_internal --priority 110 --source-address-prefixes VirtualNetwork --source-port-ranges '*' --destination-address-prefixes AzureCloud.WestEurope --destination-port-ranges '*' --access Allow --protocol '*' --description "Azure we" --direction Outbound                       az network nsg rule create -g $rg --nsg-name $nsg -n deny_all_outbound --priority 130 --source-address-prefixes '*' --source-port-ranges '*' --destination-address-prefixes '*' --destination-port-ranges '*' --access Deny --protocol '*' --description "Deny all outbound" --direction Outbound                                               # B0_8. Create subnet with NSG to VNET                       az network vnet subnet create -g $rg --vnet-name $vnet -n $subnet --address-prefixes $addressrange/24 --network-security-group $nsg                                               # B0_9. Turn on firewall                       Update-AzStorageAccountNetworkRuleSet -ResourceGroupName $rg -Name $funstor -DefaultAction Deny                                               # B0_10. Set service endpoints for storage and web app to subnet                       Get-AzVirtualNetwork -ResourceGroupName $rg -Name $vnet | Set-AzVirtualNetworkSubnetConfig -Name $subnet -AddressPrefix $addressrange/24 -ServiceEndpoint "Microsoft.Storage", "Microsoft.Web", "Microsoft.Sql", "Microsoft.KeyVault" | Set-AzVirtualNetwork                                               # B0_11. Add firewall rules to Storage Account                       $subnetobject = Get-AzVirtualNetwork -ResourceGroupName $rg -Name $vnet | Get-AzVirtualNetworkSubnetConfig -Name $subnet                       Add-AzStorageAccountNetworkRule -ResourceGroupName $rg -Name $funstor -VirtualNetworkResourceId $subnetobject.Id                                               # B0_12. Create Azure Function                       az appservice plan create -n $funplan -g $rg --sku P1v2 --is-linux                       az functionapp create -g $rg --os-type Linux --plan $funplan --runtime python --name $funname --storage-account $funstor                                               # B0_13. Turn on managed identity of Azure Function                       az webapp identity assign --name $funname --resource-group $rg                                               # B0_14. Add VNET integrationaz webapp vnet-integration add -g $rg -n $funname --vnet $vnet --subnet $subnet                                               # B0_15. Create key vault                       az keyvault create --name $akv --resource-group $rg --location $loc                                               # B0_16. Set policy such that Azure Function can read from AKV                       $objectid_funname = az functionapp identity show -n $funname -g $rg --query "principalId"                       az keyvault set-policy -n $akv --secret-permissions set get list --object-id $objectid_funname                                               # B0_17. Set acl on key vault                       az keyvault network-rule add -n $akv -g $rg --subnet $subnet --vnet-name $vnet                                               # B0_18. Get storage connection string and add to key vault                       $storageconnectionstring = az storage account show-connection-string -n $funstor --query "connectionString"                       $keyref = az keyvault secret set -n storageconnectionstring --vault-name $akv --value $storageconnectionstring --query "id"                       $appkeyref = "@Microsoft.KeyVault(SecretUri=" + $keyref + ") " -replace '"',''                                               # B0_19. Set app settings of function # Function gets function keys from AKV instead of storage account                       az functionapp config appsettings set --name $funname --resource-group $rg --settings AzureWebJobsSecretStorageKeyVaultConnectionString="" AzureWebJobsSecretStorageKeyVaultName=$akv AzureWebJobsSecretStorageType="keyvault"                                               az functionapp config appsettings set --name $funname --resource-group $rg --settings AzureWebJobsStorage=$appkeyref                       az functionapp config appsettings set --name $funname --resource-group $rg --settings AzureWebJobsDashboard=$appkeyref                                               # B0_20. Done
By default, the Azure Function key is used to authenticate requests to the Azure Function. Since an Azure Function runs in a web app, it is possible to enable Azure AD authentication for an Azure Function. In this blog, access to the Azure Function is secured as follows:
Azure AD is enabled for Azure Function such that only objects in this Azure AD tenant can call the function. Object in Azure AD can be a user, Service Principal or Managed Identity
(optional) Only whitelist particular objects in the Azure AD tenant that can acces the function. For instance, only the Managed Identity of an Azure Data Factory Instance can execute a Function (see also this blog)
(optional) Add access restrictions in firewall rules of the Azure Function
Restricting access to the Azure Function is depicted below.
The code of this step by found using the following below, script can also be found on my github.
# B1B3_1. Login Clear-AzContext -Scope CurrentUser -Force                       Connect-AzAccount# B1B3_2. Variables, take same id in all B0/B1B3/B2 scripts$id = "3405" # take same id in all B0/B1B3/B2 scripts                                               $rg = "test-funappsec" + $id + "-rg"                       $funname = "test-funappsec" + $id + "-func"                                               # 0.2 connect to AAD                       $Environment = "AzureCloud"                       $aadConnection = Connect-AzureAD -AzureEnvironmentName $Environment                                               # B1B3_3. Creat App registration                       # step 2 is derived from https://devblogs.microsoft.com/azuregov/web-app-easy-auth-configuration-using-powershell/                       $Password = [System.Convert]::ToBase64String($([guid]::NewGuid()).ToByteArray())                       $startDate = Get-Date                       $PasswordCredential = New-Object -TypeName Microsoft.Open.AzureAD.Model.PasswordCredential                       $PasswordCredential.StartDate = $startDate                       $PasswordCredential.EndDate = $startDate.AddYears(10)                       $PasswordCredential.Value = $Password                       $identifier_url = "https://" + $funname + ".azurewebsites.net"                       [string[]]$reply_url = $identifier_url + "/.auth/login/aad/callback"                       $reqAAD = New-Object -TypeName "Microsoft.Open.AzureAD.Model.RequiredResourceAccess"                       $reqAAD.ResourceAppId = "00000002-0000-0000-c000-000000000000"                       $delPermission1 = New-Object -TypeName "Microsoft.Open.AzureAD.Model.ResourceAccess" -ArgumentList "311a71cc-e848-46a1-bdf8-97ff7156d8e6","Scope"              $reqAAD.ResourceAccess = $delPermission1                                               $appRegName = $funname + "_easyauth"                       $appReg = New-AzureADApplication -DisplayName $appRegName -IdentifierUris $identifier_url -Homepage $identifier_url -ReplyUrls $reply_url -PasswordCredential $PasswordCredential -RequiredResourceAccess $reqAAD                                               # B1B3_4. Add new AppRole object to app registration                       # step 3 is derived from https://gist.github.com/psignoret/45e2a5769ea78ae9991d1adef88f6637                       $newAppRole = [Microsoft.Open.AzureAD.Model.AppRole]::new()                       $newAppRole.DisplayName = "Allow MSI SPN of ADFv2 to authenticate to Azure Function using its MSI"                       $newAppRole.Description = "Allow MSI SPN of ADFv2 to authenticate to Azure Function using its MSI"$newAppRole.Value = "Things.Read.All"                       $Id = [Guid]::NewGuid().ToString()                       $newAppRole.Id = $Id                       $newAppRole.IsEnabled = $true                       $newAppRole.AllowedMemberTypes = "Application"                       $appRoles = $appReg.AppRoles                       $appRoles += $newAppRole                       $appReg | Set-AzureADApplication -AppRoles $appRoles                                               # B1B3_5. Add app registration to web app                       $authResourceName = $funname + "/authsettings"                       $auth = Invoke-AzResourceAction -ResourceGroupName $rg -ResourceType Microsoft.Web/sites/config -ResourceName $authResourceName -Action list -ApiVersion 2016-08-01 -Force                       $auth.properties.enabled = "True"                       $auth.properties.unauthenticatedClientAction = "RedirectToLoginPage"                       $auth.properties.tokenStoreEnabled = "True"                       $auth.properties.defaultProvider = "AzureActiveDirectory"                       $auth.properties.isAadAutoProvisioned = "False"                       $auth.properties.clientId = $appReg.AppId                       $auth.properties.clientSecret = $Password                       $loginBaseUrl = $(Get-AzEnvironment -Name $environment).ActiveDirectoryAuthority                       $auth.properties.issuer = $loginBaseUrl + $aadConnection.Tenant.Id.Guid + "/"                       $auth.properties.allowedAudiences = @($identifier_url)                       New-AzResource -PropertyObject $auth.properties -ResourceGroupName $rg -ResourceType Microsoft.Web/sites/config -ResourceName $authResourceName -ApiVersion 2016-08-01 -Force                                               # B1B3_6. Create SPN connected to app registration                       $SPN = New-AzADServicePrincipal -ApplicationId $appReg.AppId -DisplayName $appRegName                                                                                             # B1B3_7. Set "User assignment required?" to true in SPN # (optional, in case you want to whitelist AAD users)                      #Set-AzureADServicePrincipal -ObjectId $SPN.Id -AppRoleAssignmentRequired $true                                                                  # B1B3_8. Set obj of ADFv2 as only authorized user to log in web app# (optional, in case you want to whitelist AAD users)                        #$adfv2_resource = Get-AzDataFactoryV2 -ResourceGroupName $rg -Name $adfv2_name                       #New-AzureADServiceAppRoleAssignment -ObjectId $adfv2_resource.Identity.PrincipalId -Id $newAppRole.Id -PrincipalId $adfv2_resource.Identity.PrincipalId -ResourceId $SPN.Id# B1B3_9. Done
Azure SQL (aka SQLDB) is used to retrieve data from. In this blog, the data processing to Azure SQL is secured as follows:
Managed Identity (MI) of Azure Function is enabled and this MI is used to authenticate to SQLDB
MI of Azure Function is configured as external Azure AD object in SQLDB and is granted a reader role to retrieve data
Only VNET of Azure Function can access SQLDB using firewall rules
Restricting access to SQLD from the Azure Function is depicted below.
The code of this step by found using the following below, script can also be found on my github.
# B2_1. Login Clear-AzContext -Scope CurrentUser -Force                       Connect-AzAccount# B2_2. Variables, take same id in all B0/B1B3/B2 scripts$id = "3405" # take same id in all B0/B1B3/B2 scripts                                               $rg = "test-funappsec" + $id + "-rg"                       $rg_sql = "test-funappsec" + $id + "-rg"                       $loc = "westeurope"                       $funname = "test-funappsec" + $id + "-func"                       $vnet = "test-funappsec" + $id + "-vnet"                       $subnet = "azurefunction"                                               $sqlserver = "test-funappsec" + $id + "-dbs"                       $sqldb = "test-funappsec" + $id + "-sqldb"                       $sqluser = "testfunappsec" + $id + "sqluser"                       $pass = "<<SQLDB password, use https://passwordsgenerator.net/>>"                       $aaduser = "<<your AAD email account>>"                                              # B2_3. Create logical SQL server and SQLDBaz sql server create -l $loc -g $rg_sql -n $sqlserv -u sqluser -p $pass                       az sql db create -g $rg_sql -s $sqlserver -n $sqldb --service-objective Basic --sample-name AdventureWorksLT                                               # B2_4. Configure AAD access to logical SQL server                       # Connect-AzureAD                       Set-AzSqlServerActiveDirectoryAdministrator -ResourceGroupName $rg_sql -ServerName $sqlserver -DisplayName $aaduser                                                # B2_5. log in SQL with AAD (e.g. via portal query editor/SSMS/VSC)                       # Execute following SQL statement                       #CREATE USER [<<your azure function name, equal to $funname>>] FROM EXTERNAL PROVIDER;                       #EXEC sp_addrolemember [db_datareader], [<<your azure function name, equal to $funname>>];                                               # B2_6. point app settings to database                       az functionapp config appsettings set --name $funname --resource-group $rg --settings sqlserver=$sqlserver sqldb=$sqldb                                               # B2_7. Add firewall rules                       $subnetobject = Get-AzVirtualNetwork -ResourceGroupName $rg -Name $vnet | Get-AzVirtualNetworkSubnetConfig -Name $subnet                       New-AzSqlServerVirtualNetworkRule -ResourceGroupName $rg_sql -ServerName $sqlserver -VirtualNetworkRuleName $subnet -VirtualNetworkSubnetId $subnetobject.Id                                               # B2_8. Upload code Azure Function                       # To create Azure Function in Python, see https://github.com/MicrosoftDocs/azure-docs/blob/master/articles/azure-functions/functions-create-first-function-python.md                       # Get code from __init__.py and requirements.txt from this git repo, then run command below                                               func azure functionapp publish $funname# B2_9. Done
An Azure Function is a popular tool to create small snippets of code that can execute simple tasks. In this blog, it is discussed how to secure a typical Azure Function as follows
0. Init: Secure Storage account of Azure Function
1/3. Request-respone: Secure access to Azure Function
2. Process data: Secure access to SQLDB from Azure Function
Pattern with security is also depicted below.
Note from Towards Data Science’s editors: While we allow independent authors to publish articles in accordance with our rules and guidelines, we do not endorse each author’s contribution. You should not rely on an author’s works without seeking professional advice. See our Reader Terms for details.