Secure AWS Lambda with IAM ABAC Policies
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.
To get started, let’s first install the Pulumi AWS 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.