Attribute-Based Access Controls for AWS Lambda Functions

Posted on

Event-driven, serverless functions have become a defining feature of many modern cloud architectures. With recent capabilities such as AWS Lambda URLs and AWS Lambda Containers, AWS has made it clear that Lambda Functions are a platform that teams can use to deliver increasingly sophisticated services without worrying about managing underlying compute resources.

Today, AWS announced another advancement for their Lambda Functions platform: Attribute-Based Access Control (ABAC). At its core, ABAC support brings more granular permissions that are automatically applied based on IAM role tags, Lambda tags, or both. This update builds on well-established Role-Based Access Control (RBAC) principles while making it possible to implement granular controls without permissions updates for every new user and resource.

What are Attributes?

Attributes are a key or key/value pair - in AWS, these attributes are called tags. Tags can be applied to multiple roles – for example identifying members of a team.

Across many teams, your organization may share a lot of common roles, and, in order to enforce the principle of least access, you may wish to restrict access across teams such that Developer Team members cannot access Ops Team resources and vice versa.

Using ABAC for AWS Lambda Functions

For this example, we’ll define a new role ‘abac-test’ as well as a team attribute that must have the value of either ‘developers’ or ‘ops’ to enable creating a Lambda function.

Controlling access to creating an AWS Lambda function via IAM Roles and Attributes

To get started, let’s first install the Pulumi AWS Classic provider:

$ npm install @pulumi/aws
$ dotnet add package Pulumi.Aws
$ pip3 install pulumi_aws
$ go get github.com/pulumi/pulumi-aws/sdk/v5

Now, create a new IAM Role that a user can assume:

const role = new aws.iam.Role("abac-test", {
    assumeRolePolicy: JSON.stringify({
        Version: "2012-10-17",
        Statement: [
            {
                Sid: "",
                Effect: "Allow",
                Principal: {
                    AWS: "arn:aws:iam::MYACCOUNTID:root",
                },
                Action: "sts:AssumeRole",
            },
        ],
    }),
});
var role = new Aws.Iam.Role("abac-test", new Aws.Iam.RoleArgs
{
    AssumeRolePolicy = JsonSerializer.Serialize(new Dictionary<string, object?>
    {
        { "Version", "2012-10-17" },
        { "Statement", new[]
            {
                new Dictionary<string, object?>
                {
                    { "Action", "sts:AssumeRole" },
                    { "Effect", "Allow" },
                    { "Principal", new Dictionary<string, object?>
                    {
                        { "AWS", "arn:aws:iam::MYACCOUNTID:root" },
                    } },
                },
            }
         },
    }),
});
role = aws.iam.Role("abac-test",
                    assume_role_policy=json.dumps({
                        "Version": "2012-10-17",
                        "Statement": [{
                            "Action": "sts:AssumeRole",
                            "Effect": "Allow",
                            "Principal": {
                                "AWS": "arn:aws:iam::MYACCOUNTID:root",
                            },
                        }],
                    }),
                    tags={
                        "Team": "developers",
                    })
role, err := iam.NewRole(ctx, "abac-test", &iam.RoleArgs{
  AssumeRolePolicy: pulumi.String(`{
    "Version": "2012-10-17",
    "Statement": [{
      "Sid": "",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::MYACCOUNTID:root"
      },
      "Action": "sts:AssumeRole"
    }]
  }`),
})
if err != nil {
  return err
}
  abacTest:
      type: aws:iam:Role
      properties:
          assumeRolePolicy:
              Fn::ToJSON:
                  Version: 2012-10-17
                  Statement:
                      - Action: sts:AssumeRole
                        Effect: Allow
                        Principal:
                            AWS: arn:aws:iam::MYACCOUNTID:root

Let’s now create a new IAM Policy that allows a user to create Lambda functions:

