Per-Function Policies

With Lambda, IAM and SAM

Building an application as a constellation of functions offers a huge security advantage. Every function can have its own set of environment variables and its own policy over what other resource APIs it has permission to use.

AWS Config

Consider our worker and user functions.

The user functions have policies that grants them permission to manage records in a single DynamoDB table via the DynamoDBCrudPolicy. The create function has permission to encrypt data, and the read function has permission to decrypt data with KMS via custom statements. Both have KEY_ID and TABLE_NAME environment variables so the functions know what AWS resources to use.

Resources:
  Key:
    Properties: ...
    Type: AWS::KMS::Key

  UsersTable:
    Properties: ...
    Type: AWS::Serverless::SimpleTable

  UserCreateFunction:
    Properties:
      Environment:
        Variables:
          KEY_ID: !Ref Key
          TABLE_NAME: !Ref UsersTable
      Policies:
        - DynamoDBCrudPolicy:
            TableName: !Ref UsersTable
        - Statement:
            - Action:
                - kms:Encrypt
              Effect: Allow
              Resource: !GetAtt Key.Arn
          Version: 2012-10-17
    Type: AWS::Serverless::Function

  UserReadFunction:
    Properties:
      Environment:
        Variables:
          KEY_ID: !Ref Key
          TABLE_NAME: !Ref UsersTable
      Policies:
        - DynamoDBReadPolicy:
            TableName: !Ref UsersTable
        - KMSDecryptPolicy:
            KeyId: !Ref Key
    Type: AWS::Serverless::Function

From template.yml

The worker functions also have fine-grained policies. One has permission to create objects via the S3CrudPolicy and one has permission to only list and delete objects via a custom statement. Both have a BUCKET environment variable so the functions know what resource to use.

Resources:
  Bucket:
    Type: AWS::S3::Bucket

  WorkerFunction:
    Properties:
      Environment:
        Variables:
          BUCKET: !Ref Bucket
      Policies:
        - S3CrudPolicy:
            BucketName: !Ref Bucket
    Type: AWS::Serverless::Function

  WorkerPeriodicFunction:
    Properties:
      Environment:
        Variables:
          BUCKET: !Ref Bucket
      Policies:
        - Statement:
            - Action:
                - s3:DeleteObject
                - s3:ListObjects
              Effect: Allow
              Resource: !Sub "arn:aws:s3:::${Bucket}/*"
    Type: AWS::Serverless::Function

From template.yml

The security implications are massive.

Our users API has limited the sensitive operation of decrypting data to a single API endpoint.

Our worker functions don’t know the name of the database nor do they have any DynamoDB permissions at all. We could now perform un-trusted work like running a user-supplied script with confidence it can never access our database.

Consider a common alternative where an app has a single set of IAM keys via AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables and a liberal policy like AdministratorAccess. A vulnerability in any one endpoint would expose keys to the entire AWS kingdom.

SAM Policy Templates

Note how the Policies section has two different formats for writing policies.

In some cases we specify a Statement, a full IAM policy body. The IAM policies docs and CloudFormation IAM role docs offer guidance for crafting these statements.

In other cases we specify simple statement like DynamoDBCrudPolicy or S3CrudPolicy scoped to a single resource. This is a feature of SAM called “policy templates”.

A SAM template is a dialect of a CloudFormation template that makes it simpler to write the configuration for a serverless app. When we deploy this template a “transform” takes place behind the scenes, and turns our simplified policy config into a full IAM policy body.

The SAM Policy Templates doc lists what policy templates are available and how they are transformed.

See a transformed AWS::IAM::Role resource…  

{
    "Resources": {
        "UserCreateFunctionRole": {
            "Type": "AWS::IAM::Role",
            "Properties": {
                "ManagedPolicyArns": [
                    "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole",
                    "arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess"
                ],
                "Policies": [
                    {
                        "PolicyName": "UserCreateFunctionRolePolicy0",
                        "PolicyDocument": {
                            "Statement": [
                                {
                                    "Action": [
                                        "dynamodb:GetItem",
                                        "dynamodb:DeleteItem",
                                        "dynamodb:PutItem",
                                        "dynamodb:Scan",
                                        "dynamodb:Query",
                                        "dynamodb:UpdateItem",
                                        "dynamodb:BatchWriteItem",
                                        "dynamodb:BatchGetItem"
                                    ],
                                    "Resource": {
                                        "Fn::Sub": [
                                            "arn:${AWS::Partition}:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${tableName}",
                                            {
                                                "tableName": {
                                                    "Ref": "UsersTable"
                                                }
                                            }
                                        ]
                                    },
                                    "Effect": "Allow"
                                }
                            ]
                        }
                    },
                    {
                        "PolicyName": "UserCreateFunctionRolePolicy2",
                        "PolicyDocument": {
                            "Version": "2012-10-17",
                            "Statement": [
                                {
                                    "Action": [
                                        "kms:Encrypt"
                                    ],
                                    "Resource": {
                                        "Fn::GetAtt": [
                                            "Key",
                                            "Arn"
                                        ]
                                    },
                                    "Effect": "Allow"
                                }
                            ]
                        }
                    }
                ],
                "AssumeRolePolicyDocument": {
                    "Version": "2012-10-17",
                    "Statement": [
                        {
                            "Action": [
                                "sts:AssumeRole"
                            ],
                            "Effect": "Allow",
                            "Principal": {
                                "Service": [
                                    "lambda.amazonaws.com"
                                ]
                            }
                        }
                    ]
                }
            }
        }
    }
}

Summary

When building an app as a set of functions we now can:

  • Set a separate environment for every function
  • Apply separate policies to every function
  • Follow the principal of least privilege

We no longer have to worry about:

  • Shared-all app secrets
  • Shared-all app policies

Lambda makes building apps significantly more secure.