Setting up AWS S3 and CloudFront with Signed URLs using CDK

September 19, 2024

Setting up AWS S3 and CloudFront with Signed URLs using CDK

Private S3 bucket and CloudFront distribution with signed URLs


In this post, we'll walk through how to set up a private AWS S3 bucket and CloudFront distribution using the AWS CDK (Cloud Development Kit). We'll focus on creating a secure setup that uses presigned URLs for content access.

Why CloudFront Signed URLs in TagBug?

At TagBug, we prioritize both the performance and privacy of our users' data. When handling sensitive information such as screenshots, videos, console logs, and network requests uploaded by users, it's crucial to ensure not only that this content is securely stored and not publicly accessible, but also that it can be quickly and efficiently delivered when needed. This is where CloudFront signed URLs play a vital role in our infrastructure, offering a perfect balance of security and speed.

CloudFront's global network of edge locations allows us to serve content with low latency, regardless of where our users are located. At the same time, signed URLs provide a robust security mechanism, ensuring that only authorized users can access the sensitive data. This combination of performance and privacy is essential for maintaining the trust of our users while delivering a smooth, responsive experience.

How CloudFront Signed URLs help for privacy?

  • Time-Limited Access: Signed URLs can be set to expire after a certain period, ensuring that access to the content is temporary.
  • Restricted Access: Only users with the correct signed URL can access the content, preventing unauthorized viewing or sharing.
  • IP Restriction: We can optionally restrict access to specific IP addresses, adding an extra layer of security.

At TagBug, we are using time-limited access for cloudfront signed urls with private S3 bucket.

Prerequisites

  • AWS account
  • AWS CDK installed
  • Node.js and TypeScript

A private S3 bucket

// I prefer using a fixed bucket name,
// you can leave it blank to let CDK generate a random name
const bucketName = "my-bucket-name";
const s3CorsRule: s3.CorsRule = {
  allowedMethods: [
    s3.HttpMethods.GET,
    s3.HttpMethods.HEAD,
    s3.HttpMethods.POST,
    s3.HttpMethods.PUT,
  ],
  allowedOrigins: ["*"],
  allowedHeaders: ["*"],
  exposedHeaders: ["ETag"],
  // 1 day
  maxAge: 60 * 60 * 24,
};

const s3Bucket = new s3.Bucket(this, "S3Bucket", {
  bucketName,
  blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
  accessControl: s3.BucketAccessControl.PRIVATE,
  cors: [s3CorsRule],
  // I enabled transfer acceleration to improve the upload speed,
  // you can disable it if you don't want to use it.
  transferAcceleration: true,
});

Setup CloudFront

First, please follow Creating CloudFront Key Pairs to generate a public key and private key.

Then put your public key to CF_PUBLIC_KEY environment variable.

Setup your cloudfront distribution with the following code:

if (!process.env.CF_PUBLIC_KEY) {
  throw new Error("CF_PUBLIC_KEY is not set");
}

const pubKey = new cloudfront.PublicKey(this, "pubkey_1", {
  encodedKey: process.env.CF_PUBLIC_KEY,
});

const keyGroup = new cloudfront.KeyGroup(this, "pubkey_group_1", {
  items: [pubKey],
});

const distribution = new cloudfront.Distribution(this, "BackendCF", {
  defaultBehavior: {
    origin: new origins.S3Origin(s3Bucket),
    allowedMethods: cloudfront.AllowedMethods.ALLOW_GET_HEAD_OPTIONS,
    viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
    trustedKeyGroups: [keyGroup],
    cachePolicy: cloudfront.CachePolicy.CACHING_OPTIMIZED,
    originRequestPolicy: cloudfront.OriginRequestPolicy.CORS_S3_ORIGIN,
    responseHeadersPolicy:
      cloudfront.ResponseHeadersPolicy.CORS_ALLOW_ALL_ORIGINS,
  },
});

The code above use the recommended way by AWS to setup CORS settings.

Create signed URL in your application's code

After setup the infrastructure, you can create a signed URL using the following code:

export function getCDNSignedUrl(key: string, expiresInSeconds: number): string {
  const url = `${env.CF_BASE_URL}/${key}`;
  const dateLessThan = new Date(
    Date.now() + expiresInSeconds * 1000,
  ).toISOString();
  const signedUrl = getCFSignedUrl({
    url,
    keyPairId: env.CF_KEY_ID ?? "",
    dateLessThan,
    privateKey: env.CF_PRIVATE_KEY ?? "",
  });
  return signedUrl;
}

In the above code:

  • env.CF_BASE_URL is the base URL of the CloudFront distribution.
  • env.CF_KEY_ID is the ID of the CloudFront key pair. You can find it in the CloudFront console.
  • env.CF_PRIVATE_KEY is the private key of the CloudFront key pair you generated in the previous step.