title: How to use AWS Lambda to backup an S3 object to a bucket on another account?
body: <p>I'm attempting to use AWS Lambda to backup objects from an <code>assets.myapp.com</code> S3 bucket under <code>Account-A</code> to a <code>backup-assets.myapp.com</code> S3 bucket under <code>Account-B</code>, but I'm getting <code>Access Denied</code> no matter the <code>IAM</code> or <code>Bucket Policy</code> configuration I use.</p>
  
  <p>I've got a Lambda function called <code>Backup-S3-Object</code> and it's fired by an <code>s3:PutObject</code> event trigger on the <code>assets.myapp.com</code> bucket.</p>
  
  <p>The desired result is that the <code>Backup-S3-Object</code> function will the perform an <code>s3.copyObject</code> to <code>backup-assets.myapp.com</code> using the AWS-SDK for Javascript.</p>
  
  <p><strong>The Lambda Code</strong></p>
  
  <pre><code>var aws = require('aws-sdk');
  var s3 = new aws.S3({ apiVersion: '2006-03-01' });
  
  exports.backupObject = function(event, context, callback) {
  
      var data = event.Records[0];
      var sourceBucket = data.s3.bucket.name;
      var targetBucket = 'backup-' + data.s3.bucket.name;
      var key = data.s3.object.key;
  
      console.log('BACKUP: ' + sourceBucket + '/' + key + ' to ' + targetBucket);
  
      s3.copyObject({
          Bucket : targetBucket,
          CopySource : sourceBucket + '/' + key,
          Key : key,
          ACL : 'private',
          ServerSideEncryption : 'AES256'
      }, function(error, data) {
  
          if (error) return context.done(error);
          return context.done(null, 'Successful backup of ' + sourceBucket + '/' + key);
  
      });
  
  };
  </code></pre>
  
  <p><strong>The Lambda Role S3 Policy</strong></p>
  
  <p>I've got the following policy for the <code>Backup-S3-Lambda-Role</code> which is assigned to my Lambda function. This allows the Lambda function access to <code>List</code> or <code>Get</code> any object, from any bucket, under <code>Account-A</code>, which is the source account. So this policy will allow the Lambda function to get the object from <code>assets.myapp.com</code>, which is the source bucket.</p>
  
  <pre><code>{
      "Version": "2012-10-17",
      "Statement": [
          {
              "Effect": "Allow",
              "Action": [
                  "s3:ListBucket",
                  "s3:ListBucketVersions",
                  "s3:GetObject",
                  "s3:GetObjectVersion"
              ],
              "Resource": [
                  "arn:aws:s3:::*"
              ]
          }
      ]
  }
  </code></pre>
  
  <p><strong>The <code>backup-assets.myapp.com</code> Bucket Policy</strong></p>
  
  <p>And then, under <code>Account-B</code>, I've got the following bucket policy attached to the <code>backup-assets.myapp.com</code>, which is intended to allow the Lambda function, running under <code>Account-A</code>, to write to the <code>backup-assets.myapp.com</code> bucket under <code>Account-B</code></p>
  
  <pre><code>{
      "Version": "2008-10-17",
      "Statement": [
          {
              "Sid": "Allow PUT from Account-A Lambda function",
              "Effect": "Allow",
              "Principal": {
                  "AWS": "arn:aws:iam::ACCOUNT-A-NUMBER-HERE:role/Backup-S3-Lambda-Role"
              },
              "Action": "s3:PutObject",
              "Resource": [
                  "arn:aws:s3:::backup-assets.myapp.com",
                  "arn:aws:s3:::backup-assets.myapp.com/*"
              ]
          }
      ]
  }
  </code></pre>
  
  <p><strong>What's Happening?</strong></p>
  
  <p>The trigger on the <code>assets.myapp.com</code> bucket fires just fine. It triggers the <code>Backup-S3-Object</code> Lambda function, and the function logic is able to determine the correct bucket names and object key to be copied, but it always fails with <code>Access Denied</code>.</p>
  
  <p>I've done a test using the same setup and transferring to another bucket that's also under <code>Account-A</code> and it works just fine. So the problem is obviously with the cross-account permissions, but nothing I've tried works.</p>
  
  <p>I've also attempted using the <code>CanonicalID</code> of <code>Account-A</code> as the <code>Principal</code> in the <code>backup-assets.myapp.com</code> bucket policy, but still get <code>Access Denied</code> as the resulting error.</p>
  
  <p>And I've attempted using <code>"AWS": "arn:aws:iam::ACCOUNT-A-NUMBER-HERE:root"</code> as the principal, same error.</p>
  
  <p>Any help?</p>
  
  <blockquote>
    <p><strong>A Solution:</strong> As outlined by Matt below, the solution is that you need to include <code>s3:PutObject</code> permission <em>in the Lambda role policy</em>.
    This is a little weird to me, but the general idea is that any
    permissions you grant the Lambda role in the Bucket Policy of your
    target bucket must also be replicated in the Lambda policy under your
    source account. It seems that the Lambda role is <em>adopted</em> entirely
    when you reference it in the Bucket Policy. To achieve this I've added
    a second inline policy to my Lambda role that allows <code>s3:PutObject</code> to
    the <code>backup-assets.myapp.com</code> bucket under <code>Account-B</code>. This disallows
    the Lambda function to write to any of the source buckets, but does
    allow it to write to the backup bucket.</p>
  </blockquote>