AWS Compute Blog

Easy Authorization of AWS Lambda Functions

Tim Wagner Tim Wagner, AWS Lambda

Authorization and security is a critical feature of every AWS service, including Lambda. But enabling developers to authorize and secure their Lambda functions isn’t enough — Lambda should also be easy to use, quick to set up, and flexible to configure. In this post we talk about how Lambda was designed to achieve both outcomes.

tldr: If you’re using the Lambda console to process events and they come from same account that owns your function, we take care of setting up authorization for you, and you can skip this article. Read on to learn more about how authorization works, to see the command line approach, or for advanced use cases like cross-account access.

First, let’s define a few terms:

  • Policy: A policy is a set of capabilities. It answers the “who can do what” question.
  • Role-based Authorization: In this approach to authorization, policies are attached to real users and temporary or simulated users (roles). The policy defines what the role can do, and services enforce that the policy terms are met. For example, if a user named Joe has a policy that lets him create Lambda functions, then Lambda will check this privilege each time Joe attempts to make a new function and will allow that activity to proceed.
  • Resource-based Authorization: Policies can also be attached to resources, such as Lambda functions. While not required, resource policies also often refer to “foreign” resources, such as restricting the set of S3 buckets that are allowed to send events to a specific Lambda function. Resource policies are especially useful in authorizing such “on-behalf-of” activities and for enabling cross-account access.

Note that role and resource-based authentication are additive, not exclusive, and AWS Lambda supports both types. Let’s take a look at some scenarios and see how authorization is handled in each.

Reminder: Each Lambda function has an execution role that determines the capabilities of the function (e.g., which AWS services it can call). The account that owns the Lambda function (and thus controls its behavior) is not necessarily the same as the role/user that calls the function. For clarity, we’ll distinguish invokers (and their invocation role when calling the Lambda API) from execution (and the execution role used for the Lambda function) in the descriptions below. In this article we’re mostly focusing on the invocation aspect rather than what the function is allowed to do once it starts running. See the Lambda Getting Started docs for more on the latter.

Scenario 1: Calling a Lambda function from a web service in the same account.

In this scenario the signer of the request determines the identity (user or role) of the invoker, and that in turn identifies one or more policies that specify what’s allowed. The union of those policies determines whether the function call is permitted to occur. In this scenario the invoker and the Lambda function owner are the same AWS account, but they’re not required to be the same role. Resource policies aren’t needed in this simple scenario; you just ensure that the user (or role) calling Lambda has permission to invoke functions. The following policy enables a caller to access a specific Lambda function owned by the same account in the us-east-1 region:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": ["lambda:InvokeFunction"],
      "Effect": "Allow",
      "Resource": "arn:aws:lambda:us-east-1:<account number>:<function name>"
    }
  ]
}

By changing the resource name to “arn:aws:lambda:*:*:*” you can allow access to any function in any region (the account check is still applied even if the resource doesn’t list it).

Scenario 2: Calling a Lambda function from a web service in a different account.

This is the same as the scenario above except that the account of the invoker and the account of the Lambda function owner are different. In this case resource policies are the easiest way to authorize the activity: The resource policy on the function being called will enable access by the “foreign” account of the caller. Here’s a sample of how to authorize account 012345678912 to call “MyFunction” from the command line:

$ aws lambda add-permission \
  --function-name MyFunction \
  --region us-west-2 \
  --statement-id Id-123 \
  --action "lambda:InvokeFunction" \
  --principal 012345678912 \
  --profile adminuser

You can also view the resource policies that apply to a function by calling get-policy:

$ aws lambda get-policy \
--function-name function-name \
--profile adminuser

The profile argument allows you to specify the role with which the add-permission call itself is made. If you don’t supply the profile argument, the CLI will attempt to pick up credentials from its configuration (set using the “aws configure” command) or environment variables. If you’re on EC2 and using instance credentials you can skip this argument and the CLI will automatically pick up the instance’s role. See the CLI credential documentation for more details.

