Manage Azure Functions API Security
Summary
There are several methods you can use to secure Azure Functions. This page particularly documents the security standards required by UK Government standards when deploying serverless code (Azure Functions HTTP endpoints). In this post, I’ll demonstrate how to use the Azure Functions API Keys.
Azure Functions Security Basics
Government guidelines often dictate how to produce discrete pieces of software. Here is a checklist for managing Azure Functions security.
- The Azure Consumption plan should not be used. Azure App Service Environment (ASE) must be used.
- Configure a Gateway service such as Azure Application Gateway or Azure Front Door correctly.
- Isolate Functions to independent Function Apps. This provides zero-shared resources between Azure Functions.
- Force HTTPS in the TLS/SSL Settings.
- Enable TLS 1.2 as a minimum.
- Enable CORS and restrict access to specific consumer endpoints.
- Use Function Access Keys only.
- Enable App Service Authentication in the Identity settings. This is an absolute mandatory requirement in Government agencies.
- Use Managed Identities.
- Use Key Vault to manage connection secrets.
- Use Azure API Management to authenticate requests to the Function App.
- Manage RBAC permissions at the Subscription, Resource Group and Resource level.
- Use middleware to manage data validation. This rule is often overlooked; in a Request and Response integration pattern, the bit in the middle should handle the validation before a response is sent back to the consumer, e.g. when a User wants to update their contact details, the middleware should carry out the necessary checks before sending the User the record details.
- Handle Errors correctly using Application Insights. Poorly written code can often expose sensitive data after the platform throws an exception.
- Ensure that the Function App is compliant with Microsoft’s Azure security baseline for Azure Functions.
- Ensure that debugging is disabled.
Running Azure Functions
Azure HTTP Trigger Functions have three authorization scopes: Anonymous, Function and Admin. This section documents how to use all three authorization levels.
Action | Scope | Valid keys |
---|---|---|
Execute a function | Specific function only | Function |
Execute a function | Any function in the Function App | Function or host |
Call an admin endpoint | Function app | Host (_master) |
Call Durable Task extension APIs | Function app1 | System2 |
Call an extension-specific Webhook (internal) | Function app1 | system2 |
1Scope determined by the extension.
2Specific names are set by extension.
You can run multiple Functions inside a single Azure Functions app. Although it’s not recommended, it’s a good way to rapidly prototype. The Azure Functions code below creates three HTTP triggers:
The code below is used to create the three Functions. Each Function creates its endpoint.
public static class Authorization
{
[FunctionName("AuthFunctionAnonymous")]
public static async Task<IActionResult> RunAnonymous([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req, ILogger log)
{
log.LogInformation("C# HTTP trigger function processed a request.");
string name = req.Query["name"];
string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
dynamic data = JsonConvert.DeserializeObject(requestBody);
name = name ?? data?.name;
string responseMessage = string.IsNullOrEmpty(name)
? "This HTTP triggered function executed successfully. Pass a name in the query string or in the request body for a personalized response."
: $"Hello, {name}. This HTTP triggered function executed successfully.";
return new OkObjectResult(responseMessage);
}
[FunctionName("AuthFunctionFunction")]
public static async Task<IActionResult> RunFunction([HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req, ILogger log)
{
log.LogInformation("C# HTTP trigger function processed a request.");
string name = req.Query["name"];
string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
dynamic data = JsonConvert.DeserializeObject(requestBody);
name = name ?? data?.name;
string responseMessage = string.IsNullOrEmpty(name)
? "This HTTP triggered function executed successfully. Pass a name in the query string or in the request body for a personalized response."
: $"Hello, {name}. This HTTP triggered function executed successfully.";
return new OkObjectResult(responseMessage);
}
[FunctionName("AuthFunctionAdmin")]
public static async Task<IActionResult> RunFunctionAdmin([HttpTrigger(AuthorizationLevel.Admin, "get", "post", Route = null)] HttpRequest req, ILogger log)
{
log.LogInformation("C# HTTP trigger function processed a request.");
string name = req.Query["name"];
string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
dynamic data = JsonConvert.DeserializeObject(requestBody);
name = name ?? data?.name;
string responseMessage = string.IsNullOrEmpty(name)
? "This HTTP triggered function executed successfully. Pass a name in the query string or in the request body for a personalized response."
: $"Hello, {name}. This HTTP triggered function executed successfully.";
return new OkObjectResult(responseMessage);
}
}
The following URLs are generated for the Functions:
No API Key AuthorizationLevel.Anonymous
https://eax360001.azurewebsites.net/api/AuthFunctionAnonymous?&name=syed
RunAnonymous([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req, ILogger log)
{}
As expected no API key is required to trigger this Function.
Function-level API Key AuthorizationLevel.Function
https://eax360001.azurewebsites.net/api/AuthFunctionFunction?code=UdlUcXRJs7tYh4bFQKIVwCyAkJfaLxJ2X6x95VL4iqEQbwGJSas8fg==&name=syed
RunFunction([HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req, ILogger log)
{}
Function Keys apply only to the specific functions under which they are defined. When used as an API key, these only allow access to that function.
In this setting, a Function specific key called devKey with a value of fJodwEO7YchhtTNyilZolePJl6DysUOMYu1qtoR99pXkrkdB8cRXg== can be used as the API key.
Host: Keys with a host scope can be used to access all functions within the function app. When used as an API key, these allow access to any function within the function app.
Will allow invocation of the following Functions:
Each key is named for reference, and there is a default key (named default) at the function and host level. Function keys take precedence over host keys. When two keys are defined with the same name, the function key is always used.
Calling a Function with an API Key
Calling this function will require the API key to be passed as a Header. Which can then be called with a simple GET request.
Note that for simplicity I have included the API key in the URL call. If you are building a Node.js app; add the key to the Environment variables.
const axios = require('axios');
async function makeRequest() {
const config = {
method: 'get',
url: 'https://eax360001.azurewebsites.net/api/AuthFunctionFunction?code=fJodwEO7YchhtTNyilZolePJl6DysUOMYu1qtoR99pXkrkdB08cRXg==&name=syed'
}
let res = await axios(config)
}
makeRequest();
Administrative Use Only
Each function app also has an admin-level host key named _master. In addition to providing host-level access to all functions in the app, the master key also provides administrative access to the runtime REST APIs. This key cannot be revoked. When you set an access level of admin
, requests must use the master key; any other key results in an access failure.
AuthorizationLevel.Admin
https://eax360001.azurewebsites.net/api/AuthFunctionAdmin?code=WkxX60mzFTFjQrqSMYniWL/HXCwbRHPIEf3DQmvCntLZdO4bysdIkg==&name=syed
RunFunctionAdmin([HttpTrigger(AuthorizationLevel.Admin, "get", "post", Route = null)] HttpRequest req, ILogger log)
{}
This Key should only be used for System Administration purposes.
AuthorizationLevel.System
This authorisation scope should be used to manage specific extensions that may require a system-managed key to access webhook endpoints. System keys are designed for extension-specific function endpoints that are called by internal components. For example, the Event Grid trigger requires that the subscription use a system key when calling the trigger endpoint. Durable Functions also uses system keys to call Durable Task extension APIs.
The scope of system keys is determined by the extension, but it generally applies to the entire function app. System keys can only be created by specific extensions, and you can’t explicitly set their values. Like other keys, you can generate a new value for the key from the portal or by using the key APIs.