Azure Functions: Access KeyVault Secrets with a Cert-secured Service Principal

Published on
Reading time
Authors

Azure Functions is one of those services in Azure that is seeing a massive amount of uptake. People are using it for so many things, some of which require access to sensitive information at runtime.

At time of writing this post there is a pending Feature Request for Functions to support storing configuration items in Azure KeyVault. If you can't wait for that Feature to drop here's how you can achieve this today.

Step 1: Create a KeyVault and Register Secrets

I'm not going to step through doing this in detail as the documentation for KeyVault is pretty good, especially for adding Secrets or Keys. For our purposes we are going to store a password in a Secret in KeyVault and have the most recent version of it be available from this URI:

https://mytestvault.vault.azure.net/secrets/remotepassword

Step 2: Setup a Cert-secured Service Principal in Azure AD

a. Generate a self-signed certificate

This certificate will be used for our Service Principal to authorise itself when calling into KeyVault. You'll notice that I'm putting a -1 day "start of" validity period into this certificate. This allows us to deal with the infrastructure running at UTC (which my location isn't) and avoid not being able to access the certificate until UTC matches our local timezone.

create-selfsignedcert.ps1
# Requires PowerShell to be run as Admin-level user.

New-SelfSignedCertificate -CertStoreLocation cert:\localmachine\my -Provider "Microsoft Enhanced RSA and AES Cryptographic Provider" `
                          -Subject "cn=mydemokvcert" -KeyDescription "Used to access Key Vault" `
                          -NotBefore (Get-Date).AddDays(-1) -NotAfter (Get-Date).AddYears(2)

#   PSParentPath: Microsoft.PowerShell.Security\Certificate::LocalMachine\my
#
#Thumbprint                                Subject
#----------                                -------
# C6XXXXXX53E8DXXXX2B217F6CD0A4A0F9E5390A5  CN=mydemokvcert
#

$pwd = ConvertTo-SecureString -String "YOUR_RANDOM_PASSWORD" -Force -AsPlainText

# Export cert to PFX - uploaded to Azure App Service

Export-PfxCertificate -cert cert:\localMachine\my\C6XXXXXX53E8DXXXX2B217F6CD0A4A0F9E5390A5 `
                      -FilePath keyvaultaccess03.pfx -Password $pwd

#    Directory: C:\WINDOWS\system32
#
#Mode                LastWriteTime         Length Name
#----                -------------         ------ ----
#-a----       14/11/2016     16:06           2565 keyvaultaccess03.pfx
#

# Export Certificate to import into the Service Principal
Export-Certificate -Cert cert:\localMachine\my\C6XXXXXX53E8DXXXX2B217F6CD0A4A0F9E5390A5 `
                   -FilePath keyvaultaccess03.crt

#####
# Prepare Cert for use with Service Principal
#####

$x509 = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2
$x509.Import("keyvaultaccess03.crt")
$credValue = [System.Convert]::ToBase64String($x509.GetRawCertData())
# should match our certificate entries above.
$validFrom = [System.DateTime]::Now.AddDays(-1)
$validTo = [System.DateTime]::Now.AddYears(2)

b. Create Service Principal with Cert Authentication**

This step requires you to log into an Azure Subscription that is tied to the target Azure AD instance in which you wish to register the Service Principal. Your user must also have sufficient privileges to create new users in Azure AD - if it doesn't this step will fail.

create-certsp.ps1
##
# Create new Service Principal with Cert configured
##
Login-AzureRmAccount -SubscriptionId XXXXXXXX-XXXX-XXXX-XXXX-86b9ebca2d13

# $credValue comes from the previous script and contains the X509 cert we wish to use.
# $validFrom comes from the previous script and is the validity start date for the cert.
# $validTo comes from the previous script and is the validity end data for the cert.

$adapp = New-AzureRmADApplication -DisplayName "KeyVault Reader - Cert" -HomePage "https://keyvaultreadr/" `
                                  -IdentifierUris "https://keyvaultreadr/" -CertValue $credValue `
                                  -StartDate $validFrom -EndDate $validTo
#
# DisplayName             : KeyVault Reader - Cert
# ObjectId                : XXXXXXXX-XXXX-XXXX-XXXX-1029a4c5be13
# IdentifierUris          : {https://keyvaultreadr/}
# HomePage                : https://keyvaultreadr/
# Type                    : Application
# ApplicationId           : XXXXXXXX-XXXX-XXXX-XXXX-b1aa47a95554
# AvailableToOtherTenants : False
# AppPermissions          :
# ReplyUrls               : {}
#

New-AzureRmADServicePrincipal -ApplicationId $adapp.ApplicationId

# DisplayName                    Type                           ObjectId
# -----------                    ----                           --------
# KeyVault Reader - Cert         ServicePrincipal               XXXXXXXX-XXXX-XXXX-XXXX-11b962b59eef

####
# Grant Service Principal Read-Only on Secrets in our KeyVault
####

Set-AzureRmKeyVaultAccessPolicy -VaultName 'mytestvault' -ResourceGroupName 'your-awesome-rg' `
                                -ServicePrincipalName $adapp.ApplicationId.Guid `
                                -PermissionsToSecrets get

##
# Print Out the Service Principal's App ID (GUID) to use later in our Function setup.
##
$adapp.ApplicationId

At this point we now have a Vault, a Secret, and a Service Principal that has permissions to read Secrets from our Vault.

Step 3: Add Cert to App Service

In order for our Function App(s) to utilise this Service Principal and its certificate to access KeyVault we need to upload the PFX file we created in 2.a above into the App Service in which our Functions live. This is just as you would do if this App Service was running a Web App but without the need to bind it to anything. The official Azure documentation on uploading certs is good so I won't duplicate the instructions here.

Watch out - Gotcha!

Once you've uploaded your certificate you do need to do one item to ensure that your Function code can read the certificate from store. You do this by adding an Application Setting "WEBSITE_LOAD_CERTIFICATES" and either specify just the thumbprint of your certificate or put "*" to specify any certificate held in the store.

Step 4: Function App KeyVault and Service Principal Setup

a. Nuget Packages

Accessing KeyVault with a Service Principal in Functions requires us to load some Nuget packages that contain the necessary logic to authenticate with Azure AD and to call KeyVault. We do this by adding the following to our Function App's project.json.

project.json
{
  "frameworks": {
    "net46": {
      "dependencies": {
        "Microsoft.IdentityModel.Clients.ActiveDirectory": "3.13.1",
        "Microsoft.IdentityModel.Logging": "1.0.0",
        "Microsoft.Azure.Common": "2.1.0",
        "Microsoft.Azure.KeyVault": "1.0.0",
      }
    }
  }
}

b. KeyVault Client CSX

Now let's go ahead and drop in our KeyVault "client" that wraps all code for accessing KeyVault in a single CSX (note that this is mostly inspired by other code that shows you how to do this for Web Apps).

keyvaultclient.csx
#r "System.Runtime"
#r "System.Threading.Tasks"

using System;
using System.Threading.Tasks;
using System.Web.Configuration;
using Microsoft.IdentityModel.Clients.ActiveDirectory;
using Microsoft.Azure.KeyVault;
using System.Security.Cryptography.X509Certificates;

public static string GetKeyVaultSecret(string secretNode)
{
    var secretUri = string.Format("{0}{1}", "https://mytestvault.vault.azure.net/secrets/", secretNode);

    var keyVaultClient = new KeyVaultClient(new KeyVaultClient.AuthenticationCallback(GetAccessToken));
    return keyVaultClient.GetSecretAsync(secretUri).Result.Value;
}

private static async Task<string> GetAccessToken(string authority, string resource, string scope)
{
    var authContext = new AuthenticationContext(authority);
    AuthenticationResult result = await authContext.AcquireTokenAsync(resource, GetCert());

    if (result == null)
         throw new InvalidOperationException("Failed to obtain the JWT token");

    return result.AccessToken;
}

private static ClientAssertionCertificate GetCert()
{
    // could read following values from App Settings if you wanted to
    var clientAssertionCertPfx = FindCertificateByThumbprint("C6XXXXXX53E8DXXXX2B217F6CD0A4A0F9E5390A5");
    // the left-hand GUID here is the output of $adapp.ApplicationId in our Service Principal setup script
    return new ClientAssertionCertificate("XXXXXXXX-XXXX-XXXX-XXXX-e643a85c7c19", clientAssertionCertPfx);
}

private static X509Certificate2 FindCertificateByThumbprint(string findValue)
{
    X509Store store = new X509Store(StoreName.My, StoreLocation.CurrentUser);
    try
    {
        store.Open(OpenFlags.ReadOnly);
        X509Certificate2Collection col = store.Certificates.Find(X509FindType.FindByThumbprint, findValue, false);
        if (col == null || col.Count == 0)
        {
            return null;
        }
        return col[0];
    }
    finally
    {
        store.Close();
    }
}

Step 5: Use in a Function

As we've encapsulated everything to do with KeyVault into a CSX we can retrieve a secret from KeyVault in a Function using a single call once we've imported our client code.

run.csx
#load "keyvaultclient.csx"

public static void Run(TraceWriter log)
{
    var secretStringClearText = GetKeyVaultSecret("remotepassword");
    log.Info(secretStringClearText);
}

Happy (Secure) Days!