Note that the user (or role) making the call still needs permission to invoke the Lambda function as in Scenario 1. Think of this as an “and” condition: As a user (or role) you’ll need permission to call Lambda functions AND you need the function owner’s permission to use his or her particular function.

Scenario 3: Triggering a Lambda function from an Amazon S3 bucket notification in another account.

In Scenario 2 the call from another account was made directly to Lambda. In this scenario the call is indirect: S3 sends the event on behalf of the bucket owner instead of that account making the call itself. The add-permission call is slightly different:

$ aws lambda add-permission \
  --function-name MyFunction \
  --region us-west-2 \
  --statement-id Id-123 \
  --action "lambda:InvokeFunction" \
  --principal s3.amazonaws.com \
  --source-arn arn:aws:s3:::<source-bucket> \
  --source-account <account number> \
  --profile adminuser

Note that the principal becomes the S3 service (it gets a service name as opposed to an account number) and the actual account number moves to the “source-account” parameter. There’s also a new argument, source-arn, which contains the name of the bucket. This mechanism gives you great flexibility in how (and what) you choose to authorize S3 to do on your behalf:

  • Specify both bucket name and owning account. This is our recommended approach because it provides the strictest security: By providing both arguments to add-permission, you authorize a specific S3 bucket owned by a specific account to send bucket notification events to your Lambda function.
  • Specify only the bucket name. This enables notification events from the named bucket to be sent to your Lambda function regardless of who currently owns the bucket.
  • Specify only the owning account. This enables events from any bucket owned by this account to be sent to your Lambda function.
  • Specify neither (not recommended). Any S3 bucket (owned by any account) can send events to your function. As the function owner you should use care in selecting this option, since you’ll have no control over the set of events reaching your function.

Note: Amazon SNS (Simple Notification Service) events sent to Lambda works the same way, with “sns.amazonaws.com” replacing “s3.amazonaws.com” as the principal.

Scenario 4: Processing Amazon Kinesis records or Amazon DynamoDB updates with AWS Lambda.

This scenario is like Scenario 1 above, except that things get turned around: Instead of authorizing a user or role to call your function, you authorize your function to read from Amazon Kinesis or Amazon DynamoDB. The policy looks similar to scenario 1’s policy, but the name of the service changes and you need a slightly different set of actions; here’s how it looks for Kinesis:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": [
        "kinesis:DescribeStream",
        "kinesis:ListStreams",
        "kinesis:GetShardIterator",
        "kinesis:GetRecords"
      ],
      "Effect": "Allow",
      "Resource": "arn:aws:kinesis:us-east-1:<account number>:stream/<kinesis stream name>"
    }
  ]
}

Your execution role also needs the “standard” capabilities to create and update logs and make calls on your behalf; see the AWS Lambda documentation for details on setting up execution roles and authorizing code to use other AWS services. You can roll the Kinesis authorization into an existing execution role or attach it as a separate managed policy. Read more about managed policies and IAM roles to help you choose among the different options. If you’re keeping it all together, here’s how it might look with both Kinesis access and standard logging access (in the policy below, we’re enabling access to any Kinesis stream owned by the same account; you can use the ARN technique above to restrict it to a specific stream):

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "lambda:InvokeFunction"
      ],
      "Resource": [
        "*"
      ]
    },
    {
      "Effect": "Allow",
      "Action": [
        "kinesis:GetRecords",
        "kinesis:GetShardIterator",
        "kinesis:DescribeStream",
        "kinesis:ListStreams",
        "logs:CreateLogGroup",
        "logs:CreateLogStream",
        "logs:PutLogEvents"
      ],
      "Resource": "*"
    }
  ]
}

I hope this post was helpful in explaining the different ways you can make your Lambda functions available to callers across different accounts and resources. As always, we appreciate your feedback and comments. Happy Lambda coding!

-Tim