Serverless Framework: Defining Per-Function IAM Roles

Separate IAM roles per Lambda function are key for achieving a least privilege setup. We’ve recently released a new serverless plugin to simplify defining such roles.

When building a serverless application, it’s important to assign each Lambda function a separate IAM role. This architecture ensures that each Lambda function is assigned least privilege permissions and is independent from other function’s permissions. It allows functions to evolve over time. As one function’s permissions are modified, they do not affect the other function’s permission scope.

The Serverless Framework is an excellent tool for deploying and operating a serverless architecture. But when it comes to setting up per function roles, the process is still rather complex (as documented here). It basically requires defining each role as a Cloud Formation resource. This leads to a lot of repetition, as each role needs to be fully defined. As an example, when I defined per function roles using the built-in mechanism of the serverless framework for the serverless-stack.com demo application, the serverless.yml file more than doubled in size. The file grew from 133 lines to almost 350. You can view the diff of the commit used to define the roles here.

I am a big promoter of the the role-per-function architecture. But, I am aware that if the tool chain doesn’t provide this as a straight forward task, developers will simply avoid this step. To simplify the task of defining per function roles, we’ve recently released a new open source serverless plugin: serverless-iam-roles-per-function. Github project page and documentation is available at: https://github.com/functionalone/serverless-iam-roles-per-function. The plugin enables you to easily define IAM roles per function via the use of iamRoleStatements at the function definition block. So now you can define a function specific role in the following form:

functions:
func1:
handler: handler.get
iamRoleStatements:
- Effect: "Allow"
Action: s3:GetObject
Resource: arn:aws:s3:::my-bucket/*

The plugin will take care of creating a dedicated role for each function that has an iamRoleStatements definition. It will include standard permissions for CloudWatch logs and if VPC is defined: AWSLambdaVPCAccessExecutionRole will be included (as accustomed to when working with iamRoleStatements at the top provider level).

By default, the statements defined at the function level via iamRoleStatements override the definition at the top provider level. It is possible to also inherit the statements from the top-level definition. Simply set the property: iamRoleStatementsInherit. For example, this scenario works well when adding AWS X-Ray permissions to multiple functions:

provider:
name: aws
iamRoleStatements:
- Effect: "Allow"
Action:
- xray:PutTelemetryRecords
- xray:PutTraceSegments
Resource: "*"
...
functions:
func1:
handler: handler.get
iamRoleStatementsInherit: true
iamRoleStatements:
- Effect: "Allow"
Action: s3:GetObject
Resource: arn:aws:s3:::my-bucket/*

In the example above the generated role assigned to func1 will contain both the general top level X-Ray permissions and the function specific s3:GetObject permission.

As a final example, let’s look again at the demo application from serverless-stack.com. This time using our plugin to define per-function roles. The file is now less than 150 lines compared to the built-in mechanism of the serverless framework for defining per-function roles, which was close to 350. The updated file can be viewed here.

Conclusion

Using per-function roles is a recommended best practice to achieve and maintain a least privilege setup for you Lambda functions. It is now super easy to define this in your serverless.yml file, so please do. Finally, if you have an existing complex stack with many functions and a large code base, you are probably wondering how to reach the proper least privilege permissions per function. If so, check-out my previous post: