Hardening Azure Functions when exposing them via Azure API Management

Introduction

In my discussions with customers about “serverless”, we often talk about the typical security patterns when embarking on the deployment of functions for Enterprise organizations. A typical combination we see here is where Azure API Management is used in front of Azure Functions. Today we’ll talk about the options at hand here. In essence this will related to a choice where an organization will need to choose between “Fully Isolated” and “Full Flexibility”!

 

App Service Plans

When looking towards functions, there are two approaches one can make ;

  • App Service Plan
  • Consumption Plan

When you’re using a Consumption plan, instances of the Azure Functions host are dynamically added and removed based on the number of incoming events. This plan scales automatically, and you are charged for compute resources only when your functions are running. Billing is based on number of executions, execution time, and memory used. Billing is aggregated across all functions within a function app. For more information, see the Azure Functions pricing page.

The app service plan bears down to a scenario where you choose to design for a given capacity yourself. Here you can do a manual sizing, or setup an auto-scale mechanism. The design choices you make bear down to setting a given minimum & maximum capacity. The advantage of the app service plan has over a consumption plan (for Functions) is the ability to deploy the plan into a private virtual network. This can be done by using an app service environment.

So basically choose a consumption plan if you want very fine-grained billing & infinite scale. If you want a fully isolated environment… Then you’ll have to look at an app service environment. With knowing that, let’s take a look at the options…

 

Option A : Full Isolated

Here we’ll deploy an app service environment into a virtual network, and we’ll do the same for our API management. Our API will then only have one external endpoint (“public IP”) and all the other communication will be fully private.

 

Option B : Hardening of Public Endpoints

Now if we are using the consumption plan, we’ll have the following setup…

Both the API management and the Azure Function will have a public endpoint;

First thing to note is that the function is secured via an authorization code. Important to note is that failed requests are not charged… and that the function is not “open to anyone” (as it’s protected by the authorization code).

Now let’s take a look at the API management part. Here we can see the public IP address of this service…

If we would check our function, then we can see that the platform adds an header “x-forwarded-for” to all requests ;

This header will reflect the ip of the client. So we could extend the code of our function to check on the IP of the requesting party / client ;

By doing this, we could configure a kind of “ACL” that would only allow request from the API management. By doing this, we will have hardened the flow by limiting the party that can access the API (~Azure Function).

Example Code : https://github.com/kvaes/TasmanianTraders-IoT-ConnectedVehicle-Functions/blob/master/getSASTokenByDeviceID-Secured-wACL/index.js 

getClientAddress = function (req) {
  return (req.headers['x-forwarded-for'] || '').split(':')[0] 
  || req.connection.remoteAddress;
};

module.exports = function (context, req) {
  var keyName = process.env.keyname;
  var keyValue = process.env.keyvalue;
  var deviceId = req.query.deviceid;
  var iothubHost = process.env.iothubhostname+"/devices/"+deviceId;
  var ip_info = getClientAddress(req);
  var allowedIP = process.env.allowedip;
  if (allowedIP != ip_info) {
    context.log("IP Address not allowed!");
    process.exit(403);
  }

  // "myhub.azure-devices.net/devices/device1";
  // var sas = serviceSas.create(connStr.HostName, connStr.SharedAccessKeyName, connStr.SharedAccessKey, anHourFromNow()).toString();
  var serviceSdk = require('azure-iothub');
  var serviceSas = require('azure-iothub').SharedAccessSignature;
  var expiresInMins = 60;
  var expires = (Date.now() / 1000) + expiresInMins * 60;
  var sasTokenDevice = serviceSas.create(iothubHost, keyName, keyValue, expires);
  var sasTokenDeviceString = sasTokenDevice.toString();
  context.res = {
    sastoken: sasTokenDeviceString
  };
  context.done();
};

[Updated] Option C)
Via the networking option (in platform features), you can also configure the IP address restrictions ;

In the networking features, select “IP Restrictions” ;

Which will provide you with the ability to restrict the IP from a platform feature (even with a consumption plan!) ;

 

Closing Thoughts

Know that a consumption plan cannot be deployed in a private virtual network. If you require this, then you need to take a look at the app service environment. Though the latter comes at a higher base cost… If you want to stay “budget friendly”, know that hardening might suffice for you!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.