Skip to main content
  1. tech-posts/

Cloud Resume Challenge Part 2/4 - Front-End

··17 mins

Draft Content #

Introduction #

This post is the second part in as series of my story on the Cloud Resume Challenge. You can read the first part here.

System Diagram #

Below is the system diagram for the whole system. In this post we are looking at the Front-end part specifically.

aws-cloud-resume-system-diagram

Implementation Details #

HTML & CSS implementation #

The first task that I did to build the cloud resume is to draft the content of my resume on a piece of paper. I made a list of things I want to include in the resume, such as “About me”, “Working Experience”, “Education”, “Certification”, etc.

Having the draft content ready, the next step is to work on the website itself, i.e. preparing HTML and CSS file. I’ve never had any experience developing a website before, so I took a crash course from W3Schools. After finishing the basic tutorial for HTML and CSS there, I attempted to make the HTML/CSS cloud resume from scratch, unfortunately it didn’t look pretty πŸ˜€. It was too plain and not responsive (i.e. the layout of my page didn’t scale well when I open it from either smartphone or large monitor).

After realizing that creating a HTML-CSS website from scratch can be really time consuming, especially if I want to make the page pretty, I started looking up in the Internet on how other Cloud Resume challenger built their resume. From my research, I found out that some people use open source Bootstrap template from Start Bootstrap (MIT License). Checking around some templates there, I decided to download this clean resume template and made some modification to the HTML and CSS (e.g. modifying the section, and changing brand color from orange to green).

S3 static website #

Having the HTML and CSS ready, the next step is to upload them to AWS object storage S3 to serve them as static website. There are four design patterns that we can choose to host a static website in S3 (see list below). This article from AWS:rePost elaborates the details of each pattern.

  1. Use a REST API endpoint as the origin, and restrict access with an origin access control (OAC) or origin access identity (OAI). Note: It’s a best practice to use origin access control (OAC) to restrict access. Origin access identity (OAI) is a legacy method for this process.
  2. Use a website endpoint as the origin, and allow anonymous (public) access.
  3. Use a website endpoint as the origin, and restrict access with a Referer header.
  4. Use AWS CloudFormation to deploy a static website endpoint as the origin, and custom domain pointing to CloudFront.

From the four options available, eventually I chose the option 3.

I dropped the option 4 because I want to do this project the hard way. Although using CloudFormation is convenient as it can help us abstract a lot of details, I left it out because the purpose of this project is to learn. Manual work is more effective to learn all moving parts.

Next, the difference between option 1 versus option 2 or 3 is the S3 endpoint. To serve static HTML/CSS resources in S3 via CloudFront, we can have either the S3 website endpoint or the S3 REST API endpoint (the default endpoint) to be set as CloudFront origin. This AWS documentation page shows the difference between using S3 website endpoint and S3 REST API endpoint. For my cloud resume challenge, in my opinion, using the website endpoint (dropping option 1) is better because of 2 reasons below.

  1. It supports redirection. I can redirect visitors from https://www.«your-domain».com to https://«your-domain».com
  2. It returns the index.html by default when we request the root bucket. In term of user experience, this is an important thing. With S3 RESTAPI endpoint, user must insert the URL https://«your-domain».com/index.html to reach the home page. On the contrary, using the S3 website endpoint, users only need to insert https://«your-domain».com/ into the address bar to reach the website home page.

Finally, the difference between option 2 and 3, are whether we want our system to be a little restricted or completely public. Without restricting access utilizing a custom Referer header, a user can bypass CloudFront CDN to access the S3 static website if he/she knows the S3 website endpoint. There is one thing to note regarding using S3 website endpoint, we cannot enforce SSL encryption when visitor accessing the S3 directly (no HTTPS). For security reason, I do not like having user bypassing CloudFront CDN and letting them accessing S3 in HTTP. Fortunately, the combination of Referer header and bucket policy can be used to resolve this problem. In CloudFront’s custom header configuration, we can insert secret/password as the header value, and have the S3 bucket to restrict the access only if the secret value is present in the header. Although this configuration can improve security, we have to remember that this method is not 100% foolproof. If somehow the secret value of our CloudFront’s custom header leaked, then the user can still bypass the CloudFront CDN and accessing the S3 in plain HTTP. Nevertheless, this project is not that sensitive that require extra protection, so I chose option 3 eventually.

DNS #

Where to buy domain name #

When I started this project, I didn’t have any domain name yet. I have two requirements when buying a domain:

  1. Reliable in the long run
  2. Cheaper in the long run

Looking around in the Internet, here are some places I can register a new domain.

  1. Namecheap
  2. GoDaddy
  3. AWS Route 53
  4. Google Domain

Considering reliability, the Namecheap and GoDaddy seems to have better reputation as they have been in domain business longer than AWS or Google. But speaking about cost, the AWS and Google are cheaper. Based on my rough estimate, the cost of owning 1 domain for 10 years is cheaper in AWS or Google by 4,000 JPY. Since the price difference is small even in 10 years, I decided to buy the domain timmytandian.com from Namecheap.

Moving domain name management from Namecheap to AWS Route 53 #

Because I want to manage some DNS records of my domain name using Terraform later, I decided to move the domain management from Namecheap to AWS Route 53. To do so I need to change the Name Servers (NS) of my domain name from using the NameCheap NS to use AWS Route 53 NS.

Procedure:

  1. Go to AWS Route 53, create a new hosted zones
    • Name: timmytandian.com
    • Hosted zone type: public
  2. Copy the 4 NS record listed in AWS Route 53 hosted zone
  3. Go to NameCheap > Domain > NameServers
  4. Change the DNS from “Basic DNS” to “Custom DNS”
  5. Register the 4 NS records noted in step 2 to NameCheap record.

HTTPS and security hardening #

Regarding HTTPS and security hardening, there are four areas where I made action:

  1. Configuring CloudFront as Content Delivery Network (CDN)
  2. Restricting access with CloudFront custom Referer header and bucket policy
  3. Configuring SSL certificate for my domain
  4. Implementing security header policy

1. Configuring CloudFront as CDN #

To configure HTTPS for the cloud resume website, I used CloudFront. There are two main benefits of using CloudFront for CDN. First, we can cache the static website resource in AWS edge location so that visitors from Europe or America don’t need to fetch the website resource from Japan (lower latency). The second benefit is from security standpoint. Remember, our S3 uses the website endpoint, so it is not capable of using HTTPS connection. Having visitors to access the website via CloudFront would enable me to enforce HTTPS protocol.

About the CloudFront distribution settings, below are some configurations that I made. Beside these, I mostly use the default value.

  • Make the S3 website endpoint as origin
  • Set Referer custom header (see next section)
  • Redirect viewer’s HTTP protocol to HTTPS
  • Configure Lambda at edge function for the security header policy
  • Use price class 100 (actually using price class all should be fine since my cloud resume usage is still within the free tier coverage)
  • Integrate with Amazon Certificate Manager (ACM) to deal with the SSL certificate

2. Restricting access with CloudFront custom Referer header and bucket policy #

As mentioned before, I want to force the website visitors to access my cloud resume only via CloudFront, preventing them to access the static website resource from S3 directly. To implement this, we can use the combination of CloudFront custom Referer header and S3 bucket policy.

The configuration in CloudFront is pretty straight forward. We only need to set a secret value in the custom header section in origin configuration.

cloudfront-referer-header image

Furthermore, we also need to tell S3 to have bucket policy that rejects all requests unless the Referer custom header contains the secret value we set in CloudFront. The S3 bucket policy looks like below.

{
	"Version": "2012-10-17",
	"Id": "server S3 static website using from CloudFront",
	"Statement": [{
		"Sid": "Allow only GET requests originating from CloudFront with specific Referer header",
		"Effect": "Allow",
		"Principal": "*",
		"Action": [
			"s3:GetObject",
			"s3:GetObjectVersion"
		],
		"Resource": "arn:aws:s3:::timmytandian.com/*",
		"Condition": {
			"StringLike": {"aws:Referer": "<the-secret-value>"}
		}
	}]
}

