This is the second part of my journey for a working auto-remediation method for public S3 buckets. If you haven’t already seen it, the first part can be found here.
This second part will address the deployment of the previously explained solution using a method for deploying across all our accounts leveraging CloudFormation StackSets. It’s assumed the reader is familiar with the use of StackSets, but those with a loose understand should be able to follow along.
In the example below, the S3 remediation solution is deployed to us-east-1 and us-west-2, but additional resources (Lambda function, Lambda permissions, and CloudWatch rules) and conditions (region check) will need to be created for each desired region. This is due to what I consider an annoying, yet understandable limitation: When searching for the S3 bucket containing the code, lambda looks for the bucket as if it were in the same region as the lambda (see location subresource here).
For this reason, the creation of a bucket located in each region is required. For clarity’s sake, I chose a single bucket name and added the region as a suffix in the template below. To simplify the management of our burgeoning security operations scripts, cross-region replication was enabled between the buckets to ensure an update in one was reflected across all regions. Additionally, the bucket policy will need to be updated to allow access by all accounts to allow CloudFormation access from the accounts local stack. The python code was then saved and then added to a zip file named RemediatePublicS3.zip to be added to the replicated buckets.
The creation of the buckets could also be added to this solution but is not included due to the added complexity at this time.
Without any more explanation, here’s the CloudFormation template:
{
"AWSTemplateFormatVersion": "2010-09-09",
"Description": "Creates CW rule and Lambda function for Public S3 remediation. Dependent on python script located in S3",
"Resources": {
"ExecutionRole": {
"Type": "AWS::IAM::Role",
"Condition" : "USEast1Check",
"Properties": {
"RoleName": "S3RemediationRole",
"AssumeRolePolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Action": [
"sts:AssumeRole"
]
}
]
},
"Path": "/",
"ManagedPolicyArns": [
"arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
],
"Policies": [
{
"PolicyName": "S3Access",
"PolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Sid": "S3a",
"Effect": "Allow",
"Action": [
"s3:PutBucketPolicy",
"s3:DeleteBucketPolicy"
],
"Resource": "arn:aws:s3:::*"
},
{
"Sid": "S3b",
"Effect": "Allow",
"Action": "s3:ObjectOwnerOverrideToBucketOwner",
"Resource": "arn:aws:s3:::*/*"
}
]
}
}
]
}
},
"S3LambdaRemediationUSEast1": {
"Type" : "AWS::Lambda::Function",
"Condition" : "USEast1Check",
"DependsOn" : "ExecutionRole",
"Properties" : {
"Code" : {
"S3Bucket": "<bucketname>-useast1",
"S3Key": "RemediatePublicS3.zip"
},
"Description" : "Works with CW rule to remediate publicly available s3 buckets",
"FunctionName" : "PublicS3Remediation",
"Handler" : "RemediatePublicS3.policy_check_JSON",
"MemorySize" : 128,
"Role" : {
"Fn::Join": [
"",
[
"arn:aws:iam::",
{
"Ref": "AWS::AccountId"
},
":role/S3RemediationRole"
]
]
},
"Runtime" : "python3.6",
"Timeout" : 3
}
},
"LambdaPermissionUSEast1": {
"Type": "AWS::Lambda::Permission",
"Condition" : "USEast1Check",
"DependsOn": [
"S3LambdaRemediationUSEast1",
"S3RemediationRuleUSEast1"
],
"Properties": {
"FunctionName": {
"Fn::GetAtt": [
"S3LambdaRemediationUSEast1",
"Arn"
]
},
"Action": "lambda:InvokeFunction",
"Principal": "events.amazonaws.com",
"SourceArn": {
"Fn::GetAtt": [
"S3RemediationRuleUSEast1",
"Arn"
]
}
}
},
"S3LambdaRemediationUSWest2": {
"Type" : "AWS::Lambda::Function",
"Condition" : "USWest2Check",
"Properties" : {
"Code" : {
"S3Bucket": "<bucketname>-uswest2",
"S3Key": "RemediatePublicS3.zip"
},
"Description" : "Works with CW rule to remediate publicly available s3 buckets",
"FunctionName" : "PublicS3Remediation",
"Handler" : "RemediatePublicS3.policy_check_JSON",
"MemorySize" : 128,
"Role" : {
"Fn::Join": [
"",
[
"arn:aws:iam::",
{
"Ref": "AWS::AccountId"
},
":role/S3RemediationRole"
]
]
},
"Runtime" : "python3.6",
"Timeout" : 3
}
},
"LambdaPermissionUSWest2": {
"Type": "AWS::Lambda::Permission",
"Condition" : "USWest2Check",
"DependsOn": [
"S3LambdaRemediationUSWest2",
"S3RemediationRuleUSWest2"
],
"Properties": {
"FunctionName": {
"Fn::GetAtt": [
"S3LambdaRemediationUSWest2",
"Arn"
]
},
"Action": "lambda:InvokeFunction",
"Principal": "events.amazonaws.com",
"SourceArn": {
"Fn::GetAtt": [
"S3RemediationRuleUSWest2",
"Arn"
]
}
}
},
"S3RemediationRuleUSWest2": {
"Type" : "AWS::Events::Rule",
"Condition" : "USWest2Check",
"Properties" : {
"Name" : "PublicS3Remediation",
"Description" : "Monitors for changes in S3 buckets.",
"EventPattern" : {
"source": [
"aws.config"
],
"detail-type": [
"Config Rules Compliance Change"
],
"detail": {
"messageType": [
"ComplianceChangeNotification"
],
"configRuleName": [
"s3-bucket-public-read-prohibited",
"s3-bucket-public-write-prohibited"
]
}
},
"State" : "ENABLED",
"Targets" : [
{
"Arn": {
"Fn::GetAtt":[
"S3LambdaRemediationUSWest2",
"Arn"
]
},
"Id": "S3RemediationUSWest2"
}
]
}
},
"S3RemediationRuleUSEast1": {
"Type" : "AWS::Events::Rule",
"Condition" : "USEast1Check",
"Properties" : {
"Name" : "PublicS3Remediation",
"Description" : "Monitors for changes in S3 buckets.",
"EventPattern" : {
"source": [
"aws.config"
],
"detail-type": [
"Config Rules Compliance Change"
],
"detail": {
"messageType": [
"ComplianceChangeNotification"
],
"configRuleName": [
"s3-bucket-public-read-prohibited",
"s3-bucket-public-write-prohibited"
]
}
},
"State" : "ENABLED",
"Targets" : [
{
"Arn": {
"Fn::GetAtt":[
"S3LambdaRemediationUSEast1",
"Arn"
]
},
"Id": "S3RemediationUSEast1"
}
]
}
}
},
"Conditions": {
"USEast1Check": {
"Fn::Equals": [
{
"Ref": "AWS::Region"
},
"us-east-1"
]
},
"USWest2Check": {
"Fn::Equals": [
{
"Ref": "AWS::Region"
},
"us-west-2"
]
}
}
}
Once this was ready for deployment, we rolled this out to our accounts in all allowed regions and added the s3:PutObjectAcl and s3:PutBucketAcl in a deny statement to our guardrails policy. I’ll be adding another post soon explaining this guardrails policy or, hopefully, expanding on my boundary policy efforts.