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.
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.
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:
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.
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
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.