Serverless and the principle of least privilege

Every program and every privileged user of the system should operate using the least amount of privilege necessary to complete the job.

— Jerome Saltzer, Communications of the ACM

That quote, by American computer scientist Jerome Saltzer, underpins the concept that became known as the "Principle of Least Privilege". The idea is that by ensuring each part of a system has the ability to access the data it needs to do its job and nothing beyond that. It applies to pretty much any software system, be it a web application or a program running on an embedded chip.

The rest of this article is going to focus on the principle of least privilege as it applies to the Serverless framework, and, in particular, Serverless applications deployed to AWS. Before we dig into Serverless it will help if you have an understanding of the Identity and Access Management, or IAM, feature of AWS.

IAM

AWS IAM is a comprehensive identity and access management tool. It provides functionality to manage users and control their access to other AWS services and resources. It provides the concept of roles that can be assumed by other entities to grant access to resources. It even provides the ability for web or mobile applications to directly access AWS resources.

IAM provides the concept of "identities" which are created to "provide authentication for people and processes in your AWS account". Identities can be, amongst others, "users", such as the personal account you might use to log in to the AWS Console, or "roles". For now we're primarily interested in IAM roles.

Imagine an AWS Lambda function that when invoked needs to read from a DynamoDB table, manipulate some data, write the modified document back to the table and then publish a message to an SNS topic to notify other interested parties that it has done its job. By default, that Lambda will not have permission to access any of those resources. To ensure the function has the ability to access the resources it depends on it can assume an IAM role when it executes. The Lambda documentation refers to this as the "execution role", and every Lambda function has one that is specified at the time it is created. Likewise, other AWS resources, such as API Gateway, may need a role that allows them to invoke Lambda functions. The Lambda web console has a helpful diagram that shows both sides of this:

Permissions in the Lambda console

We can see that the API Gateway has permission to invoke the Lambda, and the Lambda execution role gives it permission to access CloudWatch, DynamoDB and SNS.

Serverless

When developing a Serverless application for AWS you have the ability to either configure an IAM role that will be assumed by all the Lambda functions in your service (the default approach), or to define a different role for each function.

If you stick with the default you'll end up with something like this in your serverless.yml file. This example will allow all Lambda functions in the service to perform the "publish" action on a specific SNS topic:

provider:
  name: aws
  iamRoleStatements:
    - Effect: Allow
      Action:
        - sns:publish
      Resource:
        - Ref: MySNSTopic

There's a couple of things to think about here. We've gone a long way towards ensuring our functions "operate using the least amount of privilege necessary to complete the job". They are able to perform only a single type of action against a specific resource. If your function code was to attempt to publish a message to a different SNS topic it would not be able to do so. However, we can do better. As we add functions to our service it's likely that they have different access requirements. By following the default configuration you will soon end up with a wide-ranging IAM role that gives all of your Lambda functions access to everything that any one of them might need. To better adhere to the principle of least privilege we need to define an IAM role per Lambda function.

Unfortunately, achieving this with the Serverless framework is fairly involved. When not using the provider-level role configuration you don't benefit from any of the permissions that you usually get for free from Serverless, meaning you need to manually configure access to things like CloudWatch for logging. If this is the approach you want to take you can find plenty more information in the official documentation. But thanks to the wonderful open-source community there is a much easier route.

The serverless-iam-roles-per-function plugin takes care of all that other stuff you'd usually get out of the box and lets you focus just on the resources that are specific to your individual Lambda function. Activate the plugin in serverless.yml:

plugins:
  - serverless-iam-roles-per-function

And then you can define iamRoleStatements at the function level:

myLambdaFunction:
  handler: src/functions/hello/index.default
  events:
    - http:
        path: hello
        method: get

  iamRoleStatements:
    - Effect: Allow
      Action:
        - sns:publish
      Resource:
        - Ref: MySNSTopic

With this configuration any additional Lambda functions in our service will not be able to use the sns:publish action on the MySNSTopic resource. And this function itself will not be able to use any other SNS actions even on that resource, fulfilling our original goal of giving each Lambda function to least amount of privilege necessary to complete the job.

The orangejellyfish Serverless starter kit includes the aforementioned plugin by default and contains an example Lambda function along with a range of other best-practice configuration defaults and helpers.

Twitter