Skip to content

Commit 080bb4d

Browse files
committed
Add CloudFront distribution with S3 origin for enrollment checker
1 parent 9ae8624 commit 080bb4d

2 files changed

Lines changed: 140 additions & 6 deletions

File tree

tofu/modules/sebt_enrollment_checker/main.tf

Lines changed: 125 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,9 @@ resource "aws_kms_key" "site" {
5252
deletion_window_in_days = 7
5353
enable_key_rotation = true
5454
policy = jsonencode(yamldecode(templatefile("${path.module}/templates/bucket-key-policy.yaml.tftpl", {
55-
account_id = data.aws_caller_identity.current.account_id
56-
partition = data.aws_partition.current.partition
57-
# CloudFront distribution ARN is needed here but the distribution is
58-
# created in a later step. We use a wildcard for now and will tighten
59-
# this once the distribution resource exists.
60-
distribution_arn = "*"
55+
account_id = data.aws_caller_identity.current.account_id
56+
partition = data.aws_partition.current.partition
57+
distribution_arn = aws_cloudfront_distribution.site.arn
6158
})))
6259

6360
tags = {
@@ -159,3 +156,125 @@ resource "aws_acm_certificate_validation" "site" {
159156
certificate_arn = aws_acm_certificate.site.arn
160157
validation_record_fqdns = [for record in aws_route53_record.certificate_validation : record.fqdn]
161158
}
159+
160+
# ---------------------------------------------------------------------------
161+
# CloudFront distribution
162+
# ---------------------------------------------------------------------------
163+
164+
# Origin Access Control (OAC) lets CloudFront authenticate to S3 using
165+
# AWS SigV4 request signing. This replaces the older Origin Access Identity
166+
# (OAI) approach. With OAC, CloudFront signs every request to S3, and the
167+
# bucket policy checks the signature — so the bucket never needs to be public.
168+
resource "aws_cloudfront_origin_access_control" "site" {
169+
name = "${var.project}-${var.state}-${var.environment}-enrollment-checker"
170+
origin_access_control_origin_type = "s3"
171+
signing_behavior = "always"
172+
signing_protocol = "sigv4"
173+
}
174+
175+
# The CloudFront distribution serves the static site from S3 over HTTPS.
176+
# It acts as a CDN — caching files at edge locations close to users for
177+
# faster delivery — and handles TLS termination using our ACM certificate.
178+
resource "aws_cloudfront_distribution" "site" {
179+
aliases = [var.domain]
180+
default_root_object = "index.html"
181+
enabled = true
182+
price_class = "PriceClass_100"
183+
184+
# S3 origin: CloudFront fetches files from our private bucket using OAC.
185+
origin {
186+
domain_name = aws_s3_bucket.site.bucket_regional_domain_name
187+
origin_id = "s3"
188+
origin_access_control_id = aws_cloudfront_origin_access_control.site.id
189+
}
190+
191+
# Default cache behavior: serve static files from S3.
192+
# GET and HEAD only — the static site has no server-side mutations.
193+
default_cache_behavior {
194+
allowed_methods = ["GET", "HEAD"]
195+
cached_methods = ["GET", "HEAD"]
196+
compress = true
197+
target_origin_id = "s3"
198+
viewer_protocol_policy = "redirect-to-https"
199+
200+
forwarded_values {
201+
query_string = false
202+
203+
cookies {
204+
forward = "none"
205+
}
206+
}
207+
}
208+
209+
# Handle client-side routing: when S3 returns a 404 (e.g. user navigates
210+
# to /check directly), serve index.html instead so the Next.js client
211+
# router can handle the path.
212+
custom_error_response {
213+
error_code = 403
214+
response_code = 200
215+
response_page_path = "/index.html"
216+
error_caching_min_ttl = 10
217+
}
218+
219+
custom_error_response {
220+
error_code = 404
221+
response_code = 200
222+
response_page_path = "/index.html"
223+
error_caching_min_ttl = 10
224+
}
225+
226+
# Send CloudFront access logs to the shared logging bucket.
227+
logging_config {
228+
bucket = var.logging_bucket_domain_name
229+
include_cookies = false
230+
prefix = "cloudfront/enrollment-checker/"
231+
}
232+
233+
restrictions {
234+
geo_restriction {
235+
restriction_type = "none"
236+
}
237+
}
238+
239+
# Use our ACM certificate for HTTPS. TLS 1.2 is the minimum — older
240+
# protocols have known vulnerabilities. SNI (Server Name Indication)
241+
# is the standard approach and avoids the cost of a dedicated IP.
242+
viewer_certificate {
243+
acm_certificate_arn = aws_acm_certificate_validation.site.certificate_arn
244+
minimum_protocol_version = "TLSv1.2_2021"
245+
ssl_support_method = "sni-only"
246+
}
247+
248+
tags = {
249+
service = "enrollment-checker"
250+
}
251+
}
252+
253+
# Bucket policy: deny non-SSL requests and allow only this CloudFront
254+
# distribution to read objects. Applied after the distribution is created
255+
# so we can reference its ARN.
256+
resource "aws_s3_bucket_policy" "site" {
257+
bucket = aws_s3_bucket.site.id
258+
policy = jsonencode(yamldecode(templatefile("${path.module}/templates/bucket-policy.yaml.tftpl", {
259+
bucket_arn = aws_s3_bucket.site.arn
260+
distribution_arn = aws_cloudfront_distribution.site.arn
261+
})))
262+
263+
depends_on = [aws_s3_bucket_public_access_block.site]
264+
}
265+
266+
# Route53 A record pointing the enrollment checker domain at CloudFront.
267+
# This is an "alias" record — a Route53-specific feature that maps a
268+
# domain directly to an AWS resource without a CNAME. It works at the
269+
# zone apex and has no TTL (queries resolve instantly via Route53).
270+
resource "aws_route53_record" "site" {
271+
name = var.domain
272+
type = "A"
273+
zone_id = var.hosted_zone_id
274+
275+
alias {
276+
evaluate_target_health = false
277+
name = aws_cloudfront_distribution.site.domain_name
278+
zone_id = aws_cloudfront_distribution.site.hosted_zone_id
279+
}
280+
}

tofu/modules/sebt_enrollment_checker/outputs.tf

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,18 @@ output "s3_bucket_regional_domain_name" {
1212
description = "Regional domain name of the S3 bucket (used as CloudFront origin)."
1313
value = aws_s3_bucket.site.bucket_regional_domain_name
1414
}
15+
16+
output "cloudfront_distribution_id" {
17+
description = "ID of the CloudFront distribution (used for cache invalidation in CI/CD)."
18+
value = aws_cloudfront_distribution.site.id
19+
}
20+
21+
output "cloudfront_distribution_domain" {
22+
description = "Domain name of the CloudFront distribution."
23+
value = aws_cloudfront_distribution.site.domain_name
24+
}
25+
26+
output "site_url" {
27+
description = "Public URL of the enrollment checker."
28+
value = "https://${var.domain}"
29+
}

0 commit comments

Comments
 (0)