Protect Private Content S3 using AWS CloudFront

AWS SA Fundamentals

AWS S3 with Nodejs practice

Amazon S3 + Amazon CloudFront

As the title, you have a resource on S3, and you don’t want it can be accessed on a simple URL directly, it just only accesses from your authorized application. We try with the signed URL and signed cookies.

First, prepare S3 content

create S3 Bucket and upload 2 files avatar.jpg and index.html (https://github.com/nhancv/nc-aws/tree/master/aws_examples/res/cloudfront_s3)

Fill the bucket name to create S3 Bucket, Uncheck `Block all public access` and Confirm to acknowledge that, then leave the rest of the settings to their default values.

Upload 2 example files

Update Bucket Policy and Save

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "nhancv",
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::cloudfront-s3-001/*"
        }
    ]
}

Now try to access S3 resources and It works

https://cloudfront-s3-001.s3.us-east-2.amazonaws.com/avatar.jpg

https://cloudfront-s3-001.s3.us-east-2.amazonaws.com/index.html

Second, config CloudFront

Click on Create Distribution

Click on Get Started for Web

Attach S3 bucket at Origin Domain Name

Select “Yes” for “Restrict Bucket Access”
Select “Create a New Identity” at “Origin Access Identity”
Select “Yes, Update Bucket Policy” at “Grant Read Permission on Bucket”

Go to “Default Cache Behavior Settings” section

Select “HTTPS Only” if you want to drop any HTTP traffic

Select “Yes” at “Restrict Viewer Access (Use Signed URLs or Signed Cookies”

Leave the rest of the settings to their default values and “Create Distribution”

When it has done click on “Distribution” on the left menu

NOTE: you can get an error when creating distribution

com.amazonaws.services.cloudfront.model.AccessDeniedException: Your account must be verified before you can add new CloudFront resources. To verify your account, please contact AWS Support (https://console.aws.amazon.com/support/home#/) and include this error message. (Service: AmazonCloudFront; Status Code: 403; Error Code: AccessDenied; Request ID: cfce2ecb-1ff6-4de4-8940-0bdc392d0dd4)

=> To solve that error

Goto https://console.aws.amazon.com/support/home#/Support / Create New case / Service limit increaseLimit type: CloudFront DistributionsIn Requests select:Limit: Web Distributions per AccountNew limit value: 10Case: I need to use CloudFront service

You will get an approved message like this, the process may take 1 business day

Now back to CloudFront distribution detail to get “Domain Name” for a new way to access S3 resource

Now try to access: https://d1ywg9iw63kklz.cloudfront.net/avatar.jpg (remember use https if you choose HTTPS Only in Distribution setting)

If you see the above error, you have configured everything correctly. 🙂

Now come back to Bucket Policy, you will see two policies available here

Remove the public policy with Sid = “nhancv”, then Click Save -> The ‘Public’ label at “Bucket Policy” will gone

Now try to access https://cloudfront-s3-001.s3.us-east-2.amazonaws.com/avatar.jpg

Now you can not access S3 resource via S3 link or CloudFront link directly. The valid link needs to be signed.

Integrate with Nodejs

Create a new CloudFront KeyPair

Note: IAM users can’t create CloudFront key pairs. You must log in using root credentials to create key pairs.

Expand CloudFront key pairs -> Create New Key Pair -> Download Private Key File (.pem file)

NodeJs project

Create new nodejs project, you can read AWS S3 with Nodejs practice

I use aws-sdk with NestJs for all, but for this situation, you can use `aws-cloudfront-sign` npm package.

Create new CloudFront service with NestJS cli

nest g s aws/cloudfront

Put your CloudFront secret key pem file to <project>/keys

CloudFront service source here

https://gist.github.com/nhancv/425fff956ca0580514cb19ee31b838a5#file-cloudfront-service-ts

Update main.ts

import {NestFactory} from '@nestjs/core';
import {AppModule} from './app.module';
import {CloudfrontService} from "./aws/cloudfront/cloudfront.service";

async function bootstrap() {
const app = await NestFactory.create(AppModule);
const cloudFront = app.get(CloudfrontService);
const singedUrl = await cloudFront.getSignedUrl('https://d1ywg9iw63kklz.cloudfront.net/avatar.jpg');
console.log({singedUrl});
}

bootstrap();

Run with npm start The valid link looks like:

Access to signed URL and it works

You will get the error when time expires

Now try with Signed Cookies

const singedCookie = await cloudFront.getSignedCookie('d1ywg9iw63kklz.cloudfront.net');
console.log({singedCookie});

Test with Postman, add 3 cookies to request

And send request


The above solution just for specific resources. But what happens if the S3 resource linked together. For example, the index.html file uploaded to S3 displays the avatar.jpg file.

<!--index.html-->
<img src="./avatar.jpg">

Let try with the signed URL to index.html

const singedUrl = await cloudFront.getSignedUrl('https://d1ywg9iw63kklz.cloudfront.net/index.html');
console.log({singedUrl});

As you see, the index.html accessed success but can not fetch avatar.jpg which also place on S3

Because of the resource place on S3 using the CloudFront domain, we can not set the Signed Cookie to that domain while the Signed URL just only access to a specific file.

The idea is to make a new sign.html file to parse URL params and set it to cookie and upload it to S3.

https://gist.github.com/nhancv/db9ff484f6bcdf4b5e72bf38cb701d7f#file-sign-html

Create a Signed Cookie for CloudFront domain. Put Signed Cookies as a URL params to sign.html file to get a redirect URL. The final step is to sign that redirect URL.

async function bootstrap() {
const app = await NestFactory.create(AppModule);
const cloudFront = app.get(CloudfrontService);
const singedCookie = await cloudFront.getSignedCookie('d1ywg9iw63kklz.cloudfront.net');
const BPolicy = singedCookie['CloudFront-Policy'];
const BKeyPairId = singedCookie['CloudFront-Key-Pair-Id'];
const BSignature = singedCookie['CloudFront-Signature'];
const urlNeedSign = `https://d1ywg9iw63kklz.cloudfront.net/sign.html?BPolicy=${BPolicy}&BKey-Pair-Id=${BKeyPairId}&BSignature=${BSignature}`;
const singedUrl = await cloudFront.getSignedUrl(urlNeedSign);
console.log({singedUrl});
}
bootstrap();

When you access that URL, it will redirect to index.html file automatically

Now copy Url `https://d1ywg9iw63kklz.cloudfront.net/index.html` on new Browser instance (just open Incognito)

It works!

The repository here, I will update more examples later.

https://github.com/nhancv/nc-aws


Leave a Reply

Your email address will not be published.Required fields are marked *