3. Configuring SSL certificate for my domain #

To configure SSL certificate for my domain, I use Amazon Certificate Manager (ACM). Here are some key points regarding ACM that I learned:

  • There are multiple Certificate Authority (the root organization/system who issues certificate), and there are not so many CAs that we can choose. The name of root CA that publish ACM cert is Amazon Trust Services.
  • The root CA doesn’t publish all certificate alone. Instead, the publishing is helped by intermediate CA. The intermediate CA is not disclosed.
  • The validity of ACM-published certificate is 13 months (395 days), and ACM manages the renewal. We can’t op-out from this renewal process.
  • 1 ACM certificate can be paired with max 10 domains. We can use wildcards (*, ?) when specifying domain. The wildcards are only applicable for 1 sub domain.
  • There are 2 kinds of encryption-decryption algorithm supported by ACM: RSA and ECDSA. The ECDSA is newer and lighter (yet has the same security strength as the longer RSA). RSA is older but compatible with legacy applications. If possible we should choose ECDSA, especially for IoT apps that has low power/memory. ACM provides 3 types of key algorithm: RSA 2048, ECDSA P-256 and P-384. For the most optimal security and simplicity, the ECDSA P-384 would be better, but because CloudFront’s support for key algorithm is limited to 256-bit key length only, eventually I chose the ECDSA P-256 ("EC_prime256v1").
  • Fundamentally ACM is scoped per region. We need to create multiple certificates for each region we want to use. In my cloud resume case, I use the ACM in Virginia region because the ACM is paired with CloudFront distribution, which must be managed from AWS Virginia region.
  • ACM public certificate is included in free tier πŸŽ‰.
  • To link SSL certificate with a domain name, domain ownership must be validated by registering CNAME record in our domain database.
  • At 60 days prior to expiration, ACM checks for the following renewal criteria: (1) The certificate is currently in use by an AWS service. (2) All required ACM-provided DNS CNAME records are present and accessible via public DNS. If these criteria are met, ACM considers the domain name to be valid and then it renews the certificate.

To let Amazon Trust Service CA to issue certificate for our domain, we have to give it permission by registering a CAA record in our DNS record. When we add a CAA record to the DNS hosted zone, we specify three parameters separated by space with format like this: <flags> <tag> "value"

  • Flags: Can be either 0 or 128 (This value prevents the CA from issuing a certificate if the CA doesn’t support the feature.). Basically wechoose 0
  • Tag: Either “issue” or “issuewild”.
    • Use “issue” to authorize the CA to issue cert for only for certain domain/subdomain.
      • Use “issuewild” to authorize the CA to issue a wildcard certificate.
  • Value: use one of the value below to specify the CA that is authorized to issue the cert.

When I configure the timmytandian.com CAA in Route 53, I registered this: 0 issuewild "awstrust.com". Please note that I use the “issuewild” parameter because I would like to use 1 certificate publication to cover timmytandian.com, *.timmytandian.com domain name.

4. Implementing security header policy #

Until this point, configuring my cloud resume front-end seems to work well so far. However, seeing the cloud resume from website security best practice point of view, I feel my cloud resume front-end is not good enough. Using Mozilla Observatory, we can get a rough idea of how well a website follows security best practice. To be specific, Mozilla Observatory works by checking whether our website has configured security-related headers like below or not.

  • Strict-Transport-Security - This header instructs the browser to only access our website over HTTPS, and to block any HTTP requests.
  • Content-Security-Policy - This header allows us to specify which resources are allowed to be loaded on our website, including scripts, stylesheets, and images. This can help prevent cross-site scripting (XSS) attacks.
  • X-Content-Type-Options - This header tells the browser not to guess the content type of a resource based on its file extension,but instead to rely on the Content-Type header sent by the server. This can help prevent MIME-type sniffing.
  • X-Frame-Options - This header instructs the browser not to load our website inside an iframe on another website, which can help prevent click-jacking attacks
  • X-XSS-Protection - This header enables a built-in browser protection mechanism that can help prevent XSS attacks by detecting and blocking potentially malicious scripts.
  • Referrer-Policy - This header controls what information about the user’s browsing history is sent to other websites when they click on a link to our website. This can help protect user privacy by preventing other websites from tracking the user’s activity on our website.

