DEV Community

Ali Haydar for AWS Community Builders

Posted on

How to secure your site on AWS with CloudFront and ACM?

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.

Image description

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
  }
}

Enter fullscreen mode Exit fullscreen mode

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]
}

Enter fullscreen mode Exit fullscreen mode

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 created provider = aws.virgina under aws_acm_certificate and aws_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" {

}
Enter fullscreen mode Exit fullscreen mode

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
    }
Enter fullscreen mode Exit fullscreen mode
  • Mark the distribution as enabled and define the default root object (that's index.html):
  enabled             = true
  default_root_object = "index.html"
Enter fullscreen mode Exit fullscreen mode
  • Add your custom domain as an alternate domain in CloudFront
    aliases = [var.domainName]
Enter fullscreen mode Exit fullscreen mode
  • 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
    }
Enter fullscreen mode Exit fullscreen mode
  • Define any restrictions (none in our case) - this is usually used to restrict/enable content distribution by country:
  restrictions {
    geo_restriction {
      restriction_type = "none"
    }
  }
Enter fullscreen mode Exit fullscreen mode
  • 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"
  }
Enter fullscreen mode Exit fullscreen mode

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
  }
}
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
dev-alexleboucher profile image
Alex Le Boucher • Edited

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 by aws_acm_certificate_validation.example-validation.certificate_arn or it will try to create the cloudfront before the certificate is validated and it will fail