Azure Functions can be of great benefit to your automation flows! They can get data and perform business logic on request. They can even connect to your on-premises environment on demand.

And the best thing? Azure Functions can be reached over the Internet! 😧 Or is that not always a ā€˜best thing’?

In my previous blog post(s), I demonstrated how I use Azure Functions for automation tasks. Now, it’s time to investigate how to properly secure them. Neglecting the security of your Azure Functions can be very risky!

That immediately brings me to the content of this blog post (which you probably can guess already). I’m going to talk about how to secure your Azure Functions so that they only benefit you or the users you want to make it available for! And not that bully on the other side of the world who wants to mess up your infrastructure.

Also, on this blog, you’ll find the ā€˜Deploy to Azure’ button deploying a basic Azure Function for you, which will be used as an example of the hardening process.

During this post, Ā you’ll see this icon šŸŽ¬ which indicates that action is required so you get the full benefit out of this post.


And I’ve introduced the šŸ“’ which indicates that the subsequent part explains the technical details in the context of this post.

Note: The screenshots in this post are taken from the moment the configurational settings where actual. Changes from Azure which might occur in the future are not taken into account.

Deploying the basics

You can deploy an Azure function with its default settings or use the ā€˜deploy to Azure’ button below:

šŸŽ¬ Deploy an Azure function with default settings to Azure or use the button below and let me do it for you šŸ˜‰

Deploy to Azure

When you’ve deployed everything you should end up with the following resources:

This deployment file deploys a basic Azure function with the required resources from Azure which are needed for this blogpost. Please note the following settings as mentioned below:

  • Service plan Premium V2 (Because we want to implement private networking to enhance security)
  • App insights for storing telemetry, logging, and diagnostics from the function
  • Storage account which runs the Azure function
  • Function app which is the actual function app where the code is executed
  • HTTP Trigger which is called to trigger the actual function, gather a response, and return the response to the requester

Dealing with all security considerations

Now that we have the Azure Function app with the default settings, it’s time to review all settings and implement some best practices to ensure  a proper level of security!

Some of the security controls we need to check, and should have our attention, are:

  • Public Runtime keys from Storage account; by default, Azure Functions are deployed with security keys (encryption and decryption keys) which are stored on a storage account. If not explicitly configured this might expose the Azure Function keys to the public internet.
  • Configuration; by default Azure Functions get created with default configuration settings. You may want to change some settings to harden your Azure Functions resources for a higher level of security.
  • Public network exposure; by default Azure Functions are deployed with public network exposure (although Functions are protected with a security key, there are some additional things we can do to improve security on this level)

Now, let’s examine above settings, and let me explain the potential risks for each configurable parameter.

Public Runtime Keys

As you’ve probably already seen, the Azure Function ….gets deployed with a Storage account. Before I show you all the details, let’s start with the deployed infrastructure. In my example I have the infrastructure deployed as shown below;

Storage account

The storage account contains critical data for the storage account to operate such as the ā€˜secrets’  and details about the last run of the Azure Functions.

On the storage account we can find the containers and items as shown below;

If you check these files you would find content as described below (I’m not blurring or hashing it to show you proper examples)

{
  "masterKey": {
    "name": "master",
    "value": "CfDJ8AAAAAAAAAAAAAAAAAAAAABHABA6RyX9cqECspxJDnPnIe1GOezhjcPCFchVHUZ_rxgzH0bKSlTE3vYNSVAkSY-_EFtI4MYcrIXBv8zOOQ5F1sLNhRPJq4TjicXJXLFsAJ6NKxByAtpMu6ZvCm96AOnoR1lN62TsFp9-s2SZB4dX7UvUVdjhEDLs67b0UiOB9A",
    "encrypted": true
  },
  "functionKeys": [
    {
      "name": "default",
      "value": "CfDJ8AAAAAAAAAAAAAAAAAAAAAAo3gqzjH8da27Sx5_J451R867nNxqCgP7TWSm7ATIUk8_2dGPi8ufeONOd0N2-pIYzH4Y__gaM1DbbeHOIwB_ZidZKjGVNppbNUI6WonUomUYVy2g63BU3wCcn0b-RrVgHeOiB4-KdjQovI-xz8bUytkHENAgo7cHKR9WogrXKnQ",
      "encrypted": true
    }
  ],
  "systemKeys": [],
  "hostName": "bpastest.azurewebsites.net",
  "instanceId": "98a93a96b65fbe45d39b5d2f02b44b0a",
  "source": "runtime",
  "decryptionKeyId": "MACHINEKEY_DecryptionKey=oRRCtfIgJcz9m7j5Oj8Cnlj415XkleXleSCl5X/XO+I=;"
}

You don’t want to expose this information to the outside world especially if you are running the storage account with public anonymous access enabled.

Remediation

The best solution in this case (to secure your Azure Function keys) would be to deploy an Azure Key Vault and store the keys therein. 