const createPolicy = new aws.iam.Policy(
    "createLambda",
    {
        policy: JSON.stringify({
            Version: "2012-10-17",
            Statement: [
                {
                    Effect: "Allow",
                    Action: ["lambda:CreateFunction", "lambda:TagResource"],
                    Resource: "arn:aws:lambda:*:*:function:*",
                    Condition: {
                        // the requesting resource must have a tag with
                        // team = developers/ops
                        StringEquals: {
                            "aws:RequestTag/Team": ["developers", "ops"], // must match
                        },
                        // your request must contain these tags
                        // eg. project, but you might not have this defined yet
                        "ForAllValues:StringEquals": {
                            "aws:TagKeys": "Team", // must exist
                        },
                    },
                },
                {
                    // Pulumi needs to check what functions exist and what versions exist to create updates and new versions
                    Effect: "Allow",
                    Action: [
                        "lambda:GetFunction",
                        "lambda:ListVersionsByFunction",
                        "lambda:GetFunctionCodeSigningConfig",
                    ],
                    Resource: "*",
                },
                {
                    Effect: "Allow",
                    // These are assume role policies
                    Action: ["iam:ListRoles", "iam:PassRole"],
                    Resource: "*",
                },
            ],
        }),
    },
);
var createPolicy = new Aws.Iam.Policy("createPolicy", new Aws.Iam.PolicyArgs
{
    PolicyDocument = JsonSerializer.Serialize(new Dictionary<string, object?>
    {
        { "Version", "2012-10-17" },
        { "Statement", new[]
            {
                new Dictionary<string, object?>
                {
                    { "Effect", "Allow" },
                    { "Action", new[]
                        {
                            "lambda:CreateFunction",
                            "lambda:TagResource",
                        }
                     },
                    { "Resource", "arn:aws:lambda:*:*:function:*" },
                    { "Condition", new Dictionary<string, object?>
                    {
                        // the requesting resource must have a tag with
                        // team = developers/ops
                        { "StringEquals", new Dictionary<string, object?>
                        {
                            { "aws:RequestTag/Team", new[]
                                {
                                    "developers",
                                    "ops",
                                }
                             },
                        } },
                        // your request must contain these tags
                        // eg. project, but you might not have this defined yet
                        { "ForAllValues:StringEquals", new Dictionary<string, object?>
                        {
                            { "aws:TagKeys", new[]
                                {
                                    "Team",
                                }
                             },
                        } },
                    } },
                },
                new Dictionary<string, object?>
                {
                    // Pulumi needs to check what functions exist and what versions exist to create updates and new versions
                    { "Effect", "Allow" },
                    { "Action", new[]
                        {
                            "lambda:GetFunction",
                            "lambda:ListVersionsByFunction",
                            "lambda:GetFunctionCodeSigningConfig",
                        }
                     },
                    { "Resource", "*" },
                },
                new Dictionary<string, object?>
                {
                    // These are assume role policies
                    { "Effect", "Allow" },
                    { "Action", new[]
                        {
                            "iam:ListRoles",
                            "iam:PassRole",
                        }
                     },
                    { "Resource", "*" },
                },
            }
         },
    }),
});
create_policy = aws.iam.Policy("createPolicy", policy=json.dumps({
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "lambda:CreateFunction",
                "lambda:TagResource",
            ],
            "Resource": "arn:aws:lambda:*:*:function:*",
            "Condition": {
                #the requesting resource must have a tag with
                #team = developers/ops
                "StringEquals": {
                    "aws:RequestTag/Team": [
                        "developers",
                        "ops",
                    ],
                },
                # your request must contain these tags
                # eg. project, but you might not have this defined yet
                "ForAllValues:StringEquals": {
                    "aws:TagKeys": ["Team"],
                },
            },
        },
        {
            # Pulumi needs to check what functions exist and what versions exist to create updates and new versions
            "Effect": "Allow",
            "Action": [
                "lambda:GetFunction",
                "lambda:ListVersionsByFunction",
                "lambda:GetFunctionCodeSigningConfig",
            ],
            "Resource": "*",
        },
        {
            # These are assume role policies
            "Effect": "Allow",
            "Action": [
                "iam:ListRoles",
                "iam:PassRole",
            ],
            "Resource": "*",
        },
    ],
}))
createPolicy, err := iam.NewRolePolicy(ctx, "createPolicy", &iam.RoleArgs{
  Policy: pulumi.String(`{
    "Version": "2012-10-17",
    "Statement": [{
      "Sid": "",
      "Effect": "Allow",
      "Condition": map[string]interface{}{
        "StringEquals": map[string]interface{}{
          "aws:RequestTag/Team": []string{
            "developers",
            "ops",
          },
        },
        "ForAllValues:StringEquals": map[string]interface{}{
          "aws:TagKeys": []string{
            "Team",
          },
        },
      },
      "Resource": "arn:aws:lambda:*:*:function:*",
      "Action": []string{
        "lambda:CreateFunction",
        "lambda:TagResource",
      },
    },
    {
      "Effect": "Allow",
      "Resource": "*",
      "Action": []string{
        "lambda:GetFunction",
        "lambda:ListVersionsByFunction",
        "lambda:GetFunctionCodeSigningConfig",
      },
    },
    {
      {
        "Effect": "Allow",
        "Resource": "*",
        "Action": []string{
          "lambda:CreateFunction",
          "lambda:TagResource",
        },
      }
    }]
  }`),
})
if err != nil {
  return err
}
  createPolicy:
    type: aws:iam:Policy
    properties:
      policy:
        Fn::ToJSON:
          Version: 2012-10-17
          Statement:
            - Effect: Allow
              Action:
                - lambda:CreateFunction
                - lambda:TagResource
              Resource: 'arn:aws:lambda:*:*:function:*'
              Condition:
                StringEquals:
                  aws:RequestTag/Team:
                    - developers
                    - ops
                ForAllValues:StringEquals:
                  aws:TagKeys:
                    - Team
            - Effect: Allow
              Action:
                - lambda:GetFunction
                - lambda:ListVersionsByFunction
                - lambda:GetFunctionCodeSigningConfig
              Resource: '*'
            - Effect: Allow
              Action:
                - iam:ListRoles
                - iam:PassRole
              Resource: '*'

