Rob Cannon
Rob Cannon

Reputation: 392

Adding S3 Bucket Policy for any Cloudfront Origin Access Idenity

Using multiple CloudFronts with one bucket doesn't work very well because the only way I can give access in CloudFormation is to add a BucketPolicy to the template. With multiple apps/templates this will always overwrite the entire bucket policy to add permission for the S3 Origin Access Identity.

    S3OriginIdentity:
        Type: 'AWS::CloudFront::CloudFrontOriginAccessIdentity'
        Properties:
          CloudFrontOriginAccessIdentityConfig:
            Comment: S3 Origin Identity
    S3OriginIdentityS3ReadPolicy:
      Type: "AWS::S3::BucketPolicy"
      Properties:
        Bucket: my-bucket
        PolicyDocument:
          Version: '2012-10-17'
          Statement:
          - Sid: my-cloudfront-read-access
            Action:
              - s3:GetObject
            Effect: Allow
            Principal:
              CanonicalUser:
                Fn::GetAtt: S3OriginIdentity.S3CanonicalUserId
            Resource: arn:aws:s3:::my-bucket/path/*

To solve this issue it occurred to me that I could use a condition in the bucket policy to give read access to any Cloudfront identity. From looking at the documentation it appears that aws:SourceArn or aws:PrincipalArn should do the trick. Here is an example policy I tried.

{
    "Version": "2012-10-17",
    "Statement": [{
        "Sid": 1,
        "Action": [
            "s3:GetObject"
        ],
        "Effect": "Allow",
        "Resource": "arn:aws:s3:::my-bucket/*",
        "Condition": {
            "ArnLike": {
                "aws:SourceArn": "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity*"
            }
        },
        "Principal": "*"
    }]
}

When I used aws:PrincipalArn, the console wouldn't even let me save it (a generic Access Denied), but I tried it with the aws:SourceArn version above, and it did not give access. What am I doing wrong? Or is there a better approach?

Upvotes: 1

Views: 3063

Answers (3)

GoForth
GoForth

Reputation: 656

Here is a CloudFormation template for the Bucket Policy and other salient parts regarding the CloudFront Origin Access Identity:

  AppDistBucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref AppDistBucket
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              AWS: !Sub arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity ${AppOriginAccessIdentity}
            Action:
              - s3:GetObject
            Resource: !Sub 'arn:aws:s3:::${AppDistBucket}/*'

  AppOriginAccessIdentity:
    Type: AWS::CloudFront::CloudFrontOriginAccessIdentity
    Properties:
      CloudFrontOriginAccessIdentityConfig:
        Comment: !Sub 'CloudFront OAI for ${EnvironmentName}-${DistIdentifier}'

  AppDistribution:
    Type: AWS::CloudFront::Distribution
    Properties:
      DistributionConfig:
        # ...
        Origins:
          - DomainName: !Sub ${AppDistBucket}.s3.amazonaws.com
            Id: !Sub ${EnvironmentName}-${DistIdentifier}
            S3OriginConfig:
            OriginAccessIdentity: !Sub 'origin-access-identity/cloudfront/${AppOriginAccessIdentity}'
        # ...

Also, keep in mind that patience is key. Once properly configured, your distribution may show up with "Access Denied" errors for up to a few hours.

Upvotes: 2

TedOC
TedOC

Reputation: 180

I know this is an old question but I have run into a similar issue.

I might be able to shed some light at least on the AccessDenied error when using aws:PrincipalArn. In my case it was because my S3 bucket had block public policy enabled and that aws:PrincipalArn condition isn't enough to qualify as a non-public principal. The required conditions for that can be found here https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-control-block-public-access.html#access-control-block-public-access-policy-status.

What I wasn't able to get around yet was that in Terraform there appears to be no way to query for a previously created OAI. So if I create the OAI, S3 bucket and policy in one module I can't get that OAI for the CloudFront distributions later on.

Upvotes: 0

Michael - sqlbot
Michael - sqlbot

Reputation: 178966

aws:SourceArn is listed as "is available for only some services" and refers not to a principal but to resource that is the source of an action, like an SNS topic in an SQS queue policy or an S3 bucket in a Macie policy.

In that light, I don't think it would be applicable, here, and s3:GetObject doesn't mention it as a possibility -- this action only appears to support a small number of non-global condition keys, all of which are specific to S3: s3:ExistingObjectTag/<key>, s3:authtype, s3:signatureage, s3:signatureversion, s3:x-amz-content-sha256.

aws:PrincipalArn is a global condition key, so it looks like it might have worked, but I'd suggest that you're fortunate that it did not. If it had, then you would have exposed your bucket to all Origin Access Identities for all CloudFront distributions in all AWS accounts. The "access denied" error is unexpected, but might stem from the fact that you lack a permission on arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity*, which makes sense, since it's not part of in your account -- OAIs are allocated to your account by CloudFront but they aren't, strictly speaking, part of your AWS account -- they're an external entity.¹ In light of this, it seems unlikely that there's a way to express "all of my OAIs" in a policy statement.

I would suggest that the most straightfoward approach to having a single bucket accessible to multiple CloudFront distributions would be to create a single OAI and configure your multiple CloudFront distributions all to use it. Individual OAIs and individual bucket policy grants are not only unwieldy, they also won't scale -- bucket policies are limited to ~20KB in size.

But if that's the direction you want to go, it appears that a Lambda-Backed Custom Resource could be used. The idea here would be to create a Lambda function that will edit the bucket policy and append the new OAI to the Principal array containing the already-added OAIs. (Principal in the bucket policy can be either a scalar or an array). The danger of this is that it's a delicate operation -- there's no concurrency guarding of a bucket policy, so if two of these stacks try to modify the policy at effectively the same time (get, edit, put) then one of them will see its updates ignored because they're be overwritten by the other. (Concurrent operations that ultimately in this order -- Read #1, Modify #1, Read #2, Modify #2, Write #1, Write #2 -- result in Write #1's change being lost, because Write #2 stored data that was based on Read #2, which was identical to Read #1.)


¹Justification for this assertion: an OAI can't access objects in a bucket that are created by a different AWS account with write access to your bucket, even if x-amz-acl: bucket-owner-full-control is applied to the object: "If another AWS account uploads files to your bucket, that account is the owner of those files. Bucket policies only apply to files that the bucket owner owns. This means that if another account uploads files to your bucket, the bucket policy that you created for your OAI will not be evaluated for those files." Exactly the same thing is true if your bucket policy explicitly grants read-access to an IAM user in a different account -- the bucket policy can't delegate read permissions to a user outside the same account when the object was also created by a user from outside the account. Users inside your account can access such objects if the bucket policy allows them to.

Upvotes: 3

Related Questions