Cross-Org Lambda Transfer
cloudwatch on source organization is working – lambda is failing in the s3_assumed.copy_object
command. The assumed role has permissions in the destination organization as can be verified in the test lambda provided at the end of this question.
As far as I can tell, the issue is with the assumed identity accessing the source files for the copy procedure. I have tried to solve this with the bucket policy on the source bucket (shown below) – but I think this is not sufficient. do we need to add a trust policy for the destination account on the source account so the assumed role can execute the copy command? I have tried this also but still not having luck.
Source Account Config
S3 bucket
Bucket ARN: arn:aws:s3:::test-lambda-transfer
Configure event notification for put and post for lambda function defined below
Policy: some of this might not be becessary – but I am trying ot sort out where the permission issue is.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowLambdaGetObject",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::<source-account-id>:role/s3-cross-org-transfer-role"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::test-lambda-transfer/*"
},
{
"Sid": "AllowLambdaListBucket",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::<source-account-id>:role/s3-cross-org-transfer-role"
},
"Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::test-lambda-transfer"
},
{
"Sid": "AllowAssumedRoleGetObject",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::<destination-account-id>:role/test-cross-org-transfer-role"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::test-lambda-transfer/*"
}
]
}
Lambda Function
Lambda ARN: arn:aws:lambda:us-east-1:<source-account-id>:function:cross-account-s3-transfer
Permission Role: arn:aws:iam::<source-account-id>:role/s3-cross-org-transfer-role
Role Policy: arn:aws:iam::<source-account-id>:policy/s3-transfer-execution-policy
import boto3
import urllib.parse
import logging
# Initialize logging
logger = logging.getLogger()
logger.setLevel(logging.INFO)
def lambda_handler(event, context):
sts_client = boto3.client('sts')
destination_bucket = 's3-transfer-test-receiver-bucket'
role_to_assume_arn = 'arn:aws:iam::<destination-account-id>:role/test-cross-org-transfer-role'
try:
for record in event['Records']:
source_bucket = record['s3']['bucket']['name']
key = urllib.parse.unquote_plus(record['s3']['object']['key'])
logger.info(f"Detected new object: {key} in bucket {source_bucket}")
# Try to get the object metadata from the source bucket
s3_source = boto3.client('s3')
try:
response = s3_source.head_object(Bucket=source_bucket, Key=key)
logger.info(f"Successfully retrieved metadata for {key} from {source_bucket}")
except Exception as e:
logger.error(f"Error retrieving metadata for {key} from {source_bucket}: {str(e)}")
raise e
logger.info("Assuming destination role...")
assumed_role = sts_client.assume_role(RoleArn=role_to_assume_arn, RoleSessionName="S3CopySession")
logger.info(f"Assumed role ARN: {assumed_role['AssumedRoleUser']['Arn']}")
credentials = assumed_role['Credentials']
s3_assumed = boto3.client('s3',
aws_access_key_id=credentials['AccessKeyId'],
aws_secret_access_key=credentials['SecretAccessKey'],
aws_session_token=credentials['SessionToken'])
copy_source = {'Bucket': source_bucket, 'Key': key}
logger.info(f"Copying {key} from {source_bucket} to {destination_bucket}")
s3_assumed.copy_object(Bucket=destination_bucket, Key=key, CopySource=copy_source)
logger.info(f"Successfully copied {key} to {destination_bucket}")
except Exception as e:
logger.error(f"Error during copy operation: {str(e)}")
raise e
Policy for Lambda Function
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::test-lambda-transfer",
"arn:aws:s3:::test-lambda-transfer/*"
]
},
{
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:*:*:*"
},
{
"Effect": "Allow",
"Action": "sts:AssumeRole",
"Resource": "arn:aws:iam::<destination-account-id>:role/test-cross-org-transfer-role"
}
]
}
Destination Account
S3 Bucket
Bucket ARN: arn:aws:s3:::s3-transfer-test-receiver-bucket
IAM
Role: arn:aws:iam::<destination-account-id>:role/test-cross-org-transfer-role
Policy: arn:aws:iam::<destination-account-id>:policy/put-object-s3-cross-account-test-policy
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::s3-transfer-test-receiver-bucket/*"
}
]
}
Trust Relationships – Trusted Entities
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Action": "sts:AssumeRole"
},
{
"Effect": "Allow",
"Principal": {
"AWS": [
"arn:aws:iam::<source-account-id>:role/s3-cross-org-transfer-role",
"arn:aws:iam::<destination-account-id>:role/test-cross-org-transfer-role"
]
},
"Action": "sts:AssumeRole"
}
]
}
Tests
Python Lambda that works
import boto3
import logging
# Set up logging
logger = logging.getLogger()
logger.setLevel(logging.INFO)
def lambda_handler(event, context):
try:
sts_client = boto3.client('sts')
logger.info("Assuming destination role...")
assumed_role = sts_client.assume_role(
RoleArn="arn:aws:iam::<destination-account-id>:role/test-cross-org-transfer-role",
RoleSessionName="CrossAccountSession"
)
logger.info(f"Assumed Role ARN: {assumed_role['AssumedRoleUser']['Arn']}")
logger.info("Role assumed successfully.")
credentials = assumed_role['Credentials']
s3 = boto3.client(
's3',
aws_access_key_id=credentials['AccessKeyId'],
aws_secret_access_key=credentials['SecretAccessKey'],
aws_session_token=credentials['SessionToken'],
)
destination_bucket = 's3-transfer-test-receiver-bucket'
# Attempt to directly upload a simple object
test_key = 'test-object.txt'
test_body = 'This is a test object.'
logger.info(f"Directly uploading test object to {destination_bucket}...")
s3.put_object(Bucket=destination_bucket, Key=test_key, Body=test_body)
logger.info("Direct upload successful.")
except Exception as e:
logger.error(f"Error during direct upload operation: {e}")
raise e
2
Answers
Lambda S3 Sync Utility
Setup a utility to copy any uploaded files from a source bucket in one organization to a destination bucket in another organization
Resources
Source S3 Bucket:
arn:aws:s3:::s3-transfer-test-source-bucket
Source IAM role for lambda:
arn:aws:iam::<source-acct-id>:role/s3-transfer-lambda-role
Source lambda Function:
arn:aws:lambda:us-east-1:<source-acct-id>:function:s3-cross-account-sync
Destination S3 Bucket:
arn:aws:s3:::s3-transfer-test-destination-bucket
Destination IAM role for source Lambda (assumable):
arn:aws:iam::<source-acct-id>:role/s3-receiver-test-lambda-role
Destination IAM Policy:
arn:aws:iam::<dest-acct-id>:policy/put-object-s3-cross-account-test-policy
Step 1: Create S3 Buckets
arn:aws:s3:::s3-transfer-test-source-bucket
arn:aws:s3:::s3-transfer-test-destination-bucket
Step 3: Create IAM Role for Lambda (Source Account)
arn:aws:iam::<source-acct-id>:role/s3-transfer-lambda-role
Ensure the IAM role has the necessary permissions to read objects from the source S3 bucket and to log to CloudWatch Logs. The role should include two policies:
Step 4: Create Lambda Function
arn:aws:lambda:us-east-1:<source-acct-id>:function:s3-cross-account-sync
Use the default code for now
Step 5: Configure Event Notifications on S3 Bucket (Source)
Name: S3TestPutObjectNotification
Event Types:
s3:ObjectCreated:Put
ands3:ObjectCreated:Post
Lambda Function:
s3-cross-account-sync
Step 6: Test Current Config
Open CloudWatch
Select Live Tail
Filter by lambda function
Open S3 Bucket in a new tab
Upload file to S3
Watch CloudWatch for logs
If you see them, move on to next steps.
Step 7: Configure IAM Policy on Destination Account
Create Policy:
arn:aws:iam::<dest-acct-id>:policy/put-object-s3-cross-account-test-policy
Step 8: Configure IAM Role with Trusted Relationship on Destination Account
In IAM create a new role, and select "AWS Account as the trusted entity type. Select "Another AWS Account" then put the source organization ID into the box. Leave the two check boxes un-marked. Click Next. Click Next again. Enter a name and then click Create Role.
Role:
arn:aws:iam::<dest-acct-id>:role/test-cross-org-transfer-role
Open the role, navigate to the Trust relationships, and edit the trusted entities with the correct Principle. (Conform to least-privilege security practices)
Step 9: Configure IAM Policy on Destination Account
Add inline policy to
test-cross-org-transfer-role
that allows the role to put objects into destination bucket, and get objects from source bucket.name:
test-cross-org-transfer-policy
Step 10: Update the IAM policy in the Source Account
arn:aws:iam::<source-acct-id>:role/s3-transfer-lambda-role
Step 11: Test Lambda with Assumed Role
Update the lambda code to the following
Test this Lambda function. It should output something like this...
You can verify by looking for a new
.txt
file in the destination bucket as well.Step 12: Update Source Bucket Policy
We need to allow the assumed role to get objects from the source bucket.
Step 12: Update Lambda
This version performs the desired steps but still includes the logging for testing and debugging.
Step 13: Test S3 Upload
Drop a file in S3 source bucket, watch CloudWatch live tail - it should succeed. Check destination bucket for file to be sure!
Python Cleanup
For production, you'll want to streamline your code by removing excessive logging and checks that were primarily for debugging. Here's a cleaner version of the Lambda function that focuses on the essential operations, with minimal logging:
Additional Recommendations for Production
Error Handling: While the streamlined code removes detailed logging and checks, ensure you have appropriate error handling in place. Consider using AWS Lambda Dead Letter Queues (DLQs) or SNS topics to capture and alert on failures.
Monitoring and Logging: Implement CloudWatch monitoring for the Lambda function to track invocations, errors, and performance metrics. Use CloudWatch Alarms to get notified of any operational issues.
Security: Regularly review the IAM roles and policies to ensure they follow the principle of least privilege. Rotate credentials if necessary and audit access regularly.
Performance Tuning: Based on the size and number of objects being transferred, you may need to adjust the Lambda function's memory and timeout settings for optimal performance.
Cost Management: Monitor the AWS bill for S3 and Lambda usage, especially if you're transferring large volumes of data. Consider implementing cost controls and alerts.
Testing: Before going live, thoroughly test the function in a staging environment to ensure it performs as expected under various conditions.
By following these guidelines and using the streamlined Lambda function, you should have a robust solution for copying objects between S3 buckets across accounts.
The fact that you successfully assumed the
test-cross-org-transfer-role
means all your code up to that point is working fine.The issue is with the permissions on that role.
First, note the code that was working in your "Python Lambda that works":
This is successfully using
PutObject
to create an object in thetest-lambda-transfer
bucket.However, the main code is not working:
This code is performing a
copy_object()
operation, which requires:However, the IAM Role only has
PutObject
permissions on the destination:It does not have permission to read from the source bucket, or on the source object.
However, you clearly have added this permission in the Bucket Policy:
This should be sufficient for
GetObject
access, but the truth is that it isn’t!The reason is that the IAM Role also needs permission to read from the source bucket. Merely giving access via the Bucket Policy is not sufficient because the IAM Role has not been granted permission to read from S3. Therefore, add another permission to the IAM Role, like this:
Bottom line: The assumed IAM Role needs permission within its own Account to use S3, and it also needs permission within the Bucket Policy on the bucket in the other account.