Azure Function Key Refresher
When it comes to using Azure Functions to run serverless computations in the cloud, it can be quite common to have the calling client have direct knowledge of the function key. The Function Key is a unique code that is used as part of fully authorising the calling client to execute the Function code. In my case back in Overhead Part 1, I used an HTTP Function in Azure for recording/forwarding SMS messages(named "ProcessMessage" in Part 1), that was called from a Twilio SMS triggered Webhook. The webhook itself was pre-configured as the known Function Url plus the Function Key and this worked well.
However from a security standpoint, having a static function key that is copied onto any client like this (with Twilio in my case) is not always recommended. (The better solution would be to use Azure APIM as a way to hide away the sensitive function key from the client). In my case, I figured it would be an option to have an automated way to recycle this function key on a specific time schedule to always keep it refreshed and to invalidate older function keys should they ever be compromised by a nefarious actor without me knowing.
To do this in an automated fashion, I created a new Azure Function called KeyUpdater that is Timer triggered that runs on a monthly schedule, sets the function key for the existing "ProcessMessage" Function to a randomised new key, and updates the webhook configuration in Twilio with this new key:
public static class FunctionKeyManager
{
////Timer triggered Functions use CRON notation to define the schedule, below is an example of a monthly schedule (every first day of every month)
[FunctionName("KeyUpdater")]
public static void KeyUpdater([TimerTrigger("0 0 0 1 * *")]TimerInfo myTimer, ILogger log)
{
log.LogInformation($"C# Timer trigger function executed at: {DateTime.Now}");
//use Function App Application Settings to store necessary variables
string clientId = Environment.GetEnvironmentVariable("CLIENT_ID", EnvironmentVariableTarget.Process);
string secret = Environment.GetEnvironmentVariable("SECRET_ID", EnvironmentVariableTarget.Process);
string tenantId = Environment.GetEnvironmentVariable("TENANT_ID", EnvironmentVariableTarget.Process);
string functionName = Environment.GetEnvironmentVariable("FUNCTION_NAME", EnvironmentVariableTarget.Process);
var webFunctionAppName = Environment.GetEnvironmentVariable("WEB_FUNCTIONAPP_NAME", EnvironmentVariableTarget.Process);
var resourceGroup = Environment.GetEnvironmentVariable("RESOURCE_GROUP", EnvironmentVariableTarget.Process);
//Create azure credentials to be able to manipulate azure resources through the use of Azure Active Directory ClientId and ClientSecret
var credentials = new AzureCredentials(new ServicePrincipalLoginInformation {
ClientId = clientId,
ClientSecret = secret},
tenantId,
AzureEnvironment.AzureGlobalCloud);
var azure = Azure
.Configure()
.Authenticate(credentials)
.WithDefaultSubscription();
//Access Function App by resource group and functionappname, overwrite the 'default' key with null to automatically set a random new key
var functionApp = azure.AppServices.FunctionApps.GetByResourceGroup(resourceGroup, webFunctionAppName);
functionApp.AddFunctionKey(functionName, "default", null);
var newKey = functionApp.ListFunctionKeys(functionName).Where(x=>x.Key == "default").FirstOrDefault().Value;
string accountSid = Environment.GetEnvironmentVariable("TWILIO_ACCOUNT_SID", EnvironmentVariableTarget.Process);
string authToken = Environment.GetEnvironmentVariable("TWILIO_AUTH_TOKEN", EnvironmentVariableTarget.Process);
//twilio_webhook_base here is the Function Url for "ProcessMessage" without the 'code' parameter at the end
string twilioWebhookBase = Environment.GetEnvironmentVariable("TWILIO_WEBHOOK_BASE", EnvironmentVariableTarget.Process);
string phoneSid = Environment.GetEnvironmentVariable("TWILIO_PHONE_SID", EnvironmentVariableTarget.Process);
string fromNumber = Environment.GetEnvironmentVariable("FROM_NUMBER", EnvironmentVariableTarget.Process);
string toNumber = Environment.GetEnvironmentVariable("TO_NUMBER", EnvironmentVariableTarget.Process);
TwilioClient.Init(accountSid, authToken);
//Update Twilio smsUrl webhook with new generated key
var incomingPhoneNumber = IncomingPhoneNumberResource.Update(pathSid:phoneSid,
accountSid: accountSid,
smsUrl: new Uri(twilioWebhookBase + "code=" + newKey));
//notify about update made, use Twilio.Rest.Api.V2010.Account namespace here
var message = MessageResource.Create(
body: "Your Function Key has been been refreshed on a monthly schedule.",
from: new Twilio.Types.PhoneNumber(fromNumber),
to: new Twilio.Types.PhoneNumber(toNumber)
);
log.LogInformation($"Key Updater finished: {DateTime.Now}");
}
}
In my KeyUpdater Function above, I stored my application variables within the Function App's Application Settings. Microsoft mentions that these values are always encrypted at rest and transmitted over a secure channel so these will be safe to use like the above. Ultimately, the most secure way to handle sensitive variable is to utilise Azure KeyVault where applicable. For the "ProcessMessage" Function Key to be updated from another separate Function App like KeyUpdater here , we need an Azure Active Directory Service Principal created which will give us a ClientId and a ClientSecret (an Azure Portal based guide to do this is available here ). These two details are stored in the KeyUpdater Application Settings like the other details then are used above to help create the necessary authenticated Azure credentials to manipulate aspects about our resources in Azure, such as our "ProcessMessage" function key settings or details.
By locating the target Function App through its corresponding Resource group and its Function App Name (identified above within the value stored as the "WEB_FUNCTIONAPP_NAME" Application setting ), we can then overwrite the value of the already existent default key that is called 'default' for the "ProcessMessage" Function App by setting a value of null. Specifically, when null is set, the value that is created will be randomised which is ideal here for our needs. Afterwards, the "ProcessMessage" Function will only accept requests that carry the new function key as part of the request and deny any requests that still carry the old function key. It is important that that all clients that use the function key are updated accordingly.
In my case, it means that an update to the Twilio 'smsUrl' with the new key on the Twilio IncomingPhoneNumberResource object is necessary as shown above in order for the webhook to keep working as normal as it would now be out of date if nothing is done on it.
Then finally, I set an update sms sent to me/the admin to notify that the update completed. Note, from the CRON notation "0 0 0 1 * *", this means that the message will be sent at 12AM on the first day of every month (it may be a good idea to perhaps further fine tune the notification timing or just receive the SMS notification regardless but have it muted/silenced like I have :) )
Packages
<PackageReference Include="Microsoft.Azure.Management.AppService.Fluent" Version="1.35.0" />
<PackageReference Include="Microsoft.Azure.Management.Fluent" Version="1.35.0" />
<PackageReference Include="Microsoft.NET.Sdk.Functions" Version="3.0.11" />
<PackageReference Include="Microsoft.Rest.ClientRuntime.Azure" Version="3.3.19" />
<PackageReference Include="Twilio" Version="5.61.1" />