'Access denied CloudFormation cross account s3 copy
I've looked through hours of documentation and don't feel like I'm missing anything, but I obviously am. I have S3 objects in source account I want to copy to an S3 bucket in dest account. Here's my setup (note I deliberately made permissions pretty loose initially just to get this to work. Planning on scoping this down later):
Role created by Cloudformation that the lambda assumes in source_account:
CopyRole:
Properties:
RoleName: 'CopyRole'
AssumeRolePolicyDocument:
Statement:
- Action: [ 'sts:AssumeRole' ]
Effect: Allow
Principal:
Service: [ lambda.amazonaws.com ]
Version: '2012-10-17'
Policies:
- PolicyDocument:
Statement:
- Action: [ 'logs:CreateLogStream', 'logs:PutLogEvents', 'logs:CreateLogGroup' ]
Effect: Allow
Resource: '*'
- Action: [ 's3:*', 'kms:*' ]
Effect: Allow
Resource: '*'
- Action: [ 'sts:AssumeRole' ]
Effect: Allow
Resource: '*'
PolicyName: CopyPolicy
Type: AWS::IAM::Role
Note that the above is given the ability to assume any other role.
IAM role in dest_account DestAccountCopyRole which should give read permissions from source bucket (other account) and write permissions to destination bucket (in same account)
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": [
"s3:GetLifecycleConfiguration",
"s3:GetBucketTagging",
"s3:GetInventoryConfiguration",
"s3:GetObjectVersionTagging",
"s3:ListBucketVersions",
"s3:GetBucketLogging",
"s3:ListBucket",
"s3:GetAccelerateConfiguration",
"s3:GetBucketPolicy",
"s3:GetObjectVersionTorrent",
"s3:GetObjectAcl",
"s3:GetEncryptionConfiguration",
"s3:GetBucketObjectLockConfiguration",
"s3:GetBucketRequestPayment",
"s3:GetObjectVersionAcl",
"s3:GetObjectTagging",
"s3:GetMetricsConfiguration",
"s3:GetBucketPublicAccessBlock",
"s3:GetBucketPolicyStatus",
"s3:ListBucketMultipartUploads",
"s3:GetObjectRetention",
"s3:GetBucketWebsite",
"s3:GetBucketVersioning",
"s3:GetBucketAcl",
"s3:GetObjectLegalHold",
"s3:GetBucketNotification",
"s3:GetReplicationConfiguration",
"s3:ListMultipartUploadParts",
"s3:GetObject",
"s3:GetObjectTorrent",
"s3:GetBucketCORS",
"s3:GetAnalyticsConfiguration",
"s3:GetObjectVersionForReplication",
"s3:GetBucketLocation",
"s3:GetObjectVersion"
],
"Resource": [
"arn:aws:s3:::source_bucket",
"arn:aws:s3:::source_bucket/*"
]
},
{
"Sid": "VisualEditor1",
"Effect": "Allow",
"Action": [
"s3:GetAccessPoint",
"s3:GetAccountPublicAccessBlock",
"s3:ListAllMyBuckets",
"s3:ListAccessPoints",
"s3:ListJobs"
],
"Resource": "*"
},
{
"Sid": "VisualEditor2",
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:PutObjectRetention",
"s3:PutObjectVersionAcl",
"s3:PutObjectVersionTagging",
"s3:PutObjectTagging",
"s3:PutObjectLegalHold",
"s3:PutObjectAcl"
],
"Resource": "arn:aws:s3:::dest_account_bucket/*"
}
]
}
... trust policy which allows the lambda role to assume this role
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::source_account:root"
},
"Action": "sts:AssumeRole"
}
]
}
source_account bucket policy which allows the IAM role in destination account to read from the source bucket
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DelegateS3Access",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::dest_account:role/DestAccountCopyRole"
},
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::source_account_bucket",
"arn:aws:s3:::source_account_bucket/*"
]
}
]
}
dest_account bucket policy
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::dest_account:root"
},
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::dest_account_bucket",
"arn:aws:s3:::dest_account_bucket/*"
]
}
]
}
My lambda run on javascript is able to get past the part of assuming the cross account role, but fails with Access Denied when attempting to copy the data over
const creds = await getCredentials(destination_bucket)
.catch((err) => {
console.log(err);
throw err;
});
s3Dest = new aws.S3(creds);
return s3.listObjects(sourcePath).promise()
.then(async (data) => {
return Promise.all(data.Contents.map((item) => {
console.log(`Copying data over`);
s3Dest.copyObject({
Bucket: destination_bucket,
CopySource: copy_source,
Key: key
}).promise();
}));
}).catch((err) => {
console.log(`Error: ${err}`);
throw err;
});
const getCredentials = async (bucket) => {
return new Promise((resolve, reject) => {
const params = {
RoleArn: "arn:aws:iam::dest_account:role/DestAccountCopyRole",
RoleSessionName: 'copy-test'
};
sts.assumeRole(params, (err, data) => {
if (err) reject(err);
else {
console.log(data);
resolve({
accessKeyId: data.Credentials.AccessKeyId,
secretAccessKey: data.Credentials.SecretAccessKey,
sessionToken: data.Credentials.SessionToken,
});
}
});
});
};
I don't think I know enough about Javascript either because the cloudwatch logs are printing both Copying data over and Error: Access Denied then failing (I figured the Error: Access Denied would only show up if the listObjects call failed, but it looks like it's able the items and just fail at the copying portion)
Solution 1:[1]
When copying objects between buckets that belong to different AWS Accounts ('cross-account copy'), the one set of credentials requires permission to both read from the source location and write to the destination location.
Since your code is assuming an IAM Role from the destination account, you will also need to create a Bucket Policy on the source bucket that grants permission for that IAM Role to read from the source bucket.
It does not matter than the IAM Role used by the Lambda function has permission to access the source bucket because the copy() operation will be performed by the IAM Role assumed from the destination account, so it will not use any permissions from the IAM Role assigned to the Lambda function.
Also, when granting permission for the IAM Role to write to the destination bucket, it would actually be better-practice to grant this permission via a policy on the IAM Role itself, rather than as a Bucket Policy. In general, a Bucket Policy should only be used when granting public access or cross-account access, rather than granting permission to simply one user.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | John Rotenstein |
