Building a Secure & Scalable S3 and CloudFront Architecture with AWS CDK

23 March 2025, 5 min read

In today's cloud-first world, delivering static assets (like images, scripts, and stylesheets) quickly and securely is paramount. AWS offers a powerful combination of Amazon S3 for storage and Amazon CloudFront for content delivery. In this post, we’ll walk through a TypeScript CDK stack that sets up an S3 bucket with robust security controls and a CloudFront distribution configured with modern best practices.

Overview

Our AWS CDK stack creates two primary resources:

  • Amazon S3 Bucket: Serves as the origin for static assets. We enforce best practices by blocking public access and setting strict bucket policies.
  • Amazon CloudFront Distribution: Provides global content delivery with low latency. CloudFront fetches objects from our S3 bucket using an origin access control, ensuring that content is delivered securely.

By using AWS CDK, the infrastructure is defined as code, enabling you to version, review, and deploy your cloud resources consistently.

The Code Walkthrough

Below is the complete code snippet of our stack:

import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";
import * as s3 from "aws-cdk-lib/aws-s3";
import * as cloudfront from "aws-cdk-lib/aws-cloudfront";
import * as origins from "aws-cdk-lib/aws-cloudfront-origins";

export class S3CloudFrontStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const stack = this;
    const env = props?.env;

    const stage = stack.node.tryGetContext("stage");
    const stageConfig = stack.node.tryGetContext(stage);

    stageConfig["stage"] = stage;
    const region = env?.region;

    const s3CorsRule: s3.CorsRule = {
      allowedMethods: [s3.HttpMethods.GET, s3.HttpMethods.HEAD],
      allowedOrigins: ["*"],
      allowedHeaders: ["*"],
      maxAge: 300,
    };

    // Create an S3 bucket with all public access blocked
    const s3Bucket = new s3.Bucket(stack, `S3Bucket-${stage}`, {
      bucketName: `my-bucket-${stage}`,
      blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
      accessControl: s3.BucketAccessControl.PRIVATE,
      cors: [s3CorsRule],
      objectOwnership: s3.ObjectOwnership.BUCKET_OWNER_ENFORCED,
    });

    // Create Cloudfront distribution with S3 as Origin
    const distribution = new cloudfront.Distribution(
      stack,
      `cf-Distribution-${stage}`,
      {
        defaultBehavior: {
          origin: origins.S3BucketOrigin.withOriginAccessControl(s3Bucket, {
            originPath: "/public",
          }),
        },
      }
    );

    // Output the CloudFront distribution domain name so you can use it to serve your images
    new cdk.CfnOutput(stack, "DistributionDomainName", {
      value: distribution.distributionDomainName,
    });
  }
}

Let’s break down the essential parts.

Setting Up the S3 Bucket

Security First

The S3 bucket is created with a focus on security:

  • Public Access Blocked:

    Using blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL ensures that no public policies can be applied, reducing the risk of unintentional exposure.

  • Private Access Control:

    The accessControl property is set to PRIVATE, meaning only authenticated and authorized requests can access the bucket.

  • Object Ownership:

    By enforcing s3.ObjectOwnership.BUCKET_OWNER_ENFORCED, the bucket owner retains control over all objects, regardless of who uploads them. This simplifies permissions management.

CORS Configuration

The provided CORS rule allows only GET and HEAD methods, making it suitable for serving static assets while keeping the configuration as restrictive as possible. For development, broad rules (such as allowing all origins and headers) might be acceptable. However, in production, you may wish to restrict these settings further.

Configuring CloudFront Distribution

The CloudFront distribution is set up to use the S3 bucket as its origin:

  • Origin Access Control:

    Instead of the legacy Origin Access Identity (OAI), the stack uses origins.S3BucketOrigin.withOriginAccessControl to establish secure communication between CloudFront and the S3 bucket. This modern approach simplifies the management of permissions.

  • Origin Path:

    By setting originPath: "/public", CloudFront is configured to only serve content from the public folder in the S3 bucket. This is useful if your bucket contains both public and private assets, ensuring that only the intended files are accessible through the CDN.

Leveraging Context for Flexibility

The stack uses context variables such as stage to dynamically configure the environment. This makes the stack reusable across different stages (e.g., development, staging, production) without hardcoding environment-specific values.

const stage = stack.node.tryGetContext("stage");
const stageConfig = stack.node.tryGetContext(stage);
stageConfig["stage"] = stage;

By injecting context values, you can adapt bucket names, distribution identifiers, and other configurations to meet your environment’s requirements.

Outputting Useful Information

The final part of the stack outputs the CloudFront distribution’s domain name. This output is essential because it allows you to easily integrate the distribution into your application or DNS configuration.

new cdk.CfnOutput(stack, "DistributionDomainName", {
  value: distribution.distributionDomainName,
});

This makes it convenient to reference the endpoint for serving your content directly from CloudFront.

Considerations & Best Practices

  • Context Validation:

    Ensure that the stage context is provided when deploying the stack. Adding validation or default values can help prevent misconfigurations.

  • Unused Variables:

    In this example, the region variable is defined but not used. If not needed, consider removing such variables to keep your code clean.

  • Origin Path Impact:

    Remember that the originPath parameter restricts the distribution to a specific folder (/public in this case). Confirm that this aligns with your folder structure and deployment strategy.

  • Tightening CORS:

    Although the current CORS rule works for a variety of use cases, refining these settings for production can help further secure your static assets.

Conclusion

Integrating S3 with CloudFront using AWS CDK is a robust solution for hosting and delivering static assets securely. This stack not only demonstrates how to establish a secure connection between S3 and CloudFront using modern features like origin access control but also highlights best practices such as enforcing strict access policies and leveraging context for multi-environment deployments.

By following this approach, you can ensure that your content is delivered quickly to users worldwide while maintaining tight security over your data. Whether you’re building a personal project or scaling up a commercial application, this stack serves as a solid foundation for your cloud infrastructure.

Happy coding!

buymeacoffee