A visual introduction to AWS Lambda permissions
Tags: aws, serverless, security, lambda
Published: 2020-10-01 04:53:45

## Introduction

Big value in using AWS Lambda comes from its interplay with other AWS services. To achieve that, however, it's important to understand the role AWS IAM plays in both allowing and securing these interactions.

When working with Lambda using AWS console or frameworks like Serverless or SAM, most of the connectivity details are sorted out for us behind the scenes. But when dealing with scenarios like cross account access and applying principle of least privilege, there's a need to have a better understanding of how Lambda's permissions work. In this post, we will go about looking at a general model for this followed by applying the same to a couple of scenarios.

## Permissions Model

At the heart of AWS IAM are the policy documents defining "who can do what" in the AWS world. Clearly, when trying to make sense of Lambda's interactions, it's best to look at things from relevant policies point of view. In this context, for a given Lambda function, following two questions come up which in turn get answered by two different kinds of policies:

**1. Which service is allowed to invoke a Lambda function?**

This is decided by Resource Based Policy of a Lambda function. Such policies are called so as they are applied directly to a specific resource in an inline manner. In case of a Lambda function, the "Resource" element in the policy refers to the function's ARN. While the "Principal" config refers to other AWS resources to whom function invocation access is granted. Previously, Lambda's Resource Based Policies used to be referred to as Function Policies.

**2. Once invoked, which service is a Lambda function in turn allowed to invoke?**

This is controlled by policies attached to Execution Role of a Lambda function. Execution Role is the role that a Lambda function assumes during its execution. A "Principal" isn't defined in such policies as entities like Lambda function that assume the role themselves act as the principal. The "Resource" part of this policy refers to AWS resources to which the Lambda function is granted access to. We must also define a trust policy on the Execution Role so as to explicitly grant the function permission to assume the role as a principal.

