Using CloudFront with Origin Access Identity and S3
There are many ways to host a static site on S3, either directly on S3 using S3 Website or by setting up CloudFront to serve content from S3. If you want to use your own domain and TLS you more or less have to use CloudFront. However - many roads lead to Rome, and some are both quicker and more secure.
You can configure a S3 bucket to serve static content over HTTP, even using your own domain. But there's a number of limitations with this setup, as detalied in the list below. Below is how you'd typically set things up in S3. Notice that ACL is set to private, this is intentional as this feature is deprecated and will throw all kinds of alarms if you've configured your AWS accounts properly. Instead, use bucket policies.
resource "aws_s3_bucket" "this" {
# The bucket name must be the fqdn it responds to
bucket = "foobar.netwerk.io"
acl = "private"
website {
index_document = "index.html"
}
}
# Policy to grant access objects in bucket
data "aws_iam_policy_document" "this" {
statement {
actions = ["s3:GetObject"]
resources = ["${aws_s3_bucket.this.arn}/*"]
principals = {
type = "AWS"
identifiers = ["*"]
}
}
}
resource "aws_s3_bucket_policy" "this" {
bucket = "${aws_s3_bucket.this.id}"
policy = "${data.aws_iam_policy_document.this.json}"
}
resource "aws_route53_record" "this" {
zone_id = "${aws_route53_zone.this.zone_id}"
name = "parked"
type = "A"
alias {
name = "${aws_s3_bucket.this.website_domain}"
zone_id = "${aws_s3_bucket.this.hosted_zone_id}"
evaluate_target_health = true
}
}
This way isn't ideal, especially not if you want or need to run TLS.
- The bucket website endpoint exists only in one region, the primary region for that bucket
- Little to no caching abilities, except for some client-side caching
- Limited access policies, you're stuck to conditions on bucket policies
- Not built for performance
- Bucket name has to correspond to the name of your domain
The CloudFront-way
Now - you could just put CloudFront in front of a S3 Website and consider yourself done, but you don't want that, at least not when you can use Origin Access Identity instead. If you set CloudFront to use a S3 Website as backend then all backend traffic goes unencrypted from edge servers to S3. If you have some kind of policy attached to CloudFront users can just bypass it all by connecting to the backend bucket directly. Remember, they know exactly what name your bucket has.
To mitigate all of this Amazon has created Origin Access Identity. This lets CloudFront use S3 APIs to request the objects, and all requests are authenticated and encrypted from edge servers to backend buckets. Now your backend bucket can be any bucket, or sub-directory in any bucket, and CloudFront will be able to access the content, as long as you've configured an appropriate access policy.
There is one caveat, like it always is. Since CloudFront uses the S3 API to access objects they are treated like just that, objects. This means that it has no concept of sub-directories. Normally when you access a URI like /foobar/ the webserver knows that you're likely looking for /foobar/index.html and will serve that up directly. But in this case the S3 API will just return that the object doesn't exist. Luckily we can fix this with a simple Lambda@Edge.
resource "aws_s3_bucket" "this" {
bucket = "${var.bucket}"
acl = "private"
}
resource "aws_cloudfront_origin_access_identity" "this" {}
data "aws_iam_policy_document" "this" {
statement {
actions = ["s3:GetObject"]
resources = ["${aws_s3_bucket.this.arn}/*"]
principals {
type = "AWS"
identifiers = ["${aws_cloudfront_origin_access_identity.this.iam_arn}"]
}
}
statement {
actions = ["s3:ListBucket"]
resources = ["${aws_s3_bucket.this.arn}"]
principals {
type = "AWS"
identifiers = ["${aws_cloudfront_origin_access_identity.this.iam_arn}"]
}
}
}
resource "aws_s3_bucket_policy" "this" {
bucket = "${aws_s3_bucket.this.id}"
policy = "${data.aws_iam_policy_document.this.json}"
}
resource "aws_cloudfront_distribution" "this" {
enabled = "${var.enabled}"
is_ipv6_enabled = "${var.is_ipv6_enabled}"
price_class = "${var.price_class}"
default_root_object = "${var.default_root_object}"
aliases = ["${local.aliases}"]
origin {
domain_name = "${aws_s3_bucket.this.bucket_regional_domain_name}"
origin_id = "${var.domain_name}"
s3_origin_config {
origin_access_identity = "${aws_cloudfront_origin_access_identity.this.cloudfront_access_identity_path}"
}
}
viewer_certificate {
acm_certificate_arn = "${aws_acm_certificate.this.arn}"
ssl_support_method = "${var.ssl_support_method}"
minimum_protocol_version = "${var.minimum_protocol_version}"
}
default_cache_behavior {
allowed_methods = "${var.allowed_methods}"
cached_methods = "${var.cached_methods}"
target_origin_id = "${var.domain_name}"
compress = "${var.compress}"
forwarded_values {
query_string = "${var.forwarded_query_string}"
cookies {
forward = "${var.forwarded_cookies}"
}
}
viewer_protocol_policy = "${var.viewer_protocol_policy}"
default_ttl = "${var.default_ttl}"
min_ttl = "${var.min_ttl}"
max_ttl = "${var.max_ttl}"
}
restrictions {
geo_restriction {
locations = "${var.locations}"
restriction_type = "${var.restriction_type}"
}
}
}