Notice, as part of the policy condition, we ensure that the requesting resource must have a tag with team: developers or team: ops.

Let’s ensure that this policy is attached to the Role we initially created:

const rpa = new aws.iam.RolePolicyAttachment("rpa", {
    policyArn: createPolicy.arn,
    role: role,
})
var rpa = new Aws.Iam.RolePolicyAttachment("rpa", new Aws.Iam.RolePolicyAttachmentArgs
{
    PolicyArn = createPolicy.Arn,
    Role = role.Name,
});
rpa = aws.iam.RolePolicyAttachment("rpa",
    policy_arn=create_policy.arn,
    role=role.name)
rpa, err := iam.NewRolePolicyAttachment(ctx, "rpa", &iam.RolePolicyAttachmentArgs{
    PolicyArn: createPolicy.Arn,
    Role:      role.Name,
})
if err != nil {
    return err
}
  rpa:
    type: aws:iam:RolePolicyAttachment
    properties:
      policyArn: ${createPolicy.arn}
      role: ${abacTest.name}

We can deploy these resources using the command pulumi update:

pulumi up
Previewing update (abac-testing)

View Live: https://app.pulumi.com/stack72/aws-abac-test/abac-testing/previews/c6fb4580-2706-47ee-8a56-b8942631254e

     Type                             Name                        Plan
 +   pulumi:pulumi:Stack              aws-abac-test-abac-testing  create
 +   ├─ aws:iam:Policy                createLambda                create
 +   ├─ aws:iam:Role                  abac-test                   create
 +   └─ aws:iam:RolePolicyAttachment  createPolicy                create

Resources:
    + 4 to create

