Back to Home

How to create private S3 bucket + CloudFront with OAC

Using Cloudfront with an Amazon S3 bucket keeps allows us to prevent direct access to the S3 bucket while allowing viewers (users) to access the content in the bucket only through the specified CloudFront distribution.

CloudFront provides two ways to achieve this.

  • Origin Access Control - OAC
  • Origin Access Identity - OAI (legacy)

In this article we will utilize terraform automate provisioning the AWS resources.

Create an S3 bucket

1resource "aws_s3_bucket" "main" { 2 bucket = "s3-cloudfront-oac.raugustine.xyz" 3 4 tags = { 5 Environment = "dev" 6 Terraform = true 7 } 8} 9 10resource "aws_s3_bucket_acl" "main" { 11 bucket = aws_s3_bucket.main.id 12 acl = "private" 13}

Create the CloudFront distribution with OAC

1resource "aws_cloudfront_origin_access_control" "main" { 2 name = "s3-cloudfront-oac" 3 description = "Grant cloudfront access to s3 bucket ${aws_s3_bucket.main.id}" 4 origin_access_control_origin_type = "s3" 5 signing_behavior = "always" 6 signing_protocol = "sigv4" 7} 8 9locals { 10 s3_origin_id = "myS3Origin" 11} 12 13resource "aws_cloudfront_distribution" "s3_distribution" { 14 origin { 15 domain_name = aws_s3_bucket.main.bucket_regional_domain_name 16 origin_access_control_id = aws_cloudfront_origin_access_control.main.id 17 origin_id = local.s3_origin_id 18 } 19 20 enabled = true 21 default_root_object = "index.html" 22 23 # Optional - Extra CNAMEs (alternate domain names), if any, for this distribution 24 # aliases = ["mysite.example.com", "yoursite.example.com"] 25 26 default_cache_behavior { 27 allowed_methods = ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"] 28 cached_methods = ["GET", "HEAD"] 29 target_origin_id = local.s3_origin_id 30 31 forwarded_values { 32 query_string = true 33 34 cookies { 35 forward = "none" 36 } 37 } 38 39 viewer_protocol_policy = "allow-all" 40 min_ttl = 0 41 default_ttl = 3600 42 max_ttl = 86400 43 } 44 45 price_class = "PriceClass_100" 46 47 restrictions { 48 geo_restriction { 49 restriction_type = "none" 50 } 51 } 52 53 viewer_certificate { 54 cloudfront_default_certificate = true 55 } 56}

Attach a bucket policy that allows CloudFront to access the S3 bucket

1resource "aws_s3_bucket_policy" "default" { 2 bucket = aws_s3_bucket.main.id 3 policy = data.aws_iam_policy_document.cloudfront_oac_access.json 4} 5 6data "aws_iam_policy_document" "cloudfront_oac_access" { 7 statement { 8 principals { 9 type = "Service" 10 identifiers = ["cloudfront.amazonaws.com"] 11 } 12 13 actions = [ 14 "s3:GetObject" 15 ] 16 17 resources = [ 18 aws_s3_bucket.main.arn, 19 "${aws_s3_bucket.main.arn}/*" 20 ] 21 22 condition { 23 test = "StringEquals" 24 variable = "AWS:SourceArn" 25 values = [aws_cloudfront_distribution.s3_distribution.arn] 26 } 27 } 28}

Here's the full configuration

1resource "aws_s3_bucket" "main" { 2 bucket = "s3-cloudfront-oac.raugustine.xyz" 3 4 tags = { 5 Environment = "dev" 6 Terraform = true 7 } 8} 9 10resource "aws_s3_bucket_acl" "main" { 11 bucket = aws_s3_bucket.main.id 12 acl = "private" 13} 14 15resource "aws_s3_bucket_policy" "default" { 16 bucket = aws_s3_bucket.main.id 17 policy = data.aws_iam_policy_document.cloudfront_oac_access.json 18} 19 20data "aws_iam_policy_document" "cloudfront_oac_access" { 21 statement { 22 principals { 23 type = "Service" 24 identifiers = ["cloudfront.amazonaws.com"] 25 } 26 27 actions = [ 28 "s3:GetObject" 29 ] 30 31 resources = [ 32 aws_s3_bucket.main.arn, 33 "${aws_s3_bucket.main.arn}/*" 34 ] 35 36 condition { 37 test = "StringEquals" 38 variable = "AWS:SourceArn" 39 values = [aws_cloudfront_distribution.s3_distribution.arn] 40 } 41 } 42} 43 44resource "aws_cloudfront_origin_access_control" "main" { 45 name = "s3-cloudfront-oac" 46 description = "Grant cloudfront access to s3 bucket ${aws_s3_bucket.main.id}" 47 origin_access_control_origin_type = "s3" 48 signing_behavior = "always" 49 signing_protocol = "sigv4" 50} 51 52locals { 53 s3_origin_id = "myS3Origin" 54} 55 56resource "aws_cloudfront_distribution" "s3_distribution" { 57 origin { 58 domain_name = aws_s3_bucket.main.bucket_regional_domain_name 59 origin_access_control_id = aws_cloudfront_origin_access_control.main.id 60 origin_id = local.s3_origin_id 61 } 62 63 enabled = true 64 default_root_object = "index.html" 65 66 # Optional - Extra CNAMEs (alternate domain names), if any, for this distribution 67 # aliases = ["mysite.example.com", "yoursite.example.com"] 68 69 default_cache_behavior { 70 allowed_methods = ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"] 71 cached_methods = ["GET", "HEAD"] 72 target_origin_id = local.s3_origin_id 73 74 forwarded_values { 75 query_string = true 76 77 cookies { 78 forward = "none" 79 } 80 } 81 82 viewer_protocol_policy = "allow-all" 83 min_ttl = 0 84 default_ttl = 3600 85 max_ttl = 86400 86 } 87 88 price_class = "PriceClass_100" 89 90 restrictions { 91 geo_restriction { 92 restriction_type = "none" 93 } 94 } 95 96 viewer_certificate { 97 cloudfront_default_certificate = true 98 } 99}

Upload files to the S3 bucket

S3 upload

Test access using the CloudFront distribution name

CloudFront Test

You can find a copy of the terraform configuration on my https://github.com/r-augustine/private-s3-cloudfront-oac

References