Your S3 bucket is private. Nobody can reach it directly, which is what you want. But you still need something to actually serve the files. CloudFront is the CDN layer that sits in front of S3 and delivers your site over HTTPS using the ACM certificate from post 3.
These resources go in the same template as the S3 bucket. The template takes one parameter, DomainName, for your domain. The certificate ARN comes from the cert stack via !ImportValue, so there’s nothing to copy or pass manually.
Origin Access Control
Origin Access Control (OAC) tells CloudFront to sign every request to S3 with SigV4. The bucket stays completely private, but CloudFront can read from it because each request carries a valid signature. This replaces the older Origin Access Identity approach.
CloudFrontOAC:
Type: 'AWS::CloudFront::OriginAccessControl'
Properties:
OriginAccessControlConfig:
Name: !Sub '${DomainName}-oac'
OriginAccessControlOriginType: s3
SigningBehavior: always
SigningProtocol: sigv4
OriginAccessControlOriginType: s3tells CloudFront this OAC is for an S3 origin.SigningBehavior: alwaysmeans CloudFront signs every request, not just some.SigningProtocol: sigv4uses AWS Signature Version 4. This is the only supported protocol for S3.
The Distribution
The AWS::CloudFront::Distribution resource is the core of the site template. It ties together the S3 origin, the OAC, the certificate, and the caching behavior.
CloudFrontDistribution:
Type: 'AWS::CloudFront::Distribution'
Properties:
DistributionConfig:
Enabled: true
DefaultRootObject: index.html
HttpVersion: http2and3
Aliases:
- !Ref DomainName
Origins:
- Id: S3Origin
DomainName: !GetAtt S3WebsiteBucket.RegionalDomainName
S3OriginConfig:
OriginAccessIdentity: ''
OriginAccessControlId: !GetAtt CloudFrontOAC.Id
DefaultCacheBehavior:
TargetOriginId: S3Origin
ViewerProtocolPolicy: redirect-to-https
CachePolicyId: 658327ea-f89d-4fab-a63d-7e88639e58f6 # AWS Managed CachingOptimized policy
Compress: true
ViewerCertificate:
AcmCertificateArn: !ImportValue Example-CertificateArn
SslSupportMethod: sni-only
MinimumProtocolVersion: TLSv1.2_2021
A few things to note:
DefaultRootObject: index.html means a request to https://example.net/ serves index.html from the bucket root. Without this, the root URL returns a 403.
HttpVersion: http2and3 enables HTTP/2 and HTTP/3. HTTP/3 uses QUIC for faster connections, especially on mobile.
Origins uses !GetAtt S3WebsiteBucket.RegionalDomainName to reference the bucket from the same template. The S3OriginConfig.OriginAccessIdentity is set to an empty string because you’re using OAC instead of the legacy OAI. The OriginAccessControlId links to the OAC resource.
CachePolicyId references the AWS-managed CachingOptimized policy. This is a built-in policy that caches based on the full URL with gzip and Brotli compression. You don’t need to define a custom cache policy for a static site.
ViewerCertificate attaches your ACM certificate. !ImportValue Example-CertificateArn pulls the ARN exported by the cert stack from post 3, so the two stacks stay loosely coupled with no hardcoded ARNs. sni-only uses Server Name Indication so CloudFront doesn’t need a dedicated IP address. TLSv1.2_2021 sets the minimum TLS version to 1.2 with a modern cipher suite.
Compress: true enables automatic gzip and Brotli compression for responses. CloudFront compresses files at the edge before sending them to the viewer.
Bucket Policy
The S3 bucket is fully private. You need a AWS::S3::BucketPolicy that grants CloudFront permission to read from it. This is the S3 side of the OAC setup.
A BucketPolicy is a standalone resource. CloudFormation applies it directly to whichever bucket the Bucket property points at. No other resource needs to !Ref it for the policy to take effect.
BucketPolicy:
Type: 'AWS::S3::BucketPolicy'
Properties:
Bucket: !Ref S3WebsiteBucket
PolicyDocument:
Version: '2012-10-17'
Statement:
- Sid: AllowCloudFrontServicePrincipal
Effect: Allow
Principal:
Service: cloudfront.amazonaws.com
Action: 's3:GetObject'
Resource: !Sub '${S3WebsiteBucket.Arn}/*'
Condition:
StringEquals:
'AWS:SourceArn': !Sub 'arn:aws:cloudfront::${AWS::AccountId}:distribution/${CloudFrontDistribution}'
- Sid: DenyInsecureTransport
Effect: Deny
Principal: '*'
Action: 's3:*'
Resource:
- !GetAtt S3WebsiteBucket.Arn
- !Sub '${S3WebsiteBucket.Arn}/*'
Condition:
Bool:
'aws:SecureTransport': 'false'
Sid: AllowCloudFrontServicePrincipalis an optional statement identifier. It shows up in the AWS console and CloudTrail logs, making the policy easier to audit.Principal: Service: cloudfront.amazonaws.comgrants access to CloudFront’s service principal, not a specific distribution yet. The condition handles that.Action: s3:GetObjectis the only permission CloudFront needs. It reads files, nothing else.Condition: StringEquals: AWS:SourceArnscopes the policy to this specific distribution. Without it, any CloudFront distribution in your account could read from this bucket. This prevents the confused deputy problem.Sid: DenyInsecureTransportblocks any request that arrives over plain HTTP. Theaws:SecureTransportcondition key isfalsewhen the request was not sent over TLS. This ensures all traffic to the bucket is encrypted in transit, even requests from other AWS services.
Updating the Role
Your IAM role from post 4 needs permissions for CloudFront and S3 bucket policies. Add two new statements to the inline policy in your role stack.
- Effect: Allow
Action:
- 'cloudfront:CreateDistribution'
- 'cloudfront:UpdateDistribution'
- 'cloudfront:DeleteDistribution'
- 'cloudfront:GetDistribution'
- 'cloudfront:TagResource'
- 'cloudfront:CreateOriginAccessControl'
- 'cloudfront:DeleteOriginAccessControl'
- 'cloudfront:GetOriginAccessControl'
- 'cloudfront:UpdateOriginAccessControl'
- 'cloudfront:CreateResponseHeadersPolicy'
- 'cloudfront:GetResponseHeadersPolicy'
- 'cloudfront:UpdateResponseHeadersPolicy'
- 'cloudfront:DeleteResponseHeadersPolicy'
- 'cloudfront:CreateFunction'
- 'cloudfront:GetFunction'
- 'cloudfront:UpdateFunction'
- 'cloudfront:DeleteFunction'
- 'cloudfront:PublishFunction'
- 'cloudfront:DescribeFunction'
Resource: '*'
- Effect: Allow
Action:
- 's3:PutBucketPolicy'
- 's3:GetBucketPolicy'
- 's3:DeleteBucketPolicy'
Resource: '*'
- CloudFront actions cover the full lifecycle of distributions, Origin Access Controls, response headers policies, and CloudFront Functions.
TagResourcelets CloudFormation tag the distribution. - S3 bucket policy actions let CloudFormation manage the bucket policy that grants CloudFront read access.