Do you want to perform this update? yes
Updating (abac-testing)

View Live: https://app.pulumi.com/stack72/aws-abac-test/abac-testing/updates/1

     Type                             Name                        Status
 +   pulumi:pulumi:Stack              aws-abac-test-abac-testing  created
 +   ├─ aws:iam:Role                  abac-test                   created
 +   ├─ aws:iam:Policy                createLambda                created
 +   └─ aws:iam:RolePolicyAttachment  createPolicy                created

Resources:
    + 4 created

Duration: 7s

For the purposes of testing, we are now going to create a specific AWS Provider resource that we can pass the IAM Role details to assume.

const abacUserProvider = new aws.Provider(
    "assumeRole",
    {
        // assume the previously created role with the ABAC configured policy into our provider
        assumeRole: {
            roleArn: role.arn,
            sessionName: "abacTesting",
        },
        region: "us-east-1",
    },
    {
        // we want to depend on the role policy attachment being in place before we create the provider
        dependsOn: [rpa],
    }
);
var abacUserProvider = new Aws.Provider("abacUserProvider", new Aws.ProviderArgs
{
    AssumeRole = new Aws.Config.Inputs.AssumeRoleArgs
    {
        RoleArn = role.Arn,
        SessionName = "abacTesting",
    },
    Region = "us-east-1",
}, new CustomResourceOptions
{
    DependsOn =
    {
        rpa,
    },
});
abac_user_provider = pulumi.providers.Aws("abacUserProvider",
    assume_role=aws.config.AssumeRoleArgs(
        role_arn=role.arn,
        session_name="abacTesting",
    ),
    region="us-east-1",
    opts=pulumi.ResourceOptions(depends_on=[rpa]))
abacUserProvider, err := providers.Newaws(ctx, "abacUserProvider", &providers.awsArgs{
    AssumeRole: config.AssumeRole{
        RoleArn:     role.Arn,
        SessionName: "abacTesting",
    },
    Region: "us-east-1",
}, pulumi.DependsOn([]pulumi.Resource{
    rpa,
}))
if err != nil {
    return err
}
  abacUserProvider:
    type: pulumi:providers:aws
    properties:
      assumeRole:
        roleArn: ${role.arn}
        sessionName: "abacTesting"
      region: "us-east-1"
    options:
      dependsOn:
        - ${rpa}

Now we can create an AWS Lambda with Pulumi as follows:

const lambdaRole = new aws.iam.Role("iamRole", {
    assumeRolePolicy: aws.iam.assumeRolePolicyForPrincipal({
        Service: "lambda.amazonaws.com",
    }),
});

const deleteLambdaPolicy = new aws.iam.Policy(
    "deleteLambdaPolicy",
    {
        policy: JSON.stringify({
            Version: "2012-10-17",
            Statement: [
                {
                    Effect: "Allow",
                    Action: ["lambda:DeleteFunction"],
                    Resource: "*",
                },
            ],
        }),
    },
    { parent: lambdaRole }
);

new aws.iam.RolePolicyAttachment(
    `deletePolicy`,
    {
        policyArn: deletePolicy.arn,
        role: role,
    },
    { parent: deletePolicy })

const lambda = new aws.lambda.Function(
    "lambdaFunction",
    {
        code: new pulumi.asset.AssetArchive({
            ".": new pulumi.asset.FileArchive("./app"),
        }),
        runtime: "nodejs12.x",
        role: lambdaRole.arn,
        handler: "index.handler",
    },
    { provider: abacUserProvider }
);
var lambdaRole = new Aws.Iam.Role("lambdaRole", new Aws.Iam.RoleArgs
{
    AssumeRolePolicy = JsonSerializer.Serialize(new Dictionary<string, object?>
    {
        { "Version", "2012-10-17" },
        { "Statement", new[]
            {
                new Dictionary<string, object?>
                {
                    { "Action", "sts:AssumeRole" },
                    { "Effect", "Allow" },
                    { "Principal", new Dictionary<string, object?>
                    {
                        { "Service", "lambda.amazonaws.com" },
                    } },
                },
            }
         },
    }),
});
var deletePolicy = new Aws.Iam.Policy("deletePolicy", new Aws.Iam.PolicyArgs
{
    PolicyDocument = JsonSerializer.Serialize(new Dictionary<string, object?>
    {
        { "Version", "2012-10-17" },
        { "Statement", new[]
            {
                new Dictionary<string, object?>
                {
                    { "Action", "lambda:DeleteFunction" },
                    { "Effect", "Allow" },
                    { "Resource", "*" },
                },
            }
         },
    }),
});
var deletePolicyAttachment = new Aws.Iam.RolePolicyAttachment("deletePolicyAttachment", new Aws.Iam.RolePolicyAttachmentArgs
{
    Role = role.Name,
    PolicyArn = deletePolicy.Arn,
});
var lambdaFunction = new Aws.Lambda.Function("lambdaFunction", new Aws.Lambda.FunctionArgs
{
    Code = new FileArchive("./app"),
    Role = lambdaRole.Arn,
    Handler = "index.handler",
    Runtime = "nodejs14.x",
}, new CustomResourceOptions
{
    Provider = abacUserProvider,
});
lambda_role = aws.iam.Role("lambdaRole", assume_role_policy=json.dumps({
    "Version": "2012-10-17",
    "Statement": [{
        "Action": "sts:AssumeRole",
        "Effect": "Allow",
        "Principal": {
            "Service": "lambda.amazonaws.com",
        },
    }],
}))
delete_policy = aws.iam.Policy("deletePolicy", policy=json.dumps({
    "Version": "2012-10-17",
    "Statement": [{
        "Action": "lambda:DeleteFunction",
        "Effect": "Allow",
        "Resource": "*",
    }],
}))
delete_policy_attachment = aws.iam.RolePolicyAttachment("deletePolicyAttachment",
    role=role.name,
    policy_arn=delete_policy.arn)
lambda_function = aws.lambda_.Function("lambdaFunction",
    code=pulumi.FileArchive("./app"),
    role=lambda_role.arn,
    handler="index.handler",
    runtime="nodejs14.x",
    opts=pulumi.ResourceOptions(provider=abac_user_provider))
lambdaRole, err := iam.NewRole(ctx, "lambdaRole", &iam.RoleArgs{
  AssumeRolePolicy: pulumi.String(`{
    "Version": "2012-10-17",
    "Statement": [{
      "Effect": "Allow",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }]
  }`),
})
if err != nil {
  return err
}

deletePolicy, err := iam.NewRolePolicy(ctx, "deletePoicy", &iam.RoleArgs{
  Policy: pulumi.String(`{
    "Version": "2012-10-17",
    "Statement": [{
      "Effect": "Allow",
      "Resource": "*",
      "Action": []string{
        "lambda:DeleteFunction",
      },
    }]
  }`),
})
if err != nil {
  return err
}

_, err = iam.NewRolePolicyAttachment(ctx, "deletePolicyAttachment", &iam.RolePolicyAttachmentArgs{
  Role:      role.Name,
  PolicyArn: deletePolicy.Arn,
})
if err != nil {
  return err
}

_, err = lambda.NewFunction(ctx, "lambdaFunction", &lambda.FunctionArgs{
  Code:    pulumi.NewFileArchive("./app"),
  Role:    lambdaRole.Arn,
  Handler: pulumi.String("index.handler"),
  Runtime: pulumi.String("nodejs14.x"),
}, pulumi.Provider(abacUserProvider))
if err != nil {
  return err
}
  lambdaRole:
    type: aws:iam:Role
    properties:
      assumeRolePolicy:
        Fn::ToJSON:
          Version: 2012-10-17
          Statement:
            - Action: sts:AssumeRole
              Effect: Allow
              Principal:
                Service: lambda.amazonaws.com
  deletePolicy:
    type: aws:iam:Policy
    properties:
      policy:
        Fn::ToJSON:
          Version: 2012-10-17
          Statement:
            - Action: lambda:DeleteFunction
              Effect: Allow
              Resource: '*'
  deletePolicyAttachment:
    type: aws:iam:RolePolicyAttachment
    properties:
      role: ${abacRole.name}
      policyArn: ${deletePolicy.arn}
  lambdaFunction:
    type: aws:lambda:Function
    properties:
      code:
        Fn::FileArchive: ./app
      role: ${lambdaRole.arn}
      handler: index.handler
      runtime: nodejs14.x