šŸŽ¬ Follow the steps below to remediate

  1. Deploy an Azure Key Vault (for learning purposes default settings are fine. If you are dealing with this in production environments, use private endpoints!)
  2. Create a secret alternative for the master key as for the default key
  3. If not enabled yet, enable the managed identity on the Azure Function
  4. This identity now needs to have the right permissions on the Key vault to get the newly created secret values. Copy the principal ID and assign the permissions on the Key Vault (Secret: GET, LIST, SET, DELETE)
  5. Now we need to update the Azure Function so it no longer fetches the keys from the Storage Account but fetches them from the Azure Key vault. Add the environment variables defined below to the Azure Function
KeyValue
AzureWebJobsSecretStorageKeyVaultUri<FQDN of your keyvault> in my example: https://bpas.vault.azure.net/
AzureWebJobsSecretStorageTypekeyvault

In the end you will end up with:

šŸŽ¬ Now restart your function and check the KeyVault. There should be new secrets in there:

Well done! šŸ˜‰ the first issue is mitigated and keys for the function runtime are securely stored in the Azure KeyVault. Also, consider how you want your keys to be linked to your functions. Do you want to use one key to access all functions? Or do you delegate some of your functions to other business units and create a key per function for this?

It’s up to you to decide! As long as you take this topic serious.

Configuration

By default, some configuration settings might expose your Azure Function to the internet with suboptimal security settings . Let’s walk through those settings, and let me explain what they do and which option you should choose to harden your Azure Function(s).

These settings can be found underneath

šŸ“’ See the summary below for this deployment type. Please note; that some settings differentiate between runtime, stack, or platform. Follow the pattern, not the actual values if you are considering these settings.

