--- title: "Attribute-Based Access Controls for AWS Lambda Functions" date: 2022-07-19 meta_desc: "Applying Attribute-Based Access Controls to AWS Lambda Functions" meta_image: meta.png authors: - paul-stack - lee-briggs - isaac-harris tags: - aws - lambda - iam - serverless --- Event-driven, serverless functions have become a defining feature of many modern cloud architectures. With recent capabilities such as [AWS Lambda URLs](https://www.pulumi.com/blog/lambda-urls-launch/) and [AWS Lambda Containers](https://www.pulumi.com/blog/aws-lambda-container-support/), 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](https://aws.amazon.com/blogs/compute/scaling-aws-lambda-permissions-with-attribute-based-access-control-abac/) (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](./figure1.png) To get started, let's first install the Pulumi AWS Classic provider: {{< chooser language "typescript,csharp,python,go" >}} {{% choosable language typescript %}} ```bash $ npm install @pulumi/aws ``` {{% /choosable %}} {{% choosable language csharp %}} ```bash $ dotnet add package Pulumi.Aws ``` {{% /choosable %}} {{% choosable language python %}} ```bash $ pip3 install pulumi_aws ``` {{% /choosable %}} {{% choosable language go %}} ```bash $ go get github.com/pulumi/pulumi-aws/sdk/v5 ``` {{% /choosable %}} {{< /chooser >}} Now, create a new IAM Role that a user can assume: {{< chooser language "typescript,csharp,python,go,yaml" >}} {{% choosable language typescript %}} ```typescript 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", }, ], }), }); ``` {{% /choosable %}} {{% choosable language csharp %}} ```csharp var role = new Aws.Iam.Role("abac-test", new Aws.Iam.RoleArgs { AssumeRolePolicy = JsonSerializer.Serialize(new Dictionary { { "Version", "2012-10-17" }, { "Statement", new[] { new Dictionary { { "Action", "sts:AssumeRole" }, { "Effect", "Allow" }, { "Principal", new Dictionary { { "AWS", "arn:aws:iam::MYACCOUNTID:root" }, } }, }, } }, }), }); ``` {{% /choosable %}} {{% choosable language python %}} ```python 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", }) ``` {{% /choosable %}} {{% choosable language go %}} ```go 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 } ``` {{% /choosable %}} {{% choosable language yaml %}} ```yaml 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 ``` {{% /choosable %}} {{< /chooser >}} Let's now create a new IAM Policy that allows a user to create Lambda functions: {{< chooser language "typescript,csharp,python,go,yaml" >}} {{% choosable language typescript %}} ```typescript 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: "*", }, ], }), }, ); ``` {{% /choosable %}} {{% choosable language csharp %}} ```csharp var createPolicy = new Aws.Iam.Policy("createPolicy", new Aws.Iam.PolicyArgs { PolicyDocument = JsonSerializer.Serialize(new Dictionary { { "Version", "2012-10-17" }, { "Statement", new[] { new Dictionary { { "Effect", "Allow" }, { "Action", new[] { "lambda:CreateFunction", "lambda:TagResource", } }, { "Resource", "arn:aws:lambda:*:*:function:*" }, { "Condition", new Dictionary { // the requesting resource must have a tag with // team = developers/ops { "StringEquals", new Dictionary { { "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 { { "aws:TagKeys", new[] { "Team", } }, } }, } }, }, new Dictionary { // 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 { // These are assume role policies { "Effect", "Allow" }, { "Action", new[] { "iam:ListRoles", "iam:PassRole", } }, { "Resource", "*" }, }, } }, }), }); ``` {{% /choosable %}} {{% choosable language python %}} ```python 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": "*", }, ], })) ``` {{% /choosable %}} {{% choosable language go %}} ```go 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 } ``` {{% /choosable %}} {{% choosable language yaml %}} ```yaml 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: '*' ``` {{% /choosable %}} {{< /chooser >}} 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: {{< chooser language "typescript,csharp,python,go,yaml" >}} {{% choosable language typescript %}} ```typescript const rpa = new aws.iam.RolePolicyAttachment("rpa", { policyArn: createPolicy.arn, role: role, }) ``` {{% /choosable %}} {{% choosable language csharp %}} ```csharp var rpa = new Aws.Iam.RolePolicyAttachment("rpa", new Aws.Iam.RolePolicyAttachmentArgs { PolicyArn = createPolicy.Arn, Role = role.Name, }); ``` {{% /choosable %}} {{% choosable language python %}} ```python rpa = aws.iam.RolePolicyAttachment("rpa", policy_arn=create_policy.arn, role=role.name) ``` {{% /choosable %}} {{% choosable language go %}} ```go rpa, err := iam.NewRolePolicyAttachment(ctx, "rpa", &iam.RolePolicyAttachmentArgs{ PolicyArn: createPolicy.Arn, Role: role.Name, }) if err != nil { return err } ``` {{% /choosable %}} {{% choosable language yaml %}} ```yaml rpa: type: aws:iam:RolePolicyAttachment properties: policyArn: ${createPolicy.arn} role: ${abacTest.name} ``` {{% /choosable %}} {{< /chooser >}} We can deploy these resources using the command `pulumi update`: ```bash 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](https://www.pulumi.com/docs/concepts/resources/providers/) that we can pass the IAM Role details to assume. {{< chooser language "typescript,csharp,python,go,yaml" >}} {{% choosable language typescript %}} ```typescript 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], } ); ``` {{% /choosable %}} {{% choosable language csharp %}} ```csharp 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, }, }); ``` {{% /choosable %}} {{% choosable language python %}} ```python 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])) ``` {{% /choosable %}} {{% choosable language go %}} ```go 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 } ``` {{% /choosable %}} {{% choosable language yaml %}} ```yaml abacUserProvider: type: pulumi:providers:aws properties: assumeRole: roleArn: ${role.arn} sessionName: "abacTesting" region: "us-east-1" options: dependsOn: - ${rpa} ``` {{% /choosable %}} {{< /chooser >}} Now we can create an AWS Lambda with Pulumi as follows: {{< chooser language "typescript,csharp,python,go,yaml" >}} {{% choosable language typescript %}} ```typescript 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 } ); ``` {{% /choosable %}} {{% choosable language csharp %}} ```csharp var lambdaRole = new Aws.Iam.Role("lambdaRole", new Aws.Iam.RoleArgs { AssumeRolePolicy = JsonSerializer.Serialize(new Dictionary { { "Version", "2012-10-17" }, { "Statement", new[] { new Dictionary { { "Action", "sts:AssumeRole" }, { "Effect", "Allow" }, { "Principal", new Dictionary { { "Service", "lambda.amazonaws.com" }, } }, }, } }, }), }); var deletePolicy = new Aws.Iam.Policy("deletePolicy", new Aws.Iam.PolicyArgs { PolicyDocument = JsonSerializer.Serialize(new Dictionary { { "Version", "2012-10-17" }, { "Statement", new[] { new Dictionary { { "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, }); ``` {{% /choosable %}} {{% choosable language python %}} ```python 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)) ``` {{% /choosable %}} {{% choosable language go %}} ```go 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 } ``` {{% /choosable %}} {{% choosable language yaml %}} ```yaml 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 ``` {{% /choosable %}} {{< /chooser >}} 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. ```bash 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: ```bash 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](https://slack.pulumi.com).