If we try and deploy the lambda, we will get the following using the role we are going to assume then we will get an error as the lambda function has no Team tag.

pulumi up
Previewing update (abac-testing)

View Live: https://app.pulumi.com/stack72/aws-abac-test/abac-testing/previews/7fdc10b2-5e44-497e-a067-f2e8b257496c

     Type                                   Name                        Plan
     pulumi:pulumi:Stack                    aws-abac-test-abac-testing
 +   ├─ aws:iam:Role                        iamRole                     create
     ├─ aws:iam:Role                        abac-test
 +   │  └─ aws:iam:Policy                   deletePolicy                create
 +   │     └─ aws:iam:RolePolicyAttachment  deletePolicy                create
 +   ├─ pulumi:providers:aws                assumeRole                  create
 +   └─ aws:lambda:Function                 lambdaFunction              create

Resources:
    + 5 to create
    4 unchanged

Do you want to perform this update? yes
Updating (abac-testing)

View Live: https://app.pulumi.com/stack72/aws-abac-test/abac-testing/updates/2

     Type                                   Name                        Status                  Info
     pulumi:pulumi:Stack                    aws-abac-test-abac-testing  **failed**              1 error
 +   ├─ aws:iam:Role                        iamRole                     created
     ├─ aws:iam:Role                        abac-test
 +   │  └─ aws:iam:Policy                   deletePolicy                created
 +   │     └─ aws:iam:RolePolicyAttachment  deletePolicy                created
 +   ├─ pulumi:providers:aws                assumeRole                  created
 +   └─ aws:lambda:Function                 lambdaFunction              **creating failed**     1 error

Diagnostics:
  pulumi:pulumi:Stack (aws-abac-test-abac-testing):
    error: update failed

  aws:lambda:Function (lambdaFunction):
    error: 1 error occurred:
    	* error creating Lambda Function (1): AccessDeniedException:
    	status code: 403, request id: 4a8676f2-2b68-4d42-ad4b-f0e10ca31afd

Resources:
    + 4 created
    4 unchanged

Duration: 6s

If we add the correct Team tag to the Lambda function then the deploy will proceed as expected:

pulumi up
Previewing update (abac-testing)

View Live: https://app.pulumi.com/stack72/aws-abac-test/abac-testing/previews/0afa267f-5f9f-43f5-9e00-d3ee19dbf2b8

     Type                    Name                        Plan
     pulumi:pulumi:Stack     aws-abac-test-abac-testing
 +   └─ aws:lambda:Function  lambdaFunction              create

Resources:
    + 1 to create
    8 unchanged

Do you want to perform this update? yes
Updating (abac-testing)

View Live: https://app.pulumi.com/stack72/aws-abac-test/abac-testing/updates/3

     Type                    Name                        Status
     pulumi:pulumi:Stack     aws-abac-test-abac-testing
 +   └─ aws:lambda:Function  lambdaFunction              created

Resources:
    + 1 created
    8 unchanged

Duration: 9s

Try It Out!

Lambda ABAC capabilities will help you to simplify governance by helping you to standardize roles and maintain security boundaries between teams. A common use case is using tags to restrict/enable the invoke action for a function in addition to the create action demonstrated above. Give Lambda ABAC functionality a try and let us know what you think in Community Slack.