Connect Azure Functions securely to Key Vault using VNET integration and Private Link

I did some work with a customer last week to integrate Functions with Key Vault without using the public IP of the Key Vault. This setup worked perfectly, although there were a couple of steps involved. To document those steps, I decided to write this post.

In this post we’ll build a new Azure Function and a new Key Vault. We’ll integrate the Function in our VNET using VNET injection, we’ll integrate the Key Vault into our VNET using Private Link and then connect the two. There’s a little bit of extra configuration on the Function that we’ll have to do to make it use the DNS private Zone to get the right IP address for Key Vault.

For reference, here is some good documentation about what we’re setting up:

  • Azure Functions VNET integration. (important bit here is that this requires premium functions). There’s two ways Azure Functions can integrate with a VNET:
    • VNET Integration: This is useful when the Function needs to connect to another resource in our through the VNET (like we’re doing with Key Vault). This is for connections FROM your function.
    • Private Endpoints (preview right now): This is useful to protect your function and only make it available for resources in the network. This is for connections TO your function.
  • Azure Key Vault Private Link
  • Azure Functions, use Private DNS Zones

So, in terms of workflow this is what I’m planning to implement in this post:

  • Create Azure Function and Key Vault
  • Give managed identity to Azure Function.
  • Have function query public Key Vault to verify things work.
  • Integrate Key Vault using PrivateLink and function using VNET integration.
  • Configure Azure Function to use Private DNS Zone.
  • Have function query private Key Vault to verify things work

So, let’s get started!

Create Key Vault and Azure Function.

In this section, we will create the function and the key vault. Let’s start with the function. Look for functions in the Azure search bar, and hit the create button. The first blade asks for some details. In this case, I’ll be running a Python based function.

Function creation blade. We’ll create python 3.8 in a new resource group.

Next, we’ll need to create the hosting plan. The plan needs to be Premium to work with VNET integration. I’ll stick with the EP1 size, to run this in the cheapest way possible:

Make sure you’re deploying this on a premium plan.

I will enable application insights, to have access to logs in case I need them:

Enabling application insights. Just in case.

And finally, review and create (I’m not tagging for now)

Review and create.

While this is running, let’s create our new Key Vault. We’ll create this in the same region and the same resource group.

Create the Key Vault in the same RG and the same region. This is not mandatory, but will allow me to delete this easily later on.

Next up is access policies. We won’t touch this now, we’ll touch this later on once our Function has a managed identity.

We’ll assign my own user access (the default), we’ll add the function later on.

For now, we’ll also not touch networking and default to the public networking. I first want to make sure public networking works (to ensure the code works), and then we’ll switch to Private Link.

We’ll keep public networking for now.

Then we’ll review and create, and create the Key Vault.

Review and create.

Let’s now wait for all of this to create (should take about a minute to finish), and then we can move to the next step.

Give managed identity to Azure Function.

In this section we’ll do two things:

  • Give our Function a managed identity.
  • Give that managed identity permissions on Key Vault.
  • Create a secret in Key Vault.

Let start with the first thing, giving the managed identity to Key Vault. To do this, open the function in the Azure portal, and in the left hand navigation look for identity. I’ll stick with System Assigned identity for now, but this also works for user assigned identities:

Creating the system assigned managed identity.

Once the identity is created, the blade will change to this:

Grab the object ID once the managed identity is setup.

To make things easier, copy the object ID. We’ll need this in the next step, giving access to key vault.

To give access to key vault, open the key vault and open the access policies. Click the “+Add Access Policy” button here.

Add an access policy to key vault

Here, I’ll give get and list secrets permissions to my managed identity.

Give the managed identity permissions to secrets.

Once this is done, don’t forget to hit the save button on the access policies.

Don’t forget to save the access policy.

With that out the way, let’s create a secret. Click on secrets on the left, and create a new secret.

Creating a new secret.

And with the secret created, we can switch over to the function and try to get the secret through code.

Have function query public Key Vault to verify things work.

I’ll be doing the functions development locally in Visual Studio Code, and push the Function once it’s ready. To do this I run VSCode in WSL2, and installed the Functions extension.

The Azure Functions extension for VS Code. This is a fantastic tool! I loved working with it.

First up, we’ll create a new function locally. To do this, open the command palette (CTRL-shift-P) and look for Azure Functions: create Function…

Create a new function locally

You’ll then get a popup to create a new project, select yes.

Create a new project.

Language will be Python:

We’ll be working with Python.

As a trigger we’ll pick HTTP trigger.

We’ll work with a HTTP trigger.

We’ll call this HttpTrigger1:

Give the function a name. I wasn’t very creative.

And we’ll use Functions authorization

Authorization level as Function for some additional security.

This will create the Azure function locally. You’ll see a number of files and a very simple function.

Sample function being created

Let’s deploy this “raw” function to Azure, to see if everything is working as expected. To deploy to Azure, open the command palette again (CTRL+shift+P) and look for Azure Functions: Deploy to Function app…

Deploy the function to Azure.

If you’re not logged into an Azure account, it’ll ask you to sign in.

Login to your Azure account.

After logging in, select your subscription. Then, select the function we created earlier:

Select the Function we created earlier.

This will throw a little warning about overwriting the current function, click Deploy here.

Deploy and overwrite what’s there now.

This will trigger the deployment to happen. The deployment should take about half a minute to complete. Once this is complete, open the Azure extension view in VSCode, and navigate to your subscription and function. Right click on HTTPTrigger1 and copy the link. Open this in a browser, and you should see the output of the python code we saw earlier:

Copy the Function URL and enter it in a browser.
The return from the default function.

With that working, we can now get to work on the actual code we want to write. We are lucky, since the Azure documentation contains an example of this scenario: using managed identities to access a key vault secret. I modified this a little, and this is the end result we’ll deploy to functions in a second.

import logging
import os
import azure.functions as func
from azure.identity import DefaultAzureCredential
from azure.keyvault.secrets import SecretClient

def main(req: func.HttpRequest) -> func.HttpResponse:
    logging.info('Python HTTP trigger function processed a request.')

    vault_url = "https://nf-func-kv.vault.azure.net/"
    credential = DefaultAzureCredential()
    logging.info('Created managed identity.')

    secret_client = SecretClient(vault_url=vault_url, credential=credential)
    logging.info('Created secret client.')

    retrieved_secret = secret_client.get_secret("superSecretApiKey")
    logging.info('Got secret from Key Vault.')

    if retrieved_secret:
        return func.HttpResponse(f"superSecretApiKey is set to  {retrieved_secret.value} in key vault.")
    else:
        return func.HttpResponse(
             "Function triggered successfully, but failed to get secret from key vault.",
             status_code=200
        )

Since we’re importing from two new python packages, we’ll need to add those to the requirements.txt file.

Add the packages to the requirements.txt file.

With that done, we can redeploy the function to Azure. To do this, we can also use the graphical explorer in VSCode. Open the Azure window again, right click in your function and click on deploy to function.

Using the graphical way to Deploy a function in VS Code.

You’ll get another warning about overwriting, and can then deploy to the function. This will take another 30 seconds, and once live, you should be able to open the function url as we did before, and now see the secret outputted:

We’re getting the secret from key vault.

Cool! So we were able to connect a function to key vault using public networking. Let’s convert this to private networking now.

Integrate Key Vault using PrivateLink and function using VNET integration.

OK, now we can get to the actual meat of this blog post: setting up private networking. To begin, let’s create a new VNET in the resource group we created earlier.

Creating new VNET

We’ll create 2 subnets in this VNET, one for the key vault private link endpoint, another for the function VNET integration:

Setting up address space and subnets

We’ll skip security and tagging, review and hit the create button.

Review and create.

Creating the VNET should take a couple seconds. Once it’s created, head on over to your key vault so we can start integrating that into the VNET. Open the networking blade, and change the Firewalls and virtual networks option to Private endpoint and selected networks.

Change the firewall option to private endpoint.

Now switch to Private endpoint connections and hit the new Private endpoint button.

Create new private endpoint.

We’ll start with providing it a name and the resource group details:

Creating the new private endpoint basics.

Then we will select our key vault as the target resource:

Select the key vault we created.

Next, we’ll pick our VNET that we created, and allow this wizard to create a private DNS zone.

Integrate it into the VNET we created and create a new DNS private zone.

We’ll skip tagging again, review and then create the private endpoint.

Review and create

While this is creating, we can setup the functions VNET integration. Open your function in the Azure portal, and open the networking blade. Here we need to select the first option, VNET Integration:

We need VNET integration here.

In the configuration blade, hit the add VNET button, and select the VNET and subnet for the functions:

Select the VNET and subnet we created earlier.

This will only take a second, but it should show an updated VNET configuration blade:

Blade should update quickly to show updated VNET configuration.

This should be it in terms of networking configuration. Before we can make this work completely, we’ll need to make a small configuration change in Functions to use private DNS. We’ll do that next.

Configure Azure Function to use Private DNS Zone.

Our key vault is now only accessible using private link. However, Azure Functions by default uses public DNS, so doesn’t know how to find the IP address of the key vault it needs to connect to. To change this, we need to set two application settings in Azure Functions:

  • WEBSITE_DNS_SERVER with value 168.63.129.16
  • WEBSITE_VNET_ROUTE_ALL with value 1

To set these app settings, you can either use the Azure portal, or stay inside VSCode to set them. I did it in VSCode. Right click application settings, and select Add New Setting…

Add an application setting

And enter both settings.

This should do it. Let’s test this out in our function.

Have function query private Key Vault to verify things work

The thing now is, it just works. Open the function again, and you’ll notice that you’re getting the output you’re expecting. I was expecting that we would have had to change the key vault URL to the privatelink url, but it works using the regular URL.

This magically works, but how do we verify it works?

But there’s no easy way to confirm that this is flowing over the private connection rather than the public connection.

To verify that the function actually was using Private Link, I decided to cut the tie between the function and the VNET (meaning, I disabled the VNET integration).

We’ll disconnect the Function from the VNET.


Give this about a minute to disconnect (disconnecting is weirdly slower than connecting), and see now that the web page is throwing an error:

HTTP 500 error once disconnected from the VNET

We can dive into the details of this error by opening the log of our application in VScode:

More details on the error.

Which clearly tells us that now the client address is not authorized, which proves that we were using privatelink for the connection. Let’s reconnect the VNET, and restart the function (I kept getting errors, a restart of the function cleared it for me) and this will be working again:

And that’s it. That is how you connect a function to key vault using private connections.

Summary

Storing secrets in key vault is a security best practice. Protecting that key vault using private link is also a good practice, because it disables connections coming over public IPs. You can connect functions to that key vault using that private link connection by integrating your function into your VNET, and configuring it to use the private DNS zone. Once all of this is configured, you can securely and privately connect your Azure functions to key vault.

Leave a Reply