KeyExpected valueRationale
StackPowerShell CoreDefine the stack of your runtime code (whether it’s Python, PowerShell or C#)
VersionPowerShell 7.4Stick with the version of your stack which supports your development. But, stay aware of potential security risks. Define a standard like ā€˜latest minus one’
Platform64 BitFor the best memory utilization and support stay with the latest development platform and stick with 64-bit. Only fall back to 32 if you need to support legacy libraries/code.
Managed PipelineIntegratedFor modern apps always choose integrated. Classic is mostly used for legacy and older apps.
SCM Basic AuthOnYou can leave this on if you either push directly to your Azure Functions by publishing profiles. Otherwise, configure it to be ā€˜off’
FTP Basic AuthOffNo question about this, disable this if you don’t utilize FTP deployments. Rather choose between a pipeline deployment or SCM.
HTTP Version2.0Choose the latest supported HTTP version if your app supports it. Way better support for modern workloads and security controls.
Web SocketsOffIf you don’t need to implement things like SignalR or any other client-to-server two-way communication protocol. If it’s not needed, disable it!
Always OnOnIf you ask me, the warming up can sometimes conflict with whatever or whoever is calling your function.
TLS Inbound Version1.3Take 1.3 if your app/workload can support it. For me, this is an ā€˜always take the latest’ approach.
Minimum TLS Cipher suiteTLS_RSA_WITH_AES_256_CBC_SHATLS_RSA_WITH_AES_128_CBC_SHA is excluded in this one. 256 should be broadly supported and enhance your security. If you need the AES_128 because of legacy controls utilize the AES_128. Otherwise, stick with AES_256
Functions Admin IsolationDepends per environmentTest/Dev this is acceptable I would say to have it ā€˜Off’   For production environments I would configure it to be ā€˜On’ Requests to your function should be done outside of the admin panel through validated requests.
Remote debuggingOffOff for production environments!   Dev it’s fine to have it open to simulate actual behavior. But don’t put this on for production workloads. Attaching a remote debugger to a production environment makes the entire environment stale when a debug breakpoint is hit.
Client Certificate ModeIgnoreHTTP 2.0 is enabled, so Client Certificate Mode should be ignored as there is a correlation between these two settings.

If you follow the settings above, your function is already way more secure than without them. Although some settings may differentiate per workload it hopefully provides you with some valuable insight on when to make certain configuration changes for the purpose of better security.

Networking

Before we can discuss the networking configuration of an Azure Function, we need to have a virtual network in place.

šŸŽ¬ Deploy a virtual network with default settings

  • Address range can be for instance 10.0.0.0/8
  • No subnet delegation(s) should be in place for the first subnet
  • Deploy a second subnet with serverfarms delegation

Now let’s configure private networking for Azure Functions!

  1. Go to the Azure Functions ā€œPrivate endpoint connectionsā€
  2. Enter the networking details to your own standard like ā€œnameā€ and ā€œnetwork interface nameā€
  3. Ā Connect the private endpoint to your Function App (which is found underneath ā€˜sites’)
  4. Select your newly created virtual network as the source for the private endpoint
  5. The private DNS configuration can be left as is for now (we won’t be focusing too much on private DNS in this post)

šŸŽ¬ Now configure the Function outbound network to the delegated subnet

āš ļø This helps for the function to utilize this VNET to request the content it needs to operate! This VNET now supports the fetching of the content securely from the Azure Function to the Storage Account!

Now the function has a private endpoint and can communicate over it to connect securely to other resources in the same VNET.

šŸŽ¬ Next is the storage account which hosts parts of our function. Let’s not leave that one untouched! šŸ˜‰

  1. Open the storage account networking and configure the same settings for networking as you did for the Azure Function
  2. Configure the sub-resource as shown below
  3. And configure the Virtual Network
  4. DNS can be default (same as we did for the Azure Function)

If everything is deployed the Storage account will now be available over the private endpoint! But let’s review some additional things here, as there’s still an option to connect to the storage account over public endpoints!

šŸŽ¬ Disable the public endpoint on the Storage Account

But… what about the KeyVault? Correct! That one also needs to have a private endpoint to complete the chain of services we are using to run the Function!

šŸŽ¬ Configure the KeyVault to support private endpoints

  1. Same as before: configure the network settings
  2. Configure the sub-resource as shown below
  3. And configure the Virtual Network
  4. DNS can be default (same as we did for the Azure Function)

Now let’s check the Function again! As we are now secured and configured 😊

Oops! It seems like something is going wrong!

So what’s happening?

We’ve removed the access for ourselves as well, the Function can no longer reach the resources as it’s not routing over the VNET to find its required resources. To get this to work we need to tell the Function to query its resources over the VNET and use the private endpoints instead!

Add the following environment variables to the Azure Function to get it operational again;

KeyExpected valueRationale
WEBSITE_CONTENTOVERVNET1Tells the Azure function to fetch the content that is located on the Storage account over the VNET
WEBSITE_DNS_SERVER168.63.129.16Tells the Azure function to query the DNS server in the VNET to find all the private endpoint registrations

There we have it!
A close-up of a white background

AI-generated content may be incorrect.

A working Azure Function, hidden from the internet (going to the function now shows the image below)

This means that testing the Function from the Azure portal itself will also fail as these calls are client-side oriented. If you want to test this ,run the function from the portal (you will see something like this:)

šŸ’” Now ad your client IP to the allowed IP list on the Functions networking tab to allow communication to the Azure Function

Try the call again from the portal or your browser.

Your client will get a 200OK with the response

As will the portal

Summary

Well, well, well! Look what we’ve accomplished together! šŸŽ‰

We started with an Azure Function that was basically screaming “HACK ME!” to every script kiddie on the internet, and we’ve transformed it into a security fortress that would make even the most paranoid security architect shed a tear of joy.

Let’s recap our journey from “security nightmare” to “security dream”:

šŸ” Keys Locked Away Like Crown Jewels

Remember those runtime keys just sitting there in a storage account, accessible to anyone with a web browser? Yeah, those are now safely tucked away in Azure Key Vault, encrypted and protected by managed identities. Your Function Keys are now more secure than your grandmother’s secret cookie recipe!

āš™ļø Configuration Hardened Like a Tank

We went through every single configuration setting with a fine-tooth comb. From TLS 1.3 to disabling unnecessary features like FTP (seriously, who still uses FTP?), your Function is now configured like it’s protecting state secrets. Because let’s be honest, your business logic probably IS a state secret! šŸ˜‰

🌐 Network Security That Would Make a Firewall Jealous

The piĆØce de rĆ©sistance – private endpoints everywhere! Your function, storage account, and Key Vault are now having their conversations in a private VNET, away from the prying eyes of the internet. It’s like they’re whispering secrets in a soundproof room while the rest of the world is locked outside.

šŸŽÆ The Reality Check

Sure, we hit some bumps along the way (looking at you, subnet delegation requirements, which sometimes can be confusingšŸ‘€), but that’s the reality of proper security – it’s not always straightforward, but it’s always worth it. The fact that your Function now returns a 403 when accessed from the internet isn’t a bug, it’s a feature!

šŸ’” The Bottom Line

Your Azure Functions have gone from being the digital equivalent of leaving your front door wide open with a sign saying “Free Stuff Inside!” to being a properly secured, enterprise-grade solution that would make any security team proud.

And the best part? You can still test and develop normally by adding your IP to the allowed list when needed. Security doesn’t have to lead to productivity suicide!

So there you have it – your Azure Functions are now locked down tighter than a submarine’s hatch. Sleep well knowing that your functions are secure, your data is protected, and that bully on the other side of the world will have to find someone else’s infrastructure to mess with! šŸ˜Ž

Remember: Security isn’t a destination, it’s a journey. Keep those configurations updated, monitor those logs, and never stop being paranoid about security – because in this business, paranoia is just good sense! šŸ›”ļø

Leave a Reply

Your email address will not be published. Required fields are marked *