In the previous article, we've gone over setting up a static site on S3 and associating it with a custom domain.
Did you notice that the website URL uses HTTP? That's not very secure, and we'd better use HTTPS, which is mainly a layer of encryption added to HTTP. In this post, we will secure the site with an SSL Certificate, which proves ownership of the website and signed by a trusted authority. In addition, we'll build everything with Infrastructure as Code.
Introduction
We assume in this post that there is a website already deployed and working but not secured with HTTPS (If you don't have this implemented, you can have a look at this article).
Before we dig into the technical details, let's go over a high-level plan of what we need to do to secure our site:
- Create an SSL certificate to prove the ownership of the site
- Create a CloudFront distribution that would deliver our content from S3 to the viewers - We don't use S3 directly as S3 static websites don't have support for HTTPS
- Update the A record (a record that maps domain names to IPs) in Route 53 to point to the CloudFront distribution
Don't worry if you're not familiar with these services, as the following section will include a brief explanation of each one.
Create SSL Certificate
We will create an SSL Certificate using AWS Certificate Manager (ACM). If you look at the ACM in the AWS Console, you could choose between requesting a certificate, importing your own certificate or creating a private certificate authority. We want to request a certificate as we would like it to be a public certificate available on the internet and trusted by applications and browsers by default. Private Certificates are mainly used on private networks.
To do it with Terraform, create an acm.tf
file with the following content:
resource "aws_acm_certificate" "cert" {
domain_name = var.domainName
validation_method = "DNS"
lifecycle {
create_before_destroy = true
}
}
It's recommended to specify create_before_destroy = true
in a lifecycle block to replace a certificate that is currently in use.
We have two options to validate the certificate: DNS or Email. When we choose DNS, ACM provides you with one or more CNAME records that must be added to your DNS host zone - These records contain a unique key-value pair that proves that you control the domain. That's how we do it in Terraform:
resource "aws_route53_record" "cert-cname" {
for_each = {
for dvo in aws_acm_certificate.example_cert.domain_validation_options : dvo.domain_name => {
name = dvo.resource_record_name
record = dvo.resource_record_value
type = dvo.resource_record_type
}
}
allow_overwrite = true
name = each.value.name
records = [each.value.record]
ttl = 60
type = each.value.type
zone_id = aws_route53_zone.example_domain.zone_id
}
resource "aws_acm_certificate_validation" "example-validation" {
certificate_arn = aws_acm_certificate.example_cert.arn
validation_record_fqdns = [for record in aws_route53_record.cert-cname : record.fqdn]
}
It takes a bit of time to get the certificate validated (there is an open issue about this).
Create a CloudFront Distribution
You might be wondering what CloudFront is and why we need it in this case. So here's a quick summary before we continue with our setup:
CloudFront is a content delivery network service offered by AWS. It is beneficial for many purposes, such as distributing your content to edgel locations closer to your users, offering better performance, protecting against Network and Application Layer Attacks, and edge computing.
Why do we need it?
According to the documentation, Amazon S3 website endpoints do not support HTTPS. If you want to use HTTPS, you can use Amazon CloudFront to serve a static website hosted on Amazon S3.
Another thing around using ACM certificate with CloudFront is that the certificate has to be in the US East (N. Virgina) Region (us-east-1). Add the following snippet to the
acm.tf
file createdprovider = aws.virgina
underaws_acm_certificate
andaws_acm_certificate_validation
blocks, and add this provider block at the top of the file:provider "aws" { alias = "virgina" region = "us-east-1" }
Let's build it:
A cloud front distribution is where you define how your content will be distributed. So we will start with it:
resource "aws_cloudfront_distribution" "example_distribution" {
}
The following configuration exists inside the distribution block:
- Add the origin to that distribution; that's where CloudFront will be retrieving the content from (S3 bucket in our case):
origin {
domain_name = aws_s3_bucket.example.bucket_regional_domain_name
origin_id = local.s3_origin_id
}
- Mark the distribution as enabled and define the default root object (that's index.html):
enabled = true
default_root_object = "index.html"
- Add your custom domain as an alternate domain in CloudFront
aliases = [var.domainName]
- Define the default cache behaviour for this distribution. This defines which HTTP methods CloudFront forwards to the S3 bucket, which responses to cache depending on the request HTTP method, and the forwarded values that specify how CloudFront handles query strings, cookies and headers. :
default_cache_behavior {
allowed_methods = ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"]
cached_methods = ["GET", "HEAD"]
target_origin_id = local.s3_origin_id
forwarded_values {
query_string = false
cookies {
forward = "none"
}
}
viewer_protocol_policy = "allow-all"
min_ttl = 0
default_ttl = 3600
max_ttl = 86400
}
- Define any restrictions (none in our case) - this is usually used to restrict/enable content distribution by country:
restrictions {
geo_restriction {
restriction_type = "none"
}
}
-
Set up the viewer certificate; that's the arn of the certificate we created earlier, and that's how we enable viewers to use HTTPS
- There are two ways to serve HTTPS requests. The first one is using Server Name Indication (SNI), and the second is to dedicate an IP address in each edge location that would serve our content (the second method is expensive)
viewer_certificate {
acm_certificate_arn = aws_acm_certificate.cert.arn
ssl_support_method = "sni-only"
}
By this, we have finished the setup for our CloudFront distribution.
Route 53 updates
The only thing left is to have our A record in route 53 points to the CloudFront distribution rather than the S3 bucket directly. That's changing the alias inside of the aws_route53_record
:
resource "aws_route53_record" "exampleDomain-a" {
zone_id = aws_route53_zone.exampleDomain.zone_id
name = var.domainName
type = "A"
alias {
name = aws_cloudfront_distribution.example_distribution.domain_name
zone_id = aws_cloudfront_distribution.example_distribution.hosted_zone_id
evaluate_target_health = true
}
}
Run terraform apply
to deploy your change.
I hope this was helpful. I'd be keen to hear your thoughts. Do you have a different method to secure your site?
Top comments (1)
I know this article is quite old but it helps me so much! Thank you!
In your cloudfront configuration, you should replace
aws_acm_certificate.cert.arn
byaws_acm_certificate_validation.example-validation.certificate_arn
or it will try to create the cloudfront before the certificate is validated and it will fail