Before I configured any security headers onto the CloudFront, my cloud resume got F score (the user interface of Mozilla Obseratory has changed, the screen shot below was taken in 2023).

Mozilla Observatory F-score image

To make our cloud resume following security best practice, I have two choices on how to implement the security header. The first option is to use CloudFront’s managed Response Header Policy. With this, basically CloudFront would automatically add the response headers according to what we have configured in its behavior section. The second method is to utilize edge computing like CloudFront Function or Lambda Edge Function.

The edge computing method is more flexible because this allows us to implement the security header by writing a small JavaScript code. Due to this reason eventually I chose implementing the security header using Lambda Edge Function. Here are the steps I took to do so:

  1. Go to Lambda page, choose us-east-1 region
  2. Create new function from blueprint by searching “cloudfront” keyword. The blueprint to choose is “CloudFront Modify Response Header”.
  3. Replace the default Node.js code with code below. I need to customize the “Content Security Policy” header to make sure external script/styling from Bootstrap, FontAwesome, and GoogleFont to work well.
'use strict';

exports.handler = (event, context, callback) => {
	//Get contents of response
	const response = event.Records[0].cf.response;
	const headers = response.headers;

	//Set new headers
	headers['strict-transport-security'] = [{key: 'Strict-Transport-Security', value: 'max-age= 63072000; includeSubdomains; preload'}];
	headers['content-security-policy'] = [{key: 'Content-Security-Policy', value: "default-src 'self'; img-src 'self'; script-src 'shttps://cdn.jsdelivr.net/ https://use.fontawesome.com/; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com/ https:/fontawesome.com/; object-src 'none'; font-src fonts.gstatic.com"}];
	headers['x-content-type-options'] = [{key: 'X-Content-Type-Options', value: 'nosniff'}];
	headers['x-frame-options'] = [{key: 'X-Frame-Options', value: 'DENY'}];
	headers['x-xss-protection'] = [{key: 'X-XSS-Protection', value: '1; mode=block'}];
	headers['referrer-policy'] = [{key: 'Referrer-Policy', value: 'same-origin'}];

	//Return modified response
	callback(null, response);
};

Javascript #

Rough idea to track visitor count #

The role of javascript in the cloud resume front-end is to count website visitors. Initially I didn’t have any idea on how I should track the visitor count data. After doing some researches to figure out how to count website visitors, I learned that we must prepare a persistent storage to save the number of website visitor count. Persistent storage may have several forms:

  • It can be a text file stored in S3. This is one of the most primitive/naive method.
  • It can be a simple key-value database like DynamoDB.
  • It can be full-fledged relational database (RDB) like MySQL.

For this cloud resume, which only tracks 1 parameter, a simple key-value database like DynamoDB is sufficient. In order to count the website visitor, I need to store the data in DynamoDB. Reading or updating DynamoDB content can be done with the help of Lambda function. However, the JavaScript in front-end cannot call the Lambda function directly, we need an interface to call the Lambda function. Calling the Lambda function can be done securely via AWS API Gateway. In essence, the JavaScript’s role is to call the API Gateway which will have Lambda to read and update visitor count data in DynamoDB.

I will discuss about the implementation of DynamoDB, API Gateway, and Lambda Function in my next blog post about the back-end. For now, I am going to explain the JavaScript code logic. It is simple. The script tries to call API Gateway to get visitor count data. If it succeeds, then we show the count data in DOM (Document Object Model)/browser. If the API call fails, then show “(Under development)” as placeholder. See the code below.

