Create S3 Bucket and S3 Bucket Policy - Unable to ref bucket name


#1

In the resources section, I’m creating an S3 bucket and I’m also creating the S3 bucket policy. In the bucket policy I need to specify the name of the created bucket. In straight CloudFormation this would be easy as I just do !Sub, paste the policy doc and use ${<my bucket resource logical id}.

In serverless I’m unable to do that as serverless uses ${} which is fine. However, I need to use the mappings section instead in Fn::Sub which all the docs call for using: MyBucket: !Ref <MyBucketLogicalResource>. However serverless doesn’t support the shorthand since it gets converted to JSON under the hood.

At this point I’m not sure how to substitute the bucket name that’s getting created by CFN into this bucket policy, as I do not want to hard code a bucket name into this resource which would cause collisions.

CloudTrailBucket:
  Type: AWS::S3::Bucket
CloudTrailBucketPolicy:
  Type: "AWS::S3::BucketPolicy"
  Properties: 
    Bucket: 
      Ref: CloudTrailBucket
    PolicyDocument: 
      Fn::Sub: 
        - |
          {
            "Version": "2012-10-17",
            "Statement": [
                {
                    "Sid": "AWSCloudTrailAclCheck20150319",
                    "Effect": "Allow",
                    "Principal": {
                        "Service": "cloudtrail.amazonaws.com"
                    },
                    "Action": "s3:GetBucketAcl",
                    "Resource": "arn:aws:s3:::${CloudTrailBucket}"
                },
                {
                    "Sid": "AWSCloudTrailWrite20150319",
                    "Effect": "Allow",
                    "Principal": {
                        "Service": "cloudtrail.amazonaws.com"
                    },
                    "Action": "s3:PutObject",
                    "Resource": "arn:aws:s3:::${CloudTrailBucket}/AWSLogs/*",
                    "Condition": {
                        "StringEquals": {
                            "s3:x-amz-acl": "bucket-owner-full-control"
                        }
                    }
                }
            ]
          }
        - { CloudTrailBucket: 
              Ref: CloudTrailBucket }

Since I can’t use the shorthand syntax, the other option is to use the traditional Ref: which it doesn’t like here either.

I found this post from last year regarding this issue, but I’m not sure how he figured this out: Setting up S3 bucket policy


#2

Instead of !Sub, try using Fn::GetAtt. Here’s an working example from one of my stacks. (bucket names changes to protect the innocent):

    MyS3Bucket:
      Type: AWS::S3::Bucket
      Properties:
        AccessControl: Private
        BucketName: ${self:custom.stage}-my.domain.com
        CorsConfiguration:
          CorsRules:
            - AllowedHeaders: ['*']
              AllowedMethods: ['PUT', 'POST']
              AllowedOrigins: ['*']
              MaxAge: '3000'
    MyS3BucketPolicy:
      Type: AWS::S3::BucketPolicy
      DependsOn: MyS3Bucket
      Properties:
        Bucket: 
          Ref: MyS3Bucket
        PolicyDocument:
          Version: '2012-10-17'
          Statement:
          # Development Only - Comment out before dpeloying to Production
          - Sid: DEVELOPMENT ONLY - Allow get requests originating from localhost.
            Effect: Allow
            Principal: '*'
            Action:
              - s3:GetObject
            Resource: 
              Fn::Join: ['', [Fn::GetAtt: [ MyS3Bucket, Arn ], '/Contracts/*'] ]
            Condition:
              StringLike:
                aws:Referer:
                  - 'http://localhost:*/*'
                  - 'https://localhost:*/*'
          # Development Only - End
          - Sid: 
              Fn::Join: ['', ['Allow get requests originating from ', Ref: MyAppS3Bucket] ]
            Effect: Allow
            Principal: '*'
            Action:
              - s3:GetObject
            Resource: 
              Fn::Join: ['', [Fn::GetAtt: [ MyS3Bucket, Arn ], '/Contracts/*'] ]
            Condition:
              StringLike:
                aws:Referer:
                  - Fn::Join: ['', ['http://', Ref: MyAppS3Bucket, '/*'] ]
                  - Fn::Join: ['', ['https://', Ref: MyAppS3Bucket, '/*'] ]

The policy includes 2 statements. 1 for development allowing Public Get from localhost of objects in the MyS3Bucket/Contracts/ folder.
The other(for production) allows Public Get of Objects in the same folder, but only if the Referrer page is MyAppS3Bucket.

Hope this is helpful.


#3

Thanks for this. Unfortunately couldn’t get the Fn::Join’s to work. Even copying yours it says it isn’t valid YAML. But ended up building the ARN manually:

CloudTrailBucket:
  Type: AWS::S3::Bucket
CloudTrailBucketPolicy:
  Type: "AWS::S3::BucketPolicy"
  Properties: 
    Bucket: 
      Ref: CloudTrailBucket
    PolicyDocument: 
      Version: '2012-10-17'
      Statement:
      - Sid: AWSCloudTrailAclCheck20150319
        Effect: Allow
        Principal:
          Service: cloudtrail.amazonaws.com
        Action:
          - s3:GetBucketAcl
        Resource: 
          Fn::Join: ['', ['arn:aws:s3:::', Ref: CloudTrailBucket] ]
      - Sid: AWSCloudTrailWrite20150319
        Effect: Allow
        Principal:
          Service: cloudtrail.amazonaws.com
        Action:
          - s3:PutObject
        Resource: 
          Fn::Join: ['', ['arn:aws:s3:::', Ref: CloudTrailBucket, '/AWSLogs/*'] ]
        Condition:
          StringEquals:
            s3:x-amz-acl: "bucket-owner-full-control"

#4

@iscofield building the ARN manually does not seem right. Instead, try reformatting the YAML to something like:

Resource: 
  Fn::Join:
    - ''
    -
      - Fn::GetAtt: [ MyS3Bucket, Arn ]
      - '/Contracts/*'

instead of:

Resource: 
  Fn::Join: ['', [Fn::GetAtt: [ MyS3Bucket, Arn ], '/Contracts/*'] ] 

I don’t know why, but this kind of formatting has worked for me in the past, whereas I also got YAML errors with the shorter syntax.