![AWS Lambda Permissions Model](https://www.harprit.dev/visual-aws-lambda-permissions/aws-lambda-permissions-model.jpg)

An important difference to understand is that while policies attached to the Execution Role allow a set of permissions to Lambda function as a Principal. Resource Based Policies take care of the other side of things by stating what other Principals can do to a Lambda function as a resource. Put together, both the policies let us control complete access to and from a Lambda function.

## Invocation Modes

Given the above general model, things change a bit based on the manner in which Lambda function gets invoked. Of which, there are following two modes:

**1. Push Mode**

In this case, an external service pushes an event into Lambda and triggers the function. This event could originate due to some action in an another service or a user initiated web request and so on. In this scenario, from permissions perspective, the service that's the source of the event requires rights to invoke the Lambda function. As we saw above, for a Lambda function, the place to define such permissions is the Resource Based Policy.

**2. Pull Mode**

There are some services like SQS and Kinesis that do not push events directly to invoke Lambda. For these event sources, Lambda as a service polls them for events and invokes the Lambda function i.e. the function doesn't gets invoked by an external service. From Lambda's permissions point of view, this implies that Resource Based Policies are not relevant here. Secondly, Lambda's Execution Role permissions need to be broadened so as to be able to poll and read messages from services like SQS and Kinesis.

![AWS Lambda Invocation Modes](https://www.harprit.dev/visual-aws-lambda-permissions/aws-lambda-invocation-modes.jpg)

Next, we shall look at how things really look when applying Lambda's permissions model with examples on both types of invocation modes.

## Push Mode Permissions

For this scenario, let's consider a simple design where a Lambda function subscribes to a SNS topic. When a message is published to the SNS topic, the subscribed Lambda function gets invoked by SNS with the message as payload. Upon invocation, the Lambda functions reads the message, extract some data of interest and inserts it into DynamoDB. While the logs go to CloudWatch. This flow is as visually shown below along with a summarized view of how policies applied to Lambda should look like to support the interactions.

![AWS Lambda Push Mode Permissions](https://www.harprit.dev/visual-aws-lambda-permissions/aws-lambda-push-mode-permissions.jpg)

In this example, we are assuming that both SNS and Lambda belong to the same account. We will look at the cross account scenario in the next section.

Looking into more detailed look at the permissions, here's the Resource Based Policy allowing invocation of Lambda function by the subscribed SNS Topic.

```json
{
  "Version": "2012-10-17",
  "Id": "default",
  "Statement": [
    {
      "Sid": "sns-to-lambda-same-account",
      "Effect": "Allow",
      "Principal": {
        "Service": "sns.amazonaws.com"
      },
      "Action": "lambda:InvokeFunction",
      "Resource": "arn:aws:lambda:us-east-1:ACCOUNT-A:function:MyLambda",
      "Condition": {
        "ArnLike": {
          "AWS:SourceArn": "arn:aws:sns:us-east-1:ACCOUNT-A:MyTopic"
        }
      }
    }
  ]
}
```

While the Execution Role Policy as shown below grants access to the Lambda function to insert entries into DynamoDB table and write logs in CloudWatch.

```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "dynamodb:PutItem",
            "Resource": [
                "arn:aws:dynamodb:us-east-1:ACCOUNT-A:table/MyTable",
                "arn:aws:dynamodb:us-east-1:ACCOUNT-A:table/MyTable/index/*"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "*"
        }
    ]
}
```

### Cross Account Scenario

Next, let's see how things change in case the Lambda function and the subscribed SNS topic sit in different accounts. Say, Lambda function is defined in ACCOUNT-A while the subscribed SNS topic belongs to ACCOUNT-B.

To allow this cross account invocation simply update SNS topic's ARN, belonging to ACCOUNT-B, in the Lambda function's Resource Based Policy.

```json
{
  "Version": "2012-10-17",
  "Id": "default",
  "Statement": [
    {
      "Sid": "sns-to-lambda-cross-account",
      "Effect": "Allow",
      "Principal": {
        "Service": "sns.amazonaws.com"
      },
      "Action": "lambda:InvokeFunction",
      "Resource": "arn:aws:lambda:us-east-1:ACCOUNT-A:function:MyLambda",
      "Condition": {
        "ArnLike": {
          "AWS:SourceArn": "arn:aws:sns:us-east-1:ACCOUNT-B:MyTopic"
        }
      }
    }
  ]
}
```

While we are focusing here on Lambda, it's important to note that additional cross account permissions are required on SNS side of things as well. SNS Access Policy is counterpart of Lambda's Resource Based Policy on the SNS side. As the SNS Access Policy gets applied directly at the specific SNS topic level and let's us control who all can access the same.

For the above scenario, SNS Access Policy needs to be updated with following statement allowing access to ACCOUNT-A to be able to subscribe to the SNS topic.

```json
{
  "Effect": "Allow",
  "Principal": {
    "AWS": "arn:aws:iam::ACCOUNT-A:root"
  },
  "Action": [
    "SNS:Subscribe",
    "SNS:ListSubscriptionsByTopic",
    "SNS:Receive"
  ],
  "Resource": "arn:aws:sns:us-east-1:ACCOUNT-B:MyTopic"
}
```

## Pull Mode Permissions

For an example of pull mode scenario, let's consider a design where Lambda polls to read messages from a SNS Queue and invokes Lambda function. As in the push mode example, the function on invocation processes the messages and writes to DynamoDB table and CloudWatch. This is as shown below along with a summarized view of permissions that need to be defined in Lambda's execution role. As discussed above, in pull mode invocation there's no need for Resource Based Policies. This is because Lambda function is invoked by Lambda service itself and not any external entity.

![AWS Lambda Pull Mode Permissions](https://www.harprit.dev/visual-aws-lambda-permissions/aws-lambda-pull-mode-permissions.jpg)

Above visual shows a concise view of Lambda's Execution Role permissions required for this scenario. Check the following code snippet for more details on the same. To enable Lambda to continuously poll the SQS Queue for new messages, "GetQueueAttributes" and "ReceiveMessage" are required. "DeleteMessage" is required to remove the messages that get successfully processed by Lambda function. Remaining permissions are as discussed in above example for allowing updates to DynamoDB and CloudWatch Logs.

```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "sqs:GetQueueAttributes",
                "sqs:ReceiveMessage",
                "sqs:DeleteMessage"
            ],
            "Resource": "arn:aws:sqs:us-east-1:ACCOUNT-A:MyQueue"
        },
        {
            "Effect": "Allow",
            "Action": "dynamodb:PutItem",
            "Resource": [
                "arn:aws:dynamodb:us-east-1:ACCOUNT-A:table/MyTable",
                "arn:aws:dynamodb:us-east-1:ACCOUNT-A:table/MyTable/index/*"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "*"
        }
    ]
}
```

### Cross Account Scenario

For the cross account scenario, let's assume that the Lambda function is defined in ACCOUNT-A while the polled SQS Queue resides in ACCOUNT-B. In this case, Lambda's execution role needs to point to the ARN of the SQS Queue in ACCOUNT-B. This allows Lambda to pull messages from the queue in ACCOUNT-B. As rest of the services like DynamoDB are in the same account, no change is required for them.

```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "sqs:DeleteMessage",
                "sqs:GetQueueAttributes",
                "sqs:ReceiveMessage"
            ],
            "Resource": "arn:aws:sqs:us-east-1:ACCOUNT-B:MyQueue"
        },
        {
            "Effect": "Allow",
            "Action": "dynamodb:PutItem",
            "Resource": [
                "arn:aws:dynamodb:us-east-1:ACCOUNT-A:table/MyTable",
                "arn:aws:dynamodb:us-east-1:ACCOUNT-A:table/MyTable/index/*"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "*"
        }
    ]
}
```

As detailed in the push mode scenario, permissions in Lambda function's Resource Based Policy allow cross account invocation of the function. In case of SQS, similar resource level access control is achieved using SQS Access Policies. That's where we can define what all identities like users and roles can access a specific SQS queue and the actions they are allowed to perform.

In this scenario, a statement needs to be added in the Access Policy of SQS queue as follows. Specifically, to allow access to Lambda function's role in ACCOUNT-A so that it's able to poll and read messages from the SQS queue in ACCOUNT-B.

```json
{
  "Effect": "Allow",
  "Principal": {
    "AWS": "arn:aws:iam::ACCOUNT-A:role/service-role/LambdaCrossAccountSQSRole"
  },
  "Action": [
    "SQS:ChangeMessageVisibility",
    "SQS:DeleteMessage",
    "SQS:ReceiveMessage",
    "SQS:GetQueueAttributes"
  ],
  "Resource": "arn:aws:sqs:us-east-1:ACCOUNT-B:MyQueue"
}
```

To summarize, as shown in the visual below, cross account access needs to be opened by both the parties. With updates required in Execution Role for the Lambda function and in the Access Policy for the SQS queue.

![AWS Lambda Cross Account Pull Mode Permissions](https://www.harprit.dev/visual-aws-lambda-permissions/aws-lambda-cross-account-pull-permissions.jpg)

## Conclusion

AWS IAM is a vast yet profoundly important subject. It's vital for everyone who builds on AWS to take time to understand IAM well. As they say, security is job zero. Though this writeup only scratches the surface, I hope it helped you in some ways to have a basic model for working with AWS IAM and Lambda permissions.

------
References:
1. [AWS Lambda Documentation](https://docs.aws.amazon.com/lambda/index.html)
2. [Identity and access management in Amazon SNS](https://docs.aws.amazon.com/sns/latest/dg/sns-authentication-and-access-control.html)
3. [Identity and access management in Amazon SQS](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-authentication-and-access-control.html)
4. [Introduction to AWS Lambda & Serverless Applications](https://youtu.be/EBSdyoO3goc)
5. [Easy Authorization of AWS Lambda Functions](https://aws.amazon.com/blogs/compute/easy-authorization-of-aws-lambda-functions/)