function updateVisitCounterElmtInnerHtml(){
    // select the element to update
    var visitCounterElmt = document.querySelector("#visit-count");
    getVisitorCount()
    .then(
        response => {
            visitCounterElmt.innerHTML = response
        },
        errorResponse => {
            visitCounterElmt.innerHTML = "(under development)"
        }
    )
}

API Gateway invocation is performed inside the getVisitorCount function using the fetch API. Please notice that invoked API endpoint belongs to AWS domain. Behind the scene, for every API call the user made, it will not only fetch the visitor count data, but also increment the counter at the database back-end at the same time (this behavior is something I implemented in back-end Lambda function using python code).

async function getVisitorCount(){
    // define the API endpoint
    const apiEndpoint = new URL("https://vo422t3t39.execute-api.ap-northeast-1.amazonaws.com/counts/250808e1-38f9-2c29-90b9-5146319be0c3?func=addOneVisitorCount");
    
    // fetch the data from the database
    try {
        const apiResponse = await fetch(apiEndpoint, {
            method: "GET",
        });
        if (!apiResponse.ok) {
            let errorTitle = `Fetch API response not OK (status ${apiResponse.status})` 
            throw new Error(errorTitle);
        }
        // ...
}

Attempt on making the visitor count more accurate #

There is one thing that bothered me regarding the approach explained above. Because the method above is so simple, the API endpoint accepts all request without any authorization or double check, causing the number of visitor count to be less accurate. Consider these two scenarios where over-count may happen.

  • Suppose a person copies the API endpoint and paste in his/her browser, that person will increase the website visitor count even though he/she doesn’t visit my cloud resume directly.
  • Because the API will be called every time the web page is loaded, we may have more visit counts if a person refresh the web page multiple times.

I then researched ways to improve visitor count accuracy. Here’s what I found:

  • We can use cookies, i.e. setting a unique identifier in visitors’ browser to track subsequent visits. For example, cookie expiration date can prevent double-counting when a visitor presses F5.
  • Tracking pixels in the website. With this method, we embed a 1x1 pixel image in the cloud resume website. Whenever the web page and the tiny image are requested and loaded, then we track the visitors’ information (IP address, user-agent, etc.) to increment the count.
  • Use third party service like Google Analytics. In this method we configure our website to collect some data for every visit and send the data to Google for analysis.

In the end I got rid the idea of counting visitor accurately. Sure they are fancy and useful, however the cons of implementing them outweighs the benefits, at least in my simple cloud resume case. The biggest problem with the three options above is privacy regulation. Privacy regulations, like GDPR, require user consent when we want to analyze cookie or send logs to third party. Furthermore, some people are quite sensitive with privacy. I guess having a pop up window asking for privacy consent would be a turn off for some readers.

In the end, I just went with the old style method, which is simply counting up the visitor count every time a website is loaded, because accuracy is not my priority in this project.

Conclusion #

Phew, this post is pretty long. To close this post, let me conclude all of the key points that I made when implementing the cloud resume front-end.

  • HTML & CSS: use Bootstrap framework template to have a base source code, then make further customization based on it.
  • S3 static website: use the S3 website endpoint and restrict the access by allowing the request only from CloudFront CDN.
  • DNS: buy timmytandian.com domain name from NameCheap and manage the domain from AWS Route 53.
  • HTTPS: use ACM to manage and renew SSL certificate automatically, and also have CloudFront to enforce HTTPS protocol.
  • More securities improvement: use custom Referer header to only allow website access via CloudFront instead of accessing it from S3 directly, and implement some security headers like Strict-Transport-Security, Content-Security-Policy, etc. using Lambda Edge function to adhere to website security best practice.
  • JavaScript: code it to call API Gateway (back-end) to retrieve and update visitor count from DynamoDB database at the same time. No need of fancy tricks like cookies or Google Analytics to track the visitor count accurately; my cloud resume doesn’t need extra accuracy.

Hope you learned something! See you in my next post on the back-end implementation.