From 122abecdcf021ee928b64dcbaa1f9b36e8bc6360 Mon Sep 17 00:00:00 2001 From: Isaac T Date: Mon, 10 Nov 2025 21:45:54 -0500 Subject: [PATCH 01/15] feat: enable SNS to trigger email Lambda function - Created IAM role and attached least privilege policies for Lambda (logs, SES, secrets) - Created SNS topic 'user-signup-topic' and subscribed Lambda as subscriber - Added Lambda permission allowing SNS to invoke the function --- iam_role.tf | 98 +++++++++++++++++++++++++++++++++++++++++++++++++++++ lambda.tf | 7 ++++ sns.tf | 13 +++++++ 3 files changed, 118 insertions(+) create mode 100644 lambda.tf create mode 100644 sns.tf diff --git a/iam_role.tf b/iam_role.tf index 41d7f14..68ae760 100644 --- a/iam_role.tf +++ b/iam_role.tf @@ -11,6 +11,7 @@ resource "aws_iam_role" "app_ec2_role" { }] }) } + # Instance Profile is a container for the IAM Role. # EC2 cannot directly attach an IAM Role — it must attach an Instance Profile instead. # The profile allows EC2 to assume the role and get temporary credentials automatically. @@ -100,3 +101,100 @@ resource "aws_iam_role_policy_attachment" "ssm_core" { role = aws_iam_role.app_ec2_role.name policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore" } + +# ------------------------ +# EC2 publish to SNS Topic +# ------------------------ + +# policy for EC2 can publish msg to subscribed SND Topic +resource "aws_iam_role_policy" "app_publish_sns" { + name = "app-publish-sns" + role = aws_iam_role.app_ec2_role.name + + policy = jsonencode({ + Version = "2012-10-17", + Statement = [{ + Effect = "Allow", + Action = ["sns:Publish"], + Resource = aws_sns_topic.user_signup.arn + }] + }) +} + +# ------------------- +# Email Lambda Role +# ------------------- + +# Assume role +resource "aws_iam_role" "lambda_email_role" { + name = "lambda-email-sender-role" + assume_role_policy = jsonencode({ + Version = "2012-10-17", + Statement = [{ + Effect = "Allow", + Principal = { Service = "lambda.amazonaws.com" }, + Action = "sts:AssumeRole" + }] + }) +} + +# Logs +resource "aws_iam_role_policy" "lambda_logs" { + name = "lambda-basic-logs" + role = aws_iam_role.lambda_email_role.id + + # Lambda will automatically create a log group in CloudWatch e.g. /aws/lambda/ + policy = jsonencode({ + Version = "2012-10-17", + Statement = [{ + Effect = "Allow", + Action = ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"], + Resource = "arn:aws:logs:${var.region}:${data.aws_caller_identity.me.account_id}:log-group:/aws/lambda/*" + }] + }) +} + +# Secrets +resource "aws_iam_role_policy" "lambda_secrets_read" { + name = "lambda-secrets-read" + role = aws_iam_role.lambda_email_role.id + + policy = jsonencode({ + Version : "2012-10-17", + Statement = [{ + Effect = "Allow", + Action = ["secretsmanager:GetSecretValue"], + Resource = aws_secretsmanager_secret.email_credentials.arn + }] + }) +} + +# KMS decrypt (for that secret's CMK) +resource "aws_iam_role_policy" "lambda_kms_decrypt" { + name = "lambda-kms-decrypt" + role = aws_iam_role.lambda_email_role.id + + policy = jsonencode({ + Version : "2012-10-17", + Statement = [{ + Effect = "Allow", + Action = ["kms:Decrypt"], + Resource = aws_kms_key.secrets_key.arn + }] + }) +} + +# SES send +resource "aws_iam_role_policy" "lambda_ses_send" { + name = "lambda-ses-send" + role = aws_iam_role.lambda_email_role.id + + policy = jsonencode({ + Version : "2012-10-17", + Statement = [{ + Effect = "Allow", + Action = ["ses:SendEmail", "ses:SendRawEmail"], + Resource = "*" + }] + }) +} diff --git a/lambda.tf b/lambda.tf new file mode 100644 index 0000000..23def61 --- /dev/null +++ b/lambda.tf @@ -0,0 +1,7 @@ +resource "aws_lambda_permission" "allow_sns_invoke" { + statement_id = "AllowExecutionFromSNS" + action = "lambda:InvokeFunction" + function_name = aws_lambda_function.email_sender.function_name + principal = "sns.amazon.com" + source_arn = aws_sns_topic.user_signup.arn +} diff --git a/sns.tf b/sns.tf new file mode 100644 index 0000000..dc8404d --- /dev/null +++ b/sns.tf @@ -0,0 +1,13 @@ +resource "aws_sns_topic" "user_signup" { + name = "user-signup-topic" +} + +# The Lambda function subscribes to this SNS topic. +# 'topic_arn' specifies **which SNS topic** the subscription belongs to. +# 'endpoint' specifies **the subscriber** — in this case, the Lambda function ARN +# that will receive (be invoked with) every message published to the topic. +resource "aws_sns_topic_subscription" "lambda_sub" { + topic_arn = aws_ans_topic.user_signup.arn + protocol = "lambda" + endpoint = aws_lambda_function.email_sender.arn +} From 351c6fd8d96f1aef9ff675a2132c9c6a455e73f8 Mon Sep 17 00:00:00 2001 From: Isaac T Date: Tue, 11 Nov 2025 21:55:16 -0500 Subject: [PATCH 02/15] feat(lambda): deploy SES email sender - data source to zip lambda source from ../serverless - created lambda func using Node.js 20 runtime - config env vars (FROM_EMAIL, VERIFY_URL_BASE) - Included source_code_hash for automatic function update when zip content changes --- lambda.tf | 30 ++++++++++++++++++++++++++++++ security-group.tf | 18 ++++++++++++++++++ variables.tf | 12 ++++++++++++ versions.tf | 6 ++++++ 4 files changed, 66 insertions(+) diff --git a/lambda.tf b/lambda.tf index 23def61..774557a 100644 --- a/lambda.tf +++ b/lambda.tf @@ -5,3 +5,33 @@ resource "aws_lambda_permission" "allow_sns_invoke" { principal = "sns.amazon.com" source_arn = aws_sns_topic.user_signup.arn } + +# package lambda directory +data "archive_file" "lambda_zip" { + type = "zip" + source_dir = "${path.module}/../serverless" + output_path = "{path.module}/dist/email_sender.zip" + excludes = ["node_modules", ".git", "dist"] +} + +resource "aws_lambda_function" "email_sender" { + function_name = "email_sender" + runtime = "nodejs20.x" + handler = "ses_email.handler" # File name + exported function name + role = aws_iam_role.lambda_email_role.arn # IAM role with SES + CloudWatch permissions + + # Use the zip generated by the archive_file data source + filename = data.archive_file.lambda_zip.output_path + + # Ensures Lambda is automatically updated when the ZIP content changes. + # Terraform compares this hash to detect code updates. + source_code_hash = data.archive_file.lambda_zip.output_base64sha256 + + # Environment variables available in process.env + environment { + variables = { + FROM_EMAIL = var.verifiedSenderEmail # Verified sender email in SES + VERIFY_URL_BASE = var.verificationEndPoint # Your web app verification endpoint + } + } +} diff --git a/security-group.tf b/security-group.tf index 486102a..69fe1cd 100644 --- a/security-group.tf +++ b/security-group.tf @@ -26,6 +26,24 @@ resource "aws_vpc_security_group_egress_rule" "lb_all_out" { ip_protocol = "-1" } +resource "aws_vpc_security_group_ingress_rule" "lb_ingress_ipv6" { + for_each = toset([for p in local.lb_ingress_ports : tostring(p)]) + security_group_id = aws_security_group.lb_sg.id + cidr_ipv6 = "::/0" + from_port = tonumber(each.value) + to_port = tonumber(each.value) + ip_protocol = "tcp" + description = "Allow TCP ${each.value} from anywhere (IPv6)" +} + +resource "aws_vpc_security_group_egress_rule" "lb_all_out_ipv4" { + security_group_id = aws_security_group.lb_sg.id + cidr_ipv4 = "0.0.0.0/0" + ip_protocol = "-1" + description = "Allow all outbound traffic (IPv4)" +} + + #---------------------- # Web App SG #---------------------- diff --git a/variables.tf b/variables.tf index 65d45c9..3aeb6a4 100644 --- a/variables.tf +++ b/variables.tf @@ -176,6 +176,18 @@ variable "template_name" { default = "csye6225_asg" } +# Verified sender email address used by SES +variable "verifiedSenderEmail" { + description = "The SES verified email address used as the 'From' field for outgoing messages" + type = string +} + +# Base URL for the email verification link +variable "verificationEndPoint" { + description = "The base URL for the /validateEmail endpoint used in verification emails" + type = string +} + variable "tags" { description = "Common tags applied to all resources" type = map(string) diff --git a/versions.tf b/versions.tf index 51b7f8d..b16b8e5 100644 --- a/versions.tf +++ b/versions.tf @@ -4,5 +4,11 @@ terraform { source = "hashicorp/aws" version = ">= 6.0.0, <7.0.0" } + archive = { + source = "hashicorp/archive" + version = "~> 2.4" + } } + + required_version = ">= 1.5.0" } From 39d0f18e19f2783120e23bc0bf69fc8e04f4050f Mon Sep 17 00:00:00 2001 From: Isaac T Date: Tue, 11 Nov 2025 22:42:31 -0500 Subject: [PATCH 03/15] feat(lambda): add DynamoDB deduplication to prevent duplicate email - Created DynamoDB table 'SentEmails' with messageId as primary key - Added IAM policy to grant Lambda dynamodb:GetItem and dynamodb:PutItem permissions - Inserted messageId after successful delivery to avoid duplicates --- dynamodb.tf | 11 +++++++++++ iam_role.tf | 18 ++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 dynamodb.tf diff --git a/dynamodb.tf b/dynamodb.tf new file mode 100644 index 0000000..c0fccd6 --- /dev/null +++ b/dynamodb.tf @@ -0,0 +1,11 @@ +resource "aws_dynamodb_table" "sent_emails" { + name = "SentEmails" + billing_mode = "PAY_PER_REQUEST" + hash_key = "messageId" + + # defines which attributes exist and their types. + attribute { + name = "messageId" + type = "S" + } +} diff --git a/iam_role.tf b/iam_role.tf index 68ae760..6e04779 100644 --- a/iam_role.tf +++ b/iam_role.tf @@ -198,3 +198,21 @@ resource "aws_iam_role_policy" "lambda_ses_send" { }] }) } + +# Allow lambda to r/w items in the DynamoDB table +# to perform deduplication +resource "aws_iam_role_policy" "lambda_dedup_policy" { + name = "lambda-dedup-policy" + role = aws_iam_role.lambda_email_role.id + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow", + Action = ["dynamodb:PutItem", "dynamodb:GetItem"] + Resource = aws_dynamodb_table.sent_emails.arn + } + ] + }) +} From b74509e418ff74fa4774ee92807bca5864c43e6c Mon Sep 17 00:00:00 2001 From: Isaac T Date: Thu, 13 Nov 2025 07:21:42 -0500 Subject: [PATCH 04/15] feat(env): add new env vars via user data for SNS --- ec2.tf | 4 ++++ scripts/user_data.sh | 2 ++ variables.tf | 5 +++++ 3 files changed, 11 insertions(+) diff --git a/ec2.tf b/ec2.tf index d01172a..a6d141b 100644 --- a/ec2.tf +++ b/ec2.tf @@ -57,6 +57,8 @@ resource "aws_instance" "app" { aws_region = var.region s3_bucket = aws_s3_bucket.images.bucket + account_id = var.account_id + sns_topic_name = aws_sns_topic.user_signup.name }) tags = { @@ -100,6 +102,8 @@ resource "aws_launch_template" "app" { aws_region = var.region s3_bucket = aws_s3_bucket.images.bucket + account_id = var.account_id + sns_topic_name = aws_sns_topic.user_signup.name })) tag_specifications { diff --git a/scripts/user_data.sh b/scripts/user_data.sh index 4727a6d..d0c6aa8 100644 --- a/scripts/user_data.sh +++ b/scripts/user_data.sh @@ -22,6 +22,8 @@ DB_USERNAME=${db_username} DB_PASSWORD=${db_password} AWS_REGION=${aws_region} S3_BUCKET=${s3_bucket} +ACCOUNT_ID=${account_id} +SNS_TOPIC_NAME=${sns_topic_name} EOF chown "${app_user}:${app_group}" "$ENV_FILE" # Change owner of the .env file to normal user diff --git a/variables.tf b/variables.tf index 3aeb6a4..a9f6ac5 100644 --- a/variables.tf +++ b/variables.tf @@ -188,6 +188,11 @@ variable "verificationEndPoint" { type = string } +variable "account_id" { + description = "SNS resource location" + type = string +} + variable "tags" { description = "Common tags applied to all resources" type = map(string) From 5ae0cac3ef66989efbad7a19643d8c33efcf613c Mon Sep 17 00:00:00 2001 From: Isaac T Date: Thu, 13 Nov 2025 07:27:53 -0500 Subject: [PATCH 05/15] chore: terraform format --- dynamodb.tf | 4 ++-- ec2.tf | 12 ++++++------ iam_role.tf | 4 ++-- sns.tf | 2 +- variables.tf | 2 +- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/dynamodb.tf b/dynamodb.tf index c0fccd6..2b4cb22 100644 --- a/dynamodb.tf +++ b/dynamodb.tf @@ -1,7 +1,7 @@ resource "aws_dynamodb_table" "sent_emails" { - name = "SentEmails" + name = "SentEmails" billing_mode = "PAY_PER_REQUEST" - hash_key = "messageId" + hash_key = "messageId" # defines which attributes exist and their types. attribute { diff --git a/ec2.tf b/ec2.tf index a6d141b..9e5e3be 100644 --- a/ec2.tf +++ b/ec2.tf @@ -55,9 +55,9 @@ resource "aws_instance" "app" { db_username = var.db_username db_password = random_password.rds.result - aws_region = var.region - s3_bucket = aws_s3_bucket.images.bucket - account_id = var.account_id + aws_region = var.region + s3_bucket = aws_s3_bucket.images.bucket + account_id = var.account_id sns_topic_name = aws_sns_topic.user_signup.name }) @@ -100,9 +100,9 @@ resource "aws_launch_template" "app" { db_username = var.db_username db_password = random_password.rds.result - aws_region = var.region - s3_bucket = aws_s3_bucket.images.bucket - account_id = var.account_id + aws_region = var.region + s3_bucket = aws_s3_bucket.images.bucket + account_id = var.account_id sns_topic_name = aws_sns_topic.user_signup.name })) diff --git a/iam_role.tf b/iam_role.tf index 6e04779..2ee28b7 100644 --- a/iam_role.tf +++ b/iam_role.tf @@ -209,8 +209,8 @@ resource "aws_iam_role_policy" "lambda_dedup_policy" { Version = "2012-10-17" Statement = [ { - Effect = "Allow", - Action = ["dynamodb:PutItem", "dynamodb:GetItem"] + Effect = "Allow", + Action = ["dynamodb:PutItem", "dynamodb:GetItem"] Resource = aws_dynamodb_table.sent_emails.arn } ] diff --git a/sns.tf b/sns.tf index dc8404d..27c752a 100644 --- a/sns.tf +++ b/sns.tf @@ -7,7 +7,7 @@ resource "aws_sns_topic" "user_signup" { # 'endpoint' specifies **the subscriber** — in this case, the Lambda function ARN # that will receive (be invoked with) every message published to the topic. resource "aws_sns_topic_subscription" "lambda_sub" { - topic_arn = aws_ans_topic.user_signup.arn + topic_arn = aws_sns_topic.user_signup.arn protocol = "lambda" endpoint = aws_lambda_function.email_sender.arn } diff --git a/variables.tf b/variables.tf index a9f6ac5..36bce5e 100644 --- a/variables.tf +++ b/variables.tf @@ -190,7 +190,7 @@ variable "verificationEndPoint" { variable "account_id" { description = "SNS resource location" - type = string + type = string } variable "tags" { From 7346df326b71c4307f1fa81aeec5146654cca13b Mon Sep 17 00:00:00 2001 From: Isaac T Date: Thu, 13 Nov 2025 07:30:11 -0500 Subject: [PATCH 06/15] chore(terraform): refresh provider versions - using `terraform init -upgrade` cmd - Updated .terraform.lock.hcl to latest compatible provider versions - Ensures consistent provider versions across environments --- .terraform.lock.hcl | 52 +++++++++++++++++++++++++++++++-------------- 1 file changed, 36 insertions(+), 16 deletions(-) diff --git a/.terraform.lock.hcl b/.terraform.lock.hcl index dac85a6..0447c33 100644 --- a/.terraform.lock.hcl +++ b/.terraform.lock.hcl @@ -1,26 +1,46 @@ # This file is maintained automatically by "terraform init". # Manual edits may be lost in future updates. +provider "registry.terraform.io/hashicorp/archive" { + version = "2.7.1" + constraints = "~> 2.4" + hashes = [ + "h1:A7EnRBVm4h9ryO9LwxYnKr4fy7ExPMwD5a1DsY7m1Y0=", + "zh:19881bb356a4a656a865f48aee70c0b8a03c35951b7799b6113883f67f196e8e", + "zh:2fcfbf6318dd514863268b09bbe19bfc958339c636bcbcc3664b45f2b8bf5cc6", + "zh:3323ab9a504ce0a115c28e64d0739369fe85151291a2ce480d51ccbb0c381ac5", + "zh:362674746fb3da3ab9bd4e70c75a3cdd9801a6cf258991102e2c46669cf68e19", + "zh:7140a46d748fdd12212161445c46bbbf30a3f4586c6ac97dd497f0c2565fe949", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:875e6ce78b10f73b1efc849bfcc7af3a28c83a52f878f503bb22776f71d79521", + "zh:b872c6ed24e38428d817ebfb214da69ea7eefc2c38e5a774db2ccd58e54d3a22", + "zh:cd6a44f731c1633ae5d37662af86e7b01ae4c96eb8b04144255824c3f350392d", + "zh:e0600f5e8da12710b0c52d6df0ba147a5486427c1a2cc78f31eea37a47ee1b07", + "zh:f21b2e2563bbb1e44e73557bcd6cdbc1ceb369d471049c40eb56cb84b6317a60", + "zh:f752829eba1cc04a479cf7ae7271526b402e206d5bcf1fcce9f535de5ff9e4e6", + ] +} + provider "registry.terraform.io/hashicorp/aws" { - version = "6.15.0" + version = "6.20.0" constraints = ">= 6.0.0, < 7.0.0" hashes = [ - "h1:nP988jNuJMcQIB3HLzVK7EfLk5mgWmoWgOsdZDZpIOs=", - "zh:05a3d3b268761cd90cabd6106bff2bf27f480ab31305cd8ef8c749060855f84d", - "zh:0edae750ebaee784624e41b1e18fe6179a513d63c5bb8fbffab4631391092b4f", - "zh:17f3d20951662ffd6a610d9c7f44afa281db6f220685796147e4ffb6374cc8b8", - "zh:373a5446fca3aeff76bc5637babd732d6c78d9a66c82a828a1b009e8b21f33bc", - "zh:3ce69866d23b7d0bb5bfa06f5407147ed90713924cd65246858c414313a96ffc", - "zh:40ab0ca19845890df706784bb62d9fc9961a15c23c894f0e9f89b66524c4be55", - "zh:66bd5554c582c1f01c1a509eedf4a81c861065b48a49d1be3e3ea98a89b1f801", - "zh:798b66f98cc8d8ff9c6844a8238d2639f951ef3956d412fb438708ba3e4ae9e3", - "zh:943e5f918d3b470fbfb9ea1c8bcc3b97a8218a0842e77a0fdbac0941dd461cdf", + "h1:x9OpnfcX0zYkPdJ38K5vSNe78QZReatBzk/pIlSDU1I=", + "zh:05173910d3031e6ba7b985188cff620bbe48cc6bb35ffc30238b9dcd9201c0d4", + "zh:0fd36a822e15c473593b5ca826ddfabe248a89833721960781aac65f05a55bc1", + "zh:13f478b843ef1fb52780e2771098ae42d6aaceab50d56bc31fdb02ddce6f4621", + "zh:16c5a17af7d0fc5329957bc48b5d10b987240076203453a108190132622d40bf", + "zh:290342af2da821383b8b5a897920cde2ae01f25768fe5e2df80b80bcc34421cb", + "zh:4cfe46a2ed6324467b896aa0dd7fbcf74a94a046d963c8a8fb865e817bcfb955", + "zh:78c243a5ec47f6d7066104191480fb13eb85995000cc7c80bc92383baea286a5", + "zh:88004fb58967b32061120d1d668e1af1f988da694994cddd11d2d4f7726f5054", + "zh:94184d7f00ec5e30d572d56979110e6cf6d5071583713b0e357cca72785e5365", "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", - "zh:9e95f017fae84d07d6cd627949715dbe8749d4d819c13c8b0bef1a679a26671b", - "zh:aac7e07599a17fccbdd21b092a7741534af5bec60b492299f2bcd3d7279be4a9", - "zh:c6292faaf05a6dc45e170f67f251aaad9b7e1159b5946219908dd11025f4146b", - "zh:df892b9eca5ecfb3c0a0e829511aea7e6b30f08b862c7fba9de67d2ae9729983", - "zh:fb8c5ff7296d01bf60d983c64f45969ec664a40bdd768d90a35a6afe7df1aeb7", + "zh:b219d9c012db507a761c3f3a9147573f25d81d524a076e7c13099f87a50ae386", + "zh:beee1b49f414d1d8c03c9c4264725e288d77a676c093e3017de7d10a8ac45582", + "zh:e2e76be18ff4c2bf7cfcea9898475ec320b5574c01181581d76d047a2ee7be5c", + "zh:fa9afad49b0b2586032afcda2a5813da6e4ba9e56a8a25167b8f659b68e3983d", + "zh:fd284feb9f95a20ba848a9027f3c4298d75be6e882df40f075d60955f7bcf170", ] } From a63ee8ca2dca2de11ca1a2843e34202036563392 Mon Sep 17 00:00:00 2001 From: Isaac T Date: Thu, 13 Nov 2025 10:48:59 -0500 Subject: [PATCH 07/15] feat(kms): implement KMS encryption for core infrastructure - RDS, EC2, SecretManager, S3 - Avoid using AWS-managed default keys and instead use customer-managed keys - to control all encryption and description for our resources --- ec2.tf | 14 +++++++++++++ kms.tf | 65 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ rds.tf | 14 ++++++++++++- s3.tf | 6 +++++- 4 files changed, 97 insertions(+), 2 deletions(-) create mode 100644 kms.tf diff --git a/ec2.tf b/ec2.tf index 9e5e3be..c2a0f10 100644 --- a/ec2.tf +++ b/ec2.tf @@ -88,6 +88,20 @@ resource "aws_launch_template" "app" { associate_public_ip_address = true } + # Encrypt the root EBS volume with KMS key + block_device_mappings { + device_name = "/dev/xvda" # Root volume device name (varies by AMI) + + ebs { + volume_size = 20 # or var.root_volume_size if you have one + volume_type = "gp3" + encrypted = true + + # Use the customer-managed KMS key dedicated for EC2 EBS encryption. + kms_key_id = aws_kms_alias.ec2_key_alias.arn + } + } + user_data = base64encode(templatefile("${path.module}/scripts/user_data.sh", { app_user = var.app_user app_group = var.app_group diff --git a/kms.tf b/kms.tf new file mode 100644 index 0000000..c66c270 --- /dev/null +++ b/kms.tf @@ -0,0 +1,65 @@ +# EBS +resource "aws_kms_key" "ec2_key" { + description = "Customer managed KMS key for EC2 EBS volume encryption" + enable_key_rotation = true # AWS rotates the key automatically + rotation_period_in_days = 90 + + tags = { + Name = "${var.name_prefix}-kms-ec2" + } +} + +resource "aws_kms_alias" "ec2_key_alias" { + name = "alias/${var.name_prefix}-ec2-kms" + target_key_id = aws_kms_key.ec2_key.key_id +} + +# RDS +resource "aws_kms_key" "rds_key" { + description = "Customer managed KMS key for RDS instance encryption" + enable_key_rotation = true # AWS rotates the key automatically + rotation_period_in_days = 90 + + tags = { + Name = "${var.name_prefix}-kms-rds" + } +} + +# KMS alias provide a stable and permanent identifier for the key +# The KMS key itself will rotate over time (new key versions are created during rotation), +# which means the key ARN changes. By using an alias, AWS automatically points the alias +# to the newest key version, ensuring that all encrypted resources continue to work without +resource "aws_kms_alias" "rds_key_alias" { + name = "alias/${var.name_prefix}-rds-kms" + target_key_id = aws_kms_key.rds_key.key_id +} + +# S3 bucket +resource "aws_kms_key" "s3_key" { + description = "Customer managed KMS key for S3 object encryption" + enable_key_rotation = true # AWS rotates the key automatically + rotation_period_in_days = 90 + + tags = { + Name = "${var.name_prefix}-kms-s3" + } +} + +resource "aws_kms_alias" "s3_key_alias" { + name = "alias/${var.name_prefix}-s3-kms" + target_key_id = aws_kms_key.s3_key.key_id +} + +resource "aws_kms_key" "secrets_key" { + description = "Customer managed KMS key for Secrets Manager (DB + email secrets)" + enable_key_rotation = true + + tags = { + Name = "${var.name_prefix}-kms-secrets" + } +} + +resource "aws_kms_alias" "secrets_key_alias" { + name = "alias/${var.name_prefix}-secrets-kms" + target_key_id = aws_kms_key.secrets_key.key_id +} diff --git a/rds.tf b/rds.tf index 704426d..51d83dc 100644 --- a/rds.tf +++ b/rds.tf @@ -24,13 +24,19 @@ resource "random_password" "rds" { override_special = "!#$%&'()*+,-.:;<=>?[]^_{|}~" } -# Store the generated pwd in AWS Secrets Manager resource "aws_secretsmanager_secret" "rds" { name = "${var.name_prefix}-rds-master-strong-password" description = "Master password for the ${var.name_prefix} RDS instance" + + # Use a customer-managed KMS key to encrypt the secret. + kms_key_id = aws_kms_alias.secrets_key_alias.arn + + # Set the recovery window to 0 days so the secret is deleted immediately + # when destroyed by Terraform, instead of waiting for the default 30-day recovery period. recovery_window_in_days = 0 } +# Store the generated pwd in AWS Secrets Manager resource "aws_secretsmanager_secret_version" "rds" { secret_id = aws_secretsmanager_secret.rds.id secret_string = random_password.rds.result @@ -53,6 +59,12 @@ resource "aws_db_instance" "db" { multi_az = false publicly_accessible = false + # Enable encryption at rest for the database storage + storage_encrypted = true + + # Use the customer-managed KMS key dedicated for RDS encryption + kms_key_id = aws_kms_alias.rds_key_alias.arn + vpc_security_group_ids = [aws_security_group.db_sg.id] db_subnet_group_name = aws_db_subnet_group.db_private.name parameter_group_name = aws_db_parameter_group.postgres.name diff --git a/s3.tf b/s3.tf index 0fbd0de..a1dba84 100644 --- a/s3.tf +++ b/s3.tf @@ -23,7 +23,11 @@ resource "aws_s3_bucket_server_side_encryption_configuration" "images" { rule { apply_server_side_encryption_by_default { - sse_algorithm = "AES256" + # Use AWS KMS for server-side encryption instead of S3-managed AES256 + sse_algorithm = "aws:kms" + + # Encrypt objects with our customer-managed KMS key dedicated for S3. + kms_master_key_id = aws_kms_alias.s3_key_alias.arn } } } From 5c5d60ecc106f9973e3cbedd162e0b2382e72933 Mon Sep 17 00:00:00 2001 From: Isaac T Date: Thu, 13 Nov 2025 11:53:05 -0500 Subject: [PATCH 08/15] feat(lambda): integrate KMS-encrypted Mailgun API secret via Secrets Manager - Stored Mailgun API key securely in Secrets Manager instead of Lambda env vars - Correct KMS decrypt target resources - Updated Lambda IAM role to allow `secretsmanager:GetSecretValue` and `kms:Decrypt` --- iam_role.tf | 4 ++-- lambda.tf | 29 ++++++++++++++++++++++++----- variables.tf | 16 +++++++++++++--- 3 files changed, 39 insertions(+), 10 deletions(-) diff --git a/iam_role.tf b/iam_role.tf index 2ee28b7..8dbb30f 100644 --- a/iam_role.tf +++ b/iam_role.tf @@ -164,7 +164,7 @@ resource "aws_iam_role_policy" "lambda_secrets_read" { Statement = [{ Effect = "Allow", Action = ["secretsmanager:GetSecretValue"], - Resource = aws_secretsmanager_secret.email_credentials.arn + Resource = aws_secretsmanager_secret.mailgun.arn }] }) } @@ -179,7 +179,7 @@ resource "aws_iam_role_policy" "lambda_kms_decrypt" { Statement = [{ Effect = "Allow", Action = ["kms:Decrypt"], - Resource = aws_kms_key.secrets_key.arn + Resource = aws_kms_alias.secrets_key_alias.arn }] }) } diff --git a/lambda.tf b/lambda.tf index 774557a..70dce3a 100644 --- a/lambda.tf +++ b/lambda.tf @@ -6,18 +6,18 @@ resource "aws_lambda_permission" "allow_sns_invoke" { source_arn = aws_sns_topic.user_signup.arn } -# package lambda directory +# package lambda directory as a ZIP for Lambda deployment data "archive_file" "lambda_zip" { type = "zip" source_dir = "${path.module}/../serverless" - output_path = "{path.module}/dist/email_sender.zip" + output_path = "${path.module}/dist/email_sender.zip" excludes = ["node_modules", ".git", "dist"] } resource "aws_lambda_function" "email_sender" { function_name = "email_sender" runtime = "nodejs20.x" - handler = "ses_email.handler" # File name + exported function name + handler = "src/sendEmail.handler" # File name + exported function name role = aws_iam_role.lambda_email_role.arn # IAM role with SES + CloudWatch permissions # Use the zip generated by the archive_file data source @@ -30,8 +30,27 @@ resource "aws_lambda_function" "email_sender" { # Environment variables available in process.env environment { variables = { - FROM_EMAIL = var.verifiedSenderEmail # Verified sender email in SES - VERIFY_URL_BASE = var.verificationEndPoint # Your web app verification endpoint + MAILGUN_SECRET_ID = aws_secretsmanager_secret.mailgun.id + FROM_EMAIL = var.verifiedSenderEmail # Verified sender email in SES + VERIFY_URL_BASE = var.verificationEndPoint # Your web app verification endpoint + MAILGUN_DOMAIN = var.mailgun_domain # e.g. "mg.isaactai13.me" } } } + +# Secrets Manager secret for Mailgun email service credentials +resource "aws_secretsmanager_secret" "mailgun" { + name = "${var.name_prefix}-mailgun" + kms_key_id = aws_kms_alias.secrets_key_alias.arn +} + + +# Actual value of the secret (Mailgun API key) +resource "aws_secretsmanager_secret_version" "mailgun" { + secret_id = aws_secretsmanager_secret.mailgun.id + + # Store the API key as a JSON payload so Lambda can parse it + secret_string = jsonencode({ + apiKey = var.mailgun_api_key + }) +} diff --git a/variables.tf b/variables.tf index 36bce5e..5fc2ca9 100644 --- a/variables.tf +++ b/variables.tf @@ -176,6 +176,12 @@ variable "template_name" { default = "csye6225_asg" } +variable "account_id" { + description = "SNS resource location" + type = string +} + + # Verified sender email address used by SES variable "verifiedSenderEmail" { description = "The SES verified email address used as the 'From' field for outgoing messages" @@ -188,9 +194,13 @@ variable "verificationEndPoint" { type = string } -variable "account_id" { - description = "SNS resource location" - type = string +variable "mailgun_domain" { + type = string +} + +variable "mailgun_api_key" { + type = string + sensitive = true } variable "tags" { From 6005c72795cb776da7df5abdd539cf141e4796ed Mon Sep 17 00:00:00 2001 From: Isaac T Date: Thu, 13 Nov 2025 11:58:42 -0500 Subject: [PATCH 09/15] feat(user_data): provide EC2 with correct AWS account ID - change SNS topic using arn instead of name - fetch AWS account ID via aws_caller_identity - pass it into user_data for runtime configuration --- ec2.tf | 69 ++------------------------------------------ iam_role.tf | 3 ++ scripts/user_data.sh | 3 +- 3 files changed, 7 insertions(+), 68 deletions(-) diff --git a/ec2.tf b/ec2.tf index c2a0f10..9acd54a 100644 --- a/ec2.tf +++ b/ec2.tf @@ -3,68 +3,6 @@ locals { for az, s in aws_subnet.public : az => s.id } - - az_to_private_subnet_id = { - for az, s in aws_subnet.private : - az => s.id - } - - default_az = sort(keys(local.az_to_public_subnet_id))[0] - chosen_az = coalesce(var.target_az, local.default_az) - - chosen_subnet_id = (var.subnet_tier == "public" - ? lookup(local.az_to_public_subnet_id, local.chosen_az, null) - : lookup(local.az_to_private_subnet_id, local.chosen_az, null) - ) -} - -# Only if you want to create an isolated EC2 not part of ASG to use this -resource "aws_instance" "app" { - count = var.create_ec2_instance ? 1 : 0 - ami = var.ami_id - instance_type = var.instance_type - subnet_id = local.chosen_subnet_id - vpc_security_group_ids = [aws_security_group.app_sg.id] - iam_instance_profile = aws_iam_instance_profile.app_ec2_profile.name - - # assign ssh key - key_name = var.key_name != "" ? var.key_name : null - - # Do NOT protect from accidental termination - disable_api_termination = false - - # Root volume requirements - root_block_device { - volume_type = "gp2" - volume_size = 25 - delete_on_termination = true - } - - # ensure a public IP if your subnet doesn't auto-assign - associate_public_ip_address = var.subnet_tier == "public" ? true : false - - user_data = templatefile("${path.module}/scripts/user_data.sh", { - app_user = var.app_user - app_group = var.app_group - app_dir = var.app_dir - service_name = var.service_name - - db_host = aws_db_instance.db.address - db_port = aws_db_instance.db.port - db_name = var.db_name - db_username = var.db_username - db_password = random_password.rds.result - - aws_region = var.region - s3_bucket = aws_s3_bucket.images.bucket - account_id = var.account_id - sns_topic_name = aws_sns_topic.user_signup.name - }) - - tags = { - Name = "${var.name_prefix}-ec2" - Role = "webapp" - } } # ------------------ @@ -114,10 +52,9 @@ resource "aws_launch_template" "app" { db_username = var.db_username db_password = random_password.rds.result - aws_region = var.region - s3_bucket = aws_s3_bucket.images.bucket - account_id = var.account_id - sns_topic_name = aws_sns_topic.user_signup.name + aws_region = var.region + s3_bucket = aws_s3_bucket.images.bucket + sns_topic_arn = aws_sns_topic.user_signup.arn })) tag_specifications { diff --git a/iam_role.tf b/iam_role.tf index 8dbb30f..be40ce2 100644 --- a/iam_role.tf +++ b/iam_role.tf @@ -138,6 +138,9 @@ resource "aws_iam_role" "lambda_email_role" { }) } +# Get current AWS account identity (used to build ARNs) +data "aws_caller_identity" "me" {} + # Logs resource "aws_iam_role_policy" "lambda_logs" { name = "lambda-basic-logs" diff --git a/scripts/user_data.sh b/scripts/user_data.sh index d0c6aa8..633eb99 100644 --- a/scripts/user_data.sh +++ b/scripts/user_data.sh @@ -22,8 +22,7 @@ DB_USERNAME=${db_username} DB_PASSWORD=${db_password} AWS_REGION=${aws_region} S3_BUCKET=${s3_bucket} -ACCOUNT_ID=${account_id} -SNS_TOPIC_NAME=${sns_topic_name} +SNS_TOPIC_ARN=${sns_topic_arn} EOF chown "${app_user}:${app_group}" "$ENV_FILE" # Change owner of the .env file to normal user From 5fbb2993394a4c5c91d4bc1e13c2f5ff518124be Mon Sep 17 00:00:00 2001 From: Isaac T Date: Thu, 13 Nov 2025 11:59:59 -0500 Subject: [PATCH 10/15] chore: ignore lambda build out directory - Adjust format - Add comment --- .gitignore | 5 +++++ iam_role.tf | 14 +++++++++----- kms.tf | 20 ++++++++++---------- rds.tf | 4 ++-- 4 files changed, 26 insertions(+), 17 deletions(-) diff --git a/.gitignore b/.gitignore index 6349e36..f33aa50 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,8 @@ override.tf.json # Ignore CLI configuration files .terraformrc terraform.rc + +# --------------- +# lambda build output +# --------------- +dist/ diff --git a/iam_role.tf b/iam_role.tf index be40ce2..2b399a9 100644 --- a/iam_role.tf +++ b/iam_role.tf @@ -142,6 +142,8 @@ resource "aws_iam_role" "lambda_email_role" { data "aws_caller_identity" "me" {} # Logs +# Basic logging permissions for the Lambda function. +# Allows the function to create log groups/streams and send log events to CloudWatch Logs. resource "aws_iam_role_policy" "lambda_logs" { name = "lambda-basic-logs" role = aws_iam_role.lambda_email_role.id @@ -158,12 +160,14 @@ resource "aws_iam_role_policy" "lambda_logs" { } # Secrets +# Allow the Lambda function to read the Mailgun API key +# stored in AWS Secrets Manager. resource "aws_iam_role_policy" "lambda_secrets_read" { name = "lambda-secrets-read" role = aws_iam_role.lambda_email_role.id policy = jsonencode({ - Version : "2012-10-17", + Version = "2012-10-17", Statement = [{ Effect = "Allow", Action = ["secretsmanager:GetSecretValue"], @@ -178,7 +182,7 @@ resource "aws_iam_role_policy" "lambda_kms_decrypt" { role = aws_iam_role.lambda_email_role.id policy = jsonencode({ - Version : "2012-10-17", + Version = "2012-10-17", Statement = [{ Effect = "Allow", Action = ["kms:Decrypt"], @@ -193,7 +197,7 @@ resource "aws_iam_role_policy" "lambda_ses_send" { role = aws_iam_role.lambda_email_role.id policy = jsonencode({ - Version : "2012-10-17", + Version = "2012-10-17", Statement = [{ Effect = "Allow", Action = ["ses:SendEmail", "ses:SendRawEmail"], @@ -209,11 +213,11 @@ resource "aws_iam_role_policy" "lambda_dedup_policy" { role = aws_iam_role.lambda_email_role.id policy = jsonencode({ - Version = "2012-10-17" + Version = "2012-10-17", Statement = [ { Effect = "Allow", - Action = ["dynamodb:PutItem", "dynamodb:GetItem"] + Action = ["dynamodb:PutItem", "dynamodb:GetItem"], Resource = aws_dynamodb_table.sent_emails.arn } ] diff --git a/kms.tf b/kms.tf index c66c270..58661bf 100644 --- a/kms.tf +++ b/kms.tf @@ -1,9 +1,9 @@ # EBS resource "aws_kms_key" "ec2_key" { - description = "Customer managed KMS key for EC2 EBS volume encryption" - enable_key_rotation = true # AWS rotates the key automatically + description = "Customer managed KMS key for EC2 EBS volume encryption" + enable_key_rotation = true # AWS rotates the key automatically rotation_period_in_days = 90 - + tags = { Name = "${var.name_prefix}-kms-ec2" } @@ -16,10 +16,10 @@ resource "aws_kms_alias" "ec2_key_alias" { # RDS resource "aws_kms_key" "rds_key" { - description = "Customer managed KMS key for RDS instance encryption" - enable_key_rotation = true # AWS rotates the key automatically + description = "Customer managed KMS key for RDS instance encryption" + enable_key_rotation = true # AWS rotates the key automatically rotation_period_in_days = 90 - + tags = { Name = "${var.name_prefix}-kms-rds" } @@ -36,10 +36,10 @@ resource "aws_kms_alias" "rds_key_alias" { # S3 bucket resource "aws_kms_key" "s3_key" { - description = "Customer managed KMS key for S3 object encryption" - enable_key_rotation = true # AWS rotates the key automatically + description = "Customer managed KMS key for S3 object encryption" + enable_key_rotation = true # AWS rotates the key automatically rotation_period_in_days = 90 - + tags = { Name = "${var.name_prefix}-kms-s3" } @@ -55,7 +55,7 @@ resource "aws_kms_key" "secrets_key" { enable_key_rotation = true tags = { - Name = "${var.name_prefix}-kms-secrets" + Name = "${var.name_prefix}-kms-secrets" } } diff --git a/rds.tf b/rds.tf index 51d83dc..ff2fd78 100644 --- a/rds.tf +++ b/rds.tf @@ -25,8 +25,8 @@ resource "random_password" "rds" { } resource "aws_secretsmanager_secret" "rds" { - name = "${var.name_prefix}-rds-master-strong-password" - description = "Master password for the ${var.name_prefix} RDS instance" + name = "${var.name_prefix}-rds-master-strong-password" + description = "Master password for the ${var.name_prefix} RDS instance" # Use a customer-managed KMS key to encrypt the secret. kms_key_id = aws_kms_alias.secrets_key_alias.arn From 0797e7d4b1c4c09a86482185575352261d0dbf1d Mon Sep 17 00:00:00 2001 From: Isaac T Date: Thu, 13 Nov 2025 13:25:46 -0500 Subject: [PATCH 11/15] feat(permission): add kms & dynamodb full access to iam user --- AWS_TF_permission.json | 12 ++++++++++++ security-group.tf | 13 +++++++------ variables.tf | 6 ------ 3 files changed, 19 insertions(+), 12 deletions(-) diff --git a/AWS_TF_permission.json b/AWS_TF_permission.json index c4d1004..733e800 100644 --- a/AWS_TF_permission.json +++ b/AWS_TF_permission.json @@ -114,6 +114,18 @@ "arn:aws:s3:::awsserverlessrepo-changesets*", "arn:aws:s3:::secrets-manager-rotation-apps-*/*" ] + }, + { + "Sid": "FullKMSAccess", + "Effect": "Allow", + "Action": "kms:*", + "Resource": "*" + }, + { + "Sid": "FullDynamoDBAccess", + "Effect": "Allow", + "Action": "dynamodb:*", + "Resource": "*" } ] } diff --git a/security-group.tf b/security-group.tf index 69fe1cd..d36d71c 100644 --- a/security-group.tf +++ b/security-group.tf @@ -20,12 +20,6 @@ resource "aws_vpc_security_group_ingress_rule" "lb_ingress_ipv4" { description = "Allow TCP ${each.value} from anywhere (IPv4)" } -resource "aws_vpc_security_group_egress_rule" "lb_all_out" { - security_group_id = aws_security_group.lb_sg.id - cidr_ipv4 = "0.0.0.0/0" - ip_protocol = "-1" -} - resource "aws_vpc_security_group_ingress_rule" "lb_ingress_ipv6" { for_each = toset([for p in local.lb_ingress_ports : tostring(p)]) security_group_id = aws_security_group.lb_sg.id @@ -43,6 +37,13 @@ resource "aws_vpc_security_group_egress_rule" "lb_all_out_ipv4" { description = "Allow all outbound traffic (IPv4)" } +# Egress IPv6 +resource "aws_vpc_security_group_egress_rule" "lb_all_out_ipv6" { + security_group_id = aws_security_group.lb_sg.id + cidr_ipv6 = "::/0" + ip_protocol = "-1" + description = "Allow all outbound traffic (IPv6)" +} #---------------------- # Web App SG diff --git a/variables.tf b/variables.tf index 5fc2ca9..81e5415 100644 --- a/variables.tf +++ b/variables.tf @@ -176,12 +176,6 @@ variable "template_name" { default = "csye6225_asg" } -variable "account_id" { - description = "SNS resource location" - type = string -} - - # Verified sender email address used by SES variable "verifiedSenderEmail" { description = "The SES verified email address used as the 'From' field for outgoing messages" From d093056d639d41144cef4680874d29f4e8ea7f44 Mon Sep 17 00:00:00 2001 From: Isaac T Date: Thu, 13 Nov 2025 13:53:58 -0500 Subject: [PATCH 12/15] feat(permission): add lambda & acm permissions --- AWS_TF_permission.json | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/AWS_TF_permission.json b/AWS_TF_permission.json index 733e800..a549be4 100644 --- a/AWS_TF_permission.json +++ b/AWS_TF_permission.json @@ -126,6 +126,18 @@ "Effect": "Allow", "Action": "dynamodb:*", "Resource": "*" + }, + { + "Sid": "FullLambdaAccess", + "Effect": "Allow", + "Action": "lambda:*", + "Resource": "*" + }, + { + "Sid": "FullACMAccess", + "Effect": "Allow", + "Action": "acm:*", + "Resource": "*" } ] } From 0bfdfed114ce9cf2dc3505060b2bb4d74ad9e717 Mon Sep 17 00:00:00 2001 From: Isaac T Date: Thu, 13 Nov 2025 13:55:23 -0500 Subject: [PATCH 13/15] feat(https): enable HTTPS and redirect HTTP to HTTPS - Added HTTPS listener on port 443 using ACM certificate - Ensured ALB forwards only HTTPS requests to target group --- load_balancer.tf | 25 +++++++++++++++++++++++++ route53_records.tf | 2 +- variables.tf | 2 +- 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/load_balancer.tf b/load_balancer.tf index 71475e1..8308849 100644 --- a/load_balancer.tf +++ b/load_balancer.tf @@ -12,6 +12,31 @@ resource "aws_lb_listener" "http" { load_balancer_arn = aws_lb.app_alb.arn port = 80 protocol = "HTTP" + + default_action { + type = "redirect" + redirect { + port = "443" + protocol = "HTTPS" + status_code = "HTTP_301" + } + } +} + +# enable https with AWS ACM +data "aws_acm_certificate" "dev_cert" { + domain = "*.${var.domain_name}" + most_recent = true + statuses = ["ISSUED"] +} + +resource "aws_lb_listener" "https" { + load_balancer_arn = aws_lb.app_alb.arn + port = 443 + protocol = "HTTPS" + ssl_policy = "ELBSecurityPolicy-2016-08" + certificate_arn = data.aws_acm_certificate.dev_cert.arn + default_action { type = "forward" target_group_arn = aws_lb_target_group.app_tg.arn diff --git a/route53_records.tf b/route53_records.tf index 53da0ac..12e51fd 100644 --- a/route53_records.tf +++ b/route53_records.tf @@ -1,5 +1,5 @@ data "aws_route53_zone" "env" { - name = var.route53_zone_name + name = var.domain_name private_zone = false } diff --git a/variables.tf b/variables.tf index 81e5415..9772c96 100644 --- a/variables.tf +++ b/variables.tf @@ -145,7 +145,7 @@ variable "s3_prefix" { default = "" } -variable "route53_zone_name" { +variable "domain_name" { description = "Public hosted zone name for this environment (e.g., dev.domain.tld or demo.domain.tld)" type = string } From abde94952b8188dd185ef4a2d9e12a7574524e99 Mon Sep 17 00:00:00 2001 From: Isaac T Date: Thu, 13 Nov 2025 14:47:12 -0500 Subject: [PATCH 14/15] fix(lambda): Grant SNS service permission to invoke this Lambda --- ec2.tf | 24 ++++++++++++------------ lambda.tf | 6 +++++- load_balancer.tf | 1 + 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/ec2.tf b/ec2.tf index 9acd54a..d865217 100644 --- a/ec2.tf +++ b/ec2.tf @@ -27,18 +27,18 @@ resource "aws_launch_template" "app" { } # Encrypt the root EBS volume with KMS key - block_device_mappings { - device_name = "/dev/xvda" # Root volume device name (varies by AMI) - - ebs { - volume_size = 20 # or var.root_volume_size if you have one - volume_type = "gp3" - encrypted = true - - # Use the customer-managed KMS key dedicated for EC2 EBS encryption. - kms_key_id = aws_kms_alias.ec2_key_alias.arn - } - } + # block_device_mappings { + # device_name = "/dev/xvda" # Root volume device name (varies by AMI) + # + # ebs { + # volume_size = 20 # or var.root_volume_size if you have one + # volume_type = "gp3" + # encrypted = true + # + # # Use the customer-managed KMS key dedicated for EC2 EBS encryption. + # kms_key_id = aws_kms_alias.ec2_key_alias.arn + # } + # } user_data = base64encode(templatefile("${path.module}/scripts/user_data.sh", { app_user = var.app_user diff --git a/lambda.tf b/lambda.tf index 70dce3a..c094751 100644 --- a/lambda.tf +++ b/lambda.tf @@ -2,7 +2,11 @@ resource "aws_lambda_permission" "allow_sns_invoke" { statement_id = "AllowExecutionFromSNS" action = "lambda:InvokeFunction" function_name = aws_lambda_function.email_sender.function_name - principal = "sns.amazon.com" + + # Grant SNS service permission to invoke this Lambda + principal = "sns.amazonaws.com" + + # Restrict invocation to only this specific SNS topic for security source_arn = aws_sns_topic.user_signup.arn } diff --git a/load_balancer.tf b/load_balancer.tf index 8308849..c24112c 100644 --- a/load_balancer.tf +++ b/load_balancer.tf @@ -15,6 +15,7 @@ resource "aws_lb_listener" "http" { default_action { type = "redirect" + redirect { port = "443" protocol = "HTTPS" From 71459e5b579305b5b3e85ebe74b46b547fd10429 Mon Sep 17 00:00:00 2001 From: Isaac T Date: Thu, 13 Nov 2025 14:57:06 -0500 Subject: [PATCH 15/15] feat(format): fix format --- lambda.tf | 6 +++--- load_balancer.tf | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lambda.tf b/lambda.tf index c094751..159a38c 100644 --- a/lambda.tf +++ b/lambda.tf @@ -4,10 +4,10 @@ resource "aws_lambda_permission" "allow_sns_invoke" { function_name = aws_lambda_function.email_sender.function_name # Grant SNS service permission to invoke this Lambda - principal = "sns.amazonaws.com" - + principal = "sns.amazonaws.com" + # Restrict invocation to only this specific SNS topic for security - source_arn = aws_sns_topic.user_signup.arn + source_arn = aws_sns_topic.user_signup.arn } # package lambda directory as a ZIP for Lambda deployment diff --git a/load_balancer.tf b/load_balancer.tf index c24112c..880f682 100644 --- a/load_balancer.tf +++ b/load_balancer.tf @@ -14,7 +14,7 @@ resource "aws_lb_listener" "http" { protocol = "HTTP" default_action { - type = "redirect" + type = "redirect" redirect { port = "443" @@ -26,9 +26,9 @@ resource "aws_lb_listener" "http" { # enable https with AWS ACM data "aws_acm_certificate" "dev_cert" { - domain = "*.${var.domain_name}" - most_recent = true - statuses = ["ISSUED"] + domain = "*.${var.domain_name}" + most_recent = true + statuses = ["ISSUED"] } resource "aws_lb_listener" "https" {