Integrate KeyVault Secrets with Azure Functions

2022, Jan 25

Integrating KeyVault secrets on Azure Functions is not complicated, the service was designed to handle it via configuration or programmatically.

In this article, I would like to show the approaches you can adopt to integrate Azure Functions with KeyVault.

The code sample is available on PlayGoKids repository

Let's have a look at the different options:

1 - Variables with Reference Secrets

Reference secrets are the easier way to bind KeyVault variables to the Environment variable context. On this approach, only configuration is required. So if you have your Function configuration file, for instance `settings.json`, `appsettings.json` (depending on how you name it) you don't need to change a thing.

Let's have a look at this settings.json configuration file:

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "FUNCTIONS_WORKER_RUNTIME": "dotnet"
  },
  "SecretSettings": {
    "SqlConnectionString": "Sql connection string",
    "BlobConnectionString": "Blob connection string"
  }
}

The section SecretSettings contains the connection string secrets. Those secrets will get superseded by KeyVault secrets if you do this in your Azure Configuration settings:

KeyVault Reference

Note that the Source shows Key vault Reference, and the mapping that makes it possible:

@Microsoft.KeyVault(VaultName=myvault;SecretName=mysecret)

Read my previous article about its variations: Professional Real World Azure Functions

Proof of Concept

Notice that I have only provided SecretSettings:SqlConnectionString as per highlighting, but what about SecretSettings:BlobConnectionString? What's going to happen when I run this function locally? And on Azure?

Before we answer these questions, let's have a look at the function created for this proof of concept:

Project

We have 3 important classes:

  • Startup.cs - Responsible for initializing the function.
  • SecretSettings.cs - The model for SecretSettings json section.
  • TestApi.cs - Test endpoint to validate the variables

And let's look at the code implemented for these classes:

Startup.cs

using System.IO;
using FunctionEnvVariables;
using FunctionEnvVariables.Models;
using Microsoft.Azure.Functions.Extensions.DependencyInjection;
using Microsoft.Azure.WebJobs.Host.Bindings;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;

[assembly: FunctionsStartup(typeof(Startup))]
namespace FunctionEnvVariables
{
    public class Startup : FunctionsStartup
    {
        public override void Configure(IFunctionsHostBuilder builder)
        {
            var executionContextOptions = builder.Services.BuildServiceProvider().GetService<IOptions<ExecutionContextOptions>>().Value;
            var appDirectory = executionContextOptions.AppDirectory;

            var config = new ConfigurationBuilder()
                .SetBasePath(appDirectory)
                .AddJsonFile(Path.Combine(appDirectory, "settings.json"), optional: true, reloadOnChange: true)
                .AddEnvironmentVariables() // Azure portal settings.
                .Build();

            builder.Services.Configure<SecretSettings>(config.GetSection("SecretSettings"));

            builder.Services.AddSingleton<IConfiguration>(config);
        }
    }
}

SecretSettings.cs

namespace FunctionEnvVariables.Models
{
    public class SecretSettings
    {
        public string SqlConnectionString { get; set; }
        public string BlobConnectionString { get; set; }
    }
}

TestApi.cs

using System.ComponentModel;
using System.Threading.Tasks;
using FunctionEnvVariables.Models;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace FunctionEnvVariables.Apis
{
    public class TestApi
    {
        private readonly IOptions<SecretSettings> _settings;

        public TestApi(IOptions<SecretSettings> settings)
        {
            _settings = settings;
        }

        [Description("Test endpoint")]
        [FunctionName(nameof(Secret))]
        public async Task<IActionResult> Secret(
            [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "test/secret")]
            HttpRequest req,
            ILogger log)
        {
            return new OkObjectResult(new
            {
                SqlConnString = _settings.Value.SqlConnectionString,
                BlobConnString = _settings.Value.BlobConnectionString
            });
        }
    }
}

If we run the function locally, and we call the endpoint http://localhost:7071/api/test/secret, we get:

Project

In this result no KeyVault secrets were loaded, because we are running it locally.

If we run the function on Azure, and we call the endpoint https://yourfunction.azurewebsites.net/api/test/secret, we get:

Project

In this result we got the KeyVault secret that we mapped on the Function! Because we have not mapped the other secret, the Function automatically falls back to what is available on settings.json. This is because of the ConfigurationBuilder that we have mapped on Startup.cs.

2 - Variables with Configuration Provider

The Configuration Provider available in C# is called AzureKeyVaultConfigurationProvider. You get this provider available by referencing the Nuget package Azure.Extensions.AspNetCore.Configuration.Secrets. Together with the Nuget package Azure.Identity you get to authenticate to KeyVault.

In this article I use DefaultAzureCredential with Azure.Identity. Explore the different ways to use Azure.Identity here

Let's consider the same settings.json configuration file from 1 - Variables with Reference Secrets:

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "FUNCTIONS_WORKER_RUNTIME": "dotnet"
  },
  "SecretSettings": {
    "SqlConnectionString": "Sql connection string",
    "BlobConnectionString": "Blob connection string"
  }
}

And modify Startup.cs to this:

using System;
using System.IO;
using Azure.Identity;
using FunctionWithKeyVault;
using FunctionWithKeyVault.Models;
using Microsoft.Azure.Functions.Extensions.DependencyInjection;
using Microsoft.Azure.WebJobs.Host.Bindings;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;

[assembly: FunctionsStartup(typeof(Startup))]
namespace FunctionWithKeyVault
{
    public class Startup : FunctionsStartup
    {
        public override void Configure(IFunctionsHostBuilder builder)
        {
            var executionContextOptions = builder.Services.BuildServiceProvider().GetService<IOptions<ExecutionContextOptions>>().Value;
            var appDirectory = executionContextOptions.AppDirectory;

            var config = new ConfigurationBuilder()
                .SetBasePath(appDirectory)
                .AddJsonFile(Path.Combine(appDirectory, "settings.json"), optional: true, reloadOnChange: true)
                .AddAzureKeyVault(new Uri("https://youruri.vault.azure.net/"), new DefaultAzureCredential())
                .Build();

            builder.Services.Configure<SecretSettings>(config.GetSection("SecretSettings"));

            builder.Services.AddSingleton<IConfiguration>(config);
        }
    }
}

I have basically replaced the line:

`.AddEnvironmentVariables() // Azure portal settings.

with this:

.AddAzureKeyVault(new Uri("https://youruri.vault.azure.net/"), new DefaultAzureCredential())

The ConfigurationBuilder class allows you to build key/value-based configuration settings for use in an application, and with AzureKeyVaultConfigurationProvider hooked up, the key/value from Azure KeyVault gets referenced by the Builder, allowing the app to connect to KeyVault.

Let's come back to the same questions. What's going to happen when I run this function locally now? And on Azure?

Proof of Concept

Ready to run the Function with changes?

If we run the function locally, and we call the endpoint http://localhost:7071/api/test/secret, we get:

Project

If we run the function on Azure, and we call the endpoint https://yourfunction.azurewebsites.net/api/test/secret, we get:

Project

In both cases the secrets were loaded from KeyVault, and not from settings.json. Both secrets SecretSettings--SqlConnectionString and SecretSettings--BlobConnectionString came from KeyVault.