# Form-hosting and reports buckets. The Lambda artifact bucket lives in # artifacts.tf. # --------------------------------------------------------------------------- # Form bucket — private origin for the CloudFront distribution # --------------------------------------------------------------------------- resource "aws_s3_bucket" "form" { bucket = local.form_bucket_name tags = { Purpose = "meal-order-form-hosting" } lifecycle { prevent_destroy = true } } resource "aws_s3_bucket_public_access_block" "form" { bucket = aws_s3_bucket.form.id block_public_acls = true block_public_policy = true ignore_public_acls = true restrict_public_buckets = true } resource "aws_s3_bucket_ownership_controls" "form" { bucket = aws_s3_bucket.form.id rule { object_ownership = "BucketOwnerEnforced" } } resource "aws_s3_bucket_server_side_encryption_configuration" "form" { bucket = aws_s3_bucket.form.id rule { apply_server_side_encryption_by_default { sse_algorithm = "AES256" } } } resource "aws_s3_bucket_lifecycle_configuration" "form" { bucket = aws_s3_bucket.form.id rule { id = "delete-old-archives" status = "Enabled" filter { prefix = "archive/" } expiration { days = 90 } } } data "aws_iam_policy_document" "form" { statement { sid = "DenyInsecureTransport" effect = "Deny" principals { type = "*" identifiers = ["*"] } actions = ["s3:*"] resources = [ aws_s3_bucket.form.arn, "${aws_s3_bucket.form.arn}/*", ] condition { test = "Bool" variable = "aws:SecureTransport" values = ["false"] } } statement { sid = "AllowCloudFrontOAC" effect = "Allow" principals { type = "Service" identifiers = ["cloudfront.amazonaws.com"] } actions = ["s3:GetObject"] resources = ["${aws_s3_bucket.form.arn}/*"] condition { test = "StringEquals" variable = "AWS:SourceArn" values = [aws_cloudfront_distribution.form.arn] } } } resource "aws_s3_bucket_policy" "form" { bucket = aws_s3_bucket.form.id policy = data.aws_iam_policy_document.form.json depends_on = [aws_s3_bucket_public_access_block.form] } # --------------------------------------------------------------------------- # Reports bucket — payroll CSVs and weekly summary PDFs # --------------------------------------------------------------------------- resource "aws_s3_bucket" "reports" { bucket = local.reports_bucket_name tags = { Purpose = "meal-order-reports" } lifecycle { prevent_destroy = true } } resource "aws_s3_bucket_public_access_block" "reports" { bucket = aws_s3_bucket.reports.id block_public_acls = true block_public_policy = true ignore_public_acls = true restrict_public_buckets = true } resource "aws_s3_bucket_ownership_controls" "reports" { bucket = aws_s3_bucket.reports.id rule { object_ownership = "BucketOwnerEnforced" } } resource "aws_s3_bucket_server_side_encryption_configuration" "reports" { bucket = aws_s3_bucket.reports.id rule { apply_server_side_encryption_by_default { sse_algorithm = "AES256" } } } resource "aws_s3_bucket_lifecycle_configuration" "reports" { bucket = aws_s3_bucket.reports.id # Whole-bucket rule, matching the SAM template's unprefixed rule. rule { id = "archive-old-reports" status = "Enabled" filter { prefix = "" } transition { days = 90 storage_class = "GLACIER_IR" } } } # The SAM template attached no policy to this bucket. The TLS-only deny is a # deliberate addition; it grants nothing and blocks plaintext access to payroll # data. data "aws_iam_policy_document" "reports" { statement { sid = "DenyInsecureTransport" effect = "Deny" principals { type = "*" identifiers = ["*"] } actions = ["s3:*"] resources = [ aws_s3_bucket.reports.arn, "${aws_s3_bucket.reports.arn}/*", ] condition { test = "Bool" variable = "aws:SecureTransport" values = ["false"] } } } resource "aws_s3_bucket_policy" "reports" { bucket = aws_s3_bucket.reports.id policy = data.aws_iam_policy_document.reports.json depends_on = [aws_s3_bucket_public_access_block.reports] }