diff --git a/infrastructure/modules/security/.terraform.lock.hcl b/infrastructure/modules/security/.terraform.lock.hcl new file mode 100644 index 0000000000..55057fd2e7 --- /dev/null +++ b/infrastructure/modules/security/.terraform.lock.hcl @@ -0,0 +1,25 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "6.22.0" + constraints = "6.22.0" + hashes = [ + "h1:TV1UZ7DzioV1EUY/lMS+eIInU379DA1Q2QwnEGGZMks=", + "zh:0ed7ceb13bade9076021a14f995d07346d3063f4a419a904d5804d76e372bbda", + "zh:195dcde5a4b0def82bc3379053edc13941ff94ea5905808fe575f7c7bbd66693", + "zh:4047c4dba121d29859b72d2155c47f969b41d3c5768f73dff5d8a0cc55f74e52", + "zh:5694f37d6ea69b6f96dfb30d53e66f7a41c1aad214c212b6ffa54bdd799e3b27", + "zh:6cf8bb7d984b1fae9fd10d6ce1e62f6c10751a1040734b75a1f7286609782e49", + "zh:737d0e600dfe2626b4d6fc5dd2b24c0997fd983228a7a607b9176a1894a281a0", + "zh:7d328a195ce36b1170afe6758cf88223c8765620211f5cc0451bdd6899243b4e", + "zh:7edb4bc34baeba92889bd9ed50b34c04b3eeb3d8faa8bb72699c6335a2e95bab", + "zh:8e71836814e95454b00c51f3cb3e10fd78a59f7dc4c5362af64233fee989790d", + "zh:9367f63b23d9ddfab590b2247a8ff5ccf83410cbeca43c6e441c488c45efff4c", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:a007de80ffde8539a73ee39fcfbe7ed12e025c98cd29b2110a7383b41a4aad39", + "zh:aae7b7aed8bf3a4bea80a9a2f08fef1adeb748beff236c4a54af93bb6c09a56c", + "zh:b5a16b59d4210c1eaf35c8c027ecdab9e074dd081d602f5112eecdebf2e1866d", + "zh:d479bad0a004e4893bf0ba6c6cd867fefd14000051bbe3de5b44a925e3d46cd5", + ] +} diff --git a/infrastructure/modules/security/main.tf b/infrastructure/modules/security/main.tf index 7228bec2d5..408a3d3640 100644 --- a/infrastructure/modules/security/main.tf +++ b/infrastructure/modules/security/main.tf @@ -25,61 +25,6 @@ resource "aws_security_group" "alb" { } } -resource "aws_security_group" "ecs" { - description = "Security group for ECS tasks" - name = "${var.project_name}-${var.environment}-ecs-sg" - tags = merge(var.common_tags, { - Name = "${var.project_name}-${var.environment}-ecs-sg" - }) - vpc_id = var.vpc_id -} - -resource "aws_security_group" "frontend" { - description = "Security group for frontend ECS tasks" - name = "${var.project_name}-${var.environment}-frontend-sg" - tags = merge(var.common_tags, { - Name = "${var.project_name}-${var.environment}-frontend-sg" - }) - vpc_id = var.vpc_id -} - -resource "aws_security_group" "lambda" { - description = "Security group for Lambda functions (Zappa)" - name = "${var.project_name}-${var.environment}-lambda-sg" - tags = merge(var.common_tags, { - Name = "${var.project_name}-${var.environment}-lambda-sg" - }) - vpc_id = var.vpc_id -} - -resource "aws_security_group" "rds" { - description = "Security group for RDS PostgreSQL" - name = "${var.project_name}-${var.environment}-rds-sg" - tags = merge(var.common_tags, { - Name = "${var.project_name}-${var.environment}-rds-sg" - }) - vpc_id = var.vpc_id -} - -resource "aws_security_group" "rds_proxy" { - count = var.create_rds_proxy ? 1 : 0 - description = "Security group for RDS Proxy" - name = "${var.project_name}-${var.environment}-rds-proxy-sg" - tags = merge(var.common_tags, { - Name = "${var.project_name}-${var.environment}-rds-proxy-sg" - }) - vpc_id = var.vpc_id -} - -resource "aws_security_group" "redis" { - description = "Security group for ElastiCache Redis" - name = "${var.project_name}-${var.environment}-redis-sg" - tags = merge(var.common_tags, { - Name = "${var.project_name}-${var.environment}-redis-sg" - }) - vpc_id = var.vpc_id -} - resource "aws_security_group_rule" "alb_http" { cidr_blocks = ["0.0.0.0/0"] description = "Allow HTTP from internet" @@ -110,6 +55,15 @@ resource "aws_security_group_rule" "alb_to_frontend" { type = "egress" } +resource "aws_security_group" "ecs" { + description = "Security group for ECS tasks" + name = "${var.project_name}-${var.environment}-ecs-sg" + tags = merge(var.common_tags, { + Name = "${var.project_name}-${var.environment}-ecs-sg" + }) + vpc_id = var.vpc_id +} + resource "aws_security_group_rule" "ecs_egress_all" { cidr_blocks = var.default_egress_cidr_blocks description = "Allow all outbound traffic" @@ -131,6 +85,15 @@ resource "aws_security_group_rule" "ecs_to_vpc_endpoints" { type = "egress" } +resource "aws_security_group" "frontend" { + description = "Security group for frontend ECS tasks" + name = "${var.project_name}-${var.environment}-frontend-sg" + tags = merge(var.common_tags, { + Name = "${var.project_name}-${var.environment}-frontend-sg" + }) + vpc_id = var.vpc_id +} + resource "aws_security_group_rule" "frontend_from_alb" { description = "Allow traffic from ALB" from_port = 3000 @@ -162,6 +125,15 @@ resource "aws_security_group_rule" "frontend_to_vpc_endpoints" { type = "egress" } +resource "aws_security_group" "lambda" { + description = "Security group for Lambda functions (Zappa)" + name = "${var.project_name}-${var.environment}-lambda-sg" + tags = merge(var.common_tags, { + Name = "${var.project_name}-${var.environment}-lambda-sg" + }) + vpc_id = var.vpc_id +} + resource "aws_security_group_rule" "lambda_egress_all" { cidr_blocks = var.default_egress_cidr_blocks description = "Allow all outbound traffic" @@ -183,6 +155,25 @@ resource "aws_security_group_rule" "lambda_to_vpc_endpoints" { type = "egress" } +resource "aws_security_group" "rds" { + description = "Security group for RDS PostgreSQL" + name = "${var.project_name}-${var.environment}-rds-sg" + tags = merge(var.common_tags, { + Name = "${var.project_name}-${var.environment}-rds-sg" + }) + vpc_id = var.vpc_id +} + +resource "aws_security_group" "rds_proxy" { + count = var.create_rds_proxy ? 1 : 0 + description = "Security group for RDS Proxy" + name = "${var.project_name}-${var.environment}-rds-proxy-sg" + tags = merge(var.common_tags, { + Name = "${var.project_name}-${var.environment}-rds-proxy-sg" + }) + vpc_id = var.vpc_id +} + resource "aws_security_group_rule" "rds_from_ecs" { count = var.create_rds_proxy ? 0 : 1 description = "PostgreSQL from ECS" @@ -249,6 +240,15 @@ resource "aws_security_group_rule" "rds_proxy_from_lambda" { type = "ingress" } +resource "aws_security_group" "redis" { + description = "Security group for ElastiCache Redis" + name = "${var.project_name}-${var.environment}-redis-sg" + tags = merge(var.common_tags, { + Name = "${var.project_name}-${var.environment}-redis-sg" + }) + vpc_id = var.vpc_id +} + resource "aws_security_group_rule" "redis_from_ecs" { description = "Redis from ECS" from_port = var.redis_port diff --git a/infrastructure/modules/security/tests/security.tftest.hcl b/infrastructure/modules/security/tests/security.tftest.hcl new file mode 100644 index 0000000000..4622f43bb5 --- /dev/null +++ b/infrastructure/modules/security/tests/security.tftest.hcl @@ -0,0 +1,512 @@ +variables { + common_tags = { Environment = "test", Project = "nest" } + db_port = 5432 + environment = "test" + project_name = "nest" + redis_port = 6379 + vpc_id = "vpc-12345678" +} + +run "test_alb_security_group_name_format" { + command = plan + + assert { + condition = aws_security_group.alb.name == "${var.project_name}-${var.environment}-alb-sg" + error_message = "ALB security group name must follow format: {project}-{environment}-alb-sg." + } +} + +run "test_alb_http_rule_port" { + command = plan + + assert { + condition = aws_security_group_rule.alb_http.from_port == 80 + error_message = "ALB HTTP rule must allow from_port 80." + } + assert { + condition = aws_security_group_rule.alb_http.to_port == 80 + error_message = "ALB HTTP rule must allow to_port 80." + } +} + +run "test_alb_http_rule_type" { + command = plan + + assert { + condition = aws_security_group_rule.alb_http.type == "ingress" + error_message = "ALB HTTP rule must be of type ingress." + } +} + +run "test_alb_https_rule_port" { + command = plan + + assert { + condition = aws_security_group_rule.alb_https.from_port == 443 + error_message = "ALB HTTPS rule must allow from_port 443." + } + assert { + condition = aws_security_group_rule.alb_https.to_port == 443 + error_message = "ALB HTTPS rule must allow to_port 443." + } +} + +run "test_alb_https_rule_type" { + command = plan + + assert { + condition = aws_security_group_rule.alb_https.type == "ingress" + error_message = "ALB HTTPS rule must be of type ingress." + } +} + +run "test_alb_to_frontend_rule_port" { + command = plan + + assert { + condition = aws_security_group_rule.alb_to_frontend.from_port == 3000 + error_message = "ALB to Frontend rule must allow from_port 3000." + } + assert { + condition = aws_security_group_rule.alb_to_frontend.to_port == 3000 + error_message = "ALB to Frontend rule must allow to_port 3000." + } +} + +run "test_alb_to_frontend_rule_type" { + command = plan + + assert { + condition = aws_security_group_rule.alb_to_frontend.type == "egress" + error_message = "ALB to Frontend rule must be of type egress." + } +} + +run "test_ecs_security_group_name_format" { + command = plan + + assert { + condition = aws_security_group.ecs.name == "${var.project_name}-${var.environment}-ecs-sg" + error_message = "ECS security group name must follow format: {project}-{environment}-ecs-sg." + } +} + +run "test_ecs_egress_all_rule_port" { + command = plan + assert { + condition = aws_security_group_rule.ecs_egress_all.from_port == 0 + error_message = "ECS egress rule must allow from_port 0." + } + assert { + condition = aws_security_group_rule.ecs_egress_all.to_port == 0 + error_message = "ECS egress rule must allow to_port 0." + } +} + +run "test_ecs_egress_all_rule_protocol" { + command = plan + assert { + condition = aws_security_group_rule.ecs_egress_all.protocol == "-1" + error_message = "ECS egress rule must use protocol -1 (all)." + } +} + +run "test_ecs_egress_all_rule_type" { + command = plan + assert { + condition = aws_security_group_rule.ecs_egress_all.type == "egress" + error_message = "ECS egress rule must be of type egress." + } +} + +run "test_frontend_security_group_name_format" { + command = plan + assert { + condition = aws_security_group.frontend.name == "${var.project_name}-${var.environment}-frontend-sg" + error_message = "Frontend security group name must follow format: {project}-{environment}-frontend-sg." + } +} + +run "test_frontend_from_alb_rule_port" { + command = plan + assert { + condition = aws_security_group_rule.frontend_from_alb.from_port == 3000 + error_message = "Frontend ingress from ALB must allow from_port 3000." + } + assert { + condition = aws_security_group_rule.frontend_from_alb.to_port == 3000 + error_message = "Frontend ingress from ALB must allow to_port 3000." + } +} + +run "test_frontend_from_alb_rule_type" { + command = plan + assert { + condition = aws_security_group_rule.frontend_from_alb.type == "ingress" + error_message = "Frontend ingress from ALB must be of type ingress." + } +} + +run "test_frontend_https_rule_port" { + command = plan + assert { + condition = aws_security_group_rule.frontend_https.from_port == 443 + error_message = "Frontend HTTPS egress rule must allow from_port 443." + } + assert { + condition = aws_security_group_rule.frontend_https.to_port == 443 + error_message = "Frontend HTTPS egress rule must allow to_port 443." + } +} + +run "test_frontend_https_rule_type" { + command = plan + assert { + condition = aws_security_group_rule.frontend_https.type == "egress" + error_message = "Frontend HTTPS egress rule must be of type egress." + } +} + +run "test_lambda_security_group_name_format" { + command = plan + assert { + condition = aws_security_group.lambda.name == "${var.project_name}-${var.environment}-lambda-sg" + error_message = "Lambda security group name must follow format: {project}-{environment}-lambda-sg." + } +} + +run "test_lambda_egress_all_rule_port" { + command = plan + assert { + condition = aws_security_group_rule.lambda_egress_all.from_port == 0 + error_message = "Lambda egress rule must allow from_port 0." + } + assert { + condition = aws_security_group_rule.lambda_egress_all.to_port == 0 + error_message = "Lambda egress rule must allow to_port 0." + } +} + +run "test_lambda_egress_all_rule_protocol" { + command = plan + assert { + condition = aws_security_group_rule.lambda_egress_all.protocol == "-1" + error_message = "Lambda egress rule must use protocol -1 (all)." + } +} + +run "test_lambda_egress_all_rule_type" { + command = plan + assert { + condition = aws_security_group_rule.lambda_egress_all.type == "egress" + error_message = "Lambda egress rule must be of type egress." + } +} + +run "test_rds_security_group_name_format" { + command = plan + assert { + condition = aws_security_group.rds.name == "${var.project_name}-${var.environment}-rds-sg" + error_message = "RDS security group name must follow format: {project}-{environment}-rds-sg." + } +} + +run "test_rds_proxy_security_group_name_format" { + command = plan + variables { + create_rds_proxy = true + } + assert { + condition = aws_security_group.rds_proxy[0].name == "${var.project_name}-${var.environment}-rds-proxy-sg" + error_message = "RDS Proxy security group name must follow format: {project}-{environment}-rds-proxy-sg." + } +} + +run "test_rds_from_ecs_rule_port" { + command = plan + variables { + create_rds_proxy = false + } + assert { + condition = aws_security_group_rule.rds_from_ecs[0].from_port == var.db_port + error_message = "RDS ingress from ECS must allow database port." + } + assert { + condition = aws_security_group_rule.rds_from_ecs[0].to_port == var.db_port + error_message = "RDS ingress from ECS must allow database port." + } +} + +run "test_rds_from_ecs_rule_type" { + command = plan + variables { + create_rds_proxy = false + } + assert { + condition = aws_security_group_rule.rds_from_ecs[0].type == "ingress" + error_message = "RDS ingress from ECS must be of type ingress." + } +} + +run "test_rds_from_lambda_rule_port" { + command = plan + variables { + create_rds_proxy = false + } + assert { + condition = aws_security_group_rule.rds_from_lambda[0].from_port == var.db_port + error_message = "RDS ingress from Lambda must allow database port." + } + assert { + condition = aws_security_group_rule.rds_from_lambda[0].to_port == var.db_port + error_message = "RDS ingress from Lambda must allow database port." + } +} + +run "test_rds_from_lambda_rule_type" { + command = plan + variables { + create_rds_proxy = false + } + assert { + condition = aws_security_group_rule.rds_from_lambda[0].type == "ingress" + error_message = "RDS ingress from Lambda must be of type ingress." + } +} + +run "test_rds_from_proxy_rule_port" { + command = plan + variables { + create_rds_proxy = true + } + assert { + condition = aws_security_group_rule.rds_from_proxy[0].from_port == var.db_port + error_message = "RDS to RDS Proxy ingress rule must allow database port." + } + assert { + condition = aws_security_group_rule.rds_from_proxy[0].to_port == var.db_port + error_message = "RDS to RDS Proxy ingress rule must allow database port." + } +} + +run "test_rds_from_proxy_rule_type" { + command = plan + variables { + create_rds_proxy = true + } + assert { + condition = aws_security_group_rule.rds_from_proxy[0].type == "ingress" + error_message = "RDS Proxy to RDS rule must be of type ingress." + } +} + +run "test_proxy_to_rds_rule_port" { + command = plan + variables { + create_rds_proxy = true + } + assert { + condition = aws_security_group_rule.rds_proxy_to_rds[0].from_port == var.db_port + error_message = "RDS Proxy to RDS egress rule must allow database port." + } + assert { + condition = aws_security_group_rule.rds_proxy_to_rds[0].to_port == var.db_port + error_message = "RDS Proxy to RDS egress rule must allow database port." + } +} + +run "test_proxy_to_rds_rule_type" { + command = plan + variables { + create_rds_proxy = true + } + assert { + condition = aws_security_group_rule.rds_proxy_to_rds[0].type == "egress" + error_message = "RDS Proxy to RDS rule must be of type egress." + } +} + +run "test_rds_proxy_from_ecs_rule_port" { + command = plan + variables { + create_rds_proxy = true + } + assert { + condition = aws_security_group_rule.rds_proxy_from_ecs[0].from_port == var.db_port + error_message = "RDS Proxy ingress from ECS must allow database port." + } + assert { + condition = aws_security_group_rule.rds_proxy_from_ecs[0].to_port == var.db_port + error_message = "RDS Proxy ingress from ECS must allow database port." + } +} + +run "test_rds_proxy_from_ecs_rule_type" { + command = plan + variables { + create_rds_proxy = true + } + assert { + condition = aws_security_group_rule.rds_proxy_from_ecs[0].type == "ingress" + error_message = "RDS Proxy ingress from ECS must be of type ingress." + } +} + +run "test_rds_proxy_from_lambda_rule_port" { + command = plan + variables { + create_rds_proxy = true + } + assert { + condition = aws_security_group_rule.rds_proxy_from_lambda[0].from_port == var.db_port + error_message = "RDS Proxy ingress from Lambda must allow database port." + } + assert { + condition = aws_security_group_rule.rds_proxy_from_lambda[0].to_port == var.db_port + error_message = "RDS Proxy ingress from Lambda must allow database port." + } +} + +run "test_rds_proxy_from_lambda_rule_type" { + command = plan + variables { + create_rds_proxy = true + } + assert { + condition = aws_security_group_rule.rds_proxy_from_lambda[0].type == "ingress" + error_message = "RDS Proxy ingress from Lambda must be of type ingress." + } +} + +run "test_redis_security_group_name_format" { + command = plan + assert { + condition = aws_security_group.redis.name == "${var.project_name}-${var.environment}-redis-sg" + error_message = "Redis security group name must follow format: {project}-{environment}-redis-sg." + } +} + +run "test_redis_from_ecs_rule_port" { + command = plan + assert { + condition = aws_security_group_rule.redis_from_ecs.from_port == var.redis_port + error_message = "Redis ingress from ECS must allow redis port." + } + assert { + condition = aws_security_group_rule.redis_from_ecs.to_port == var.redis_port + error_message = "Redis ingress from ECS must allow redis port." + } +} + +run "test_redis_from_ecs_rule_type" { + command = plan + assert { + condition = aws_security_group_rule.redis_from_ecs.type == "ingress" + error_message = "Redis ingress from ECS must be of type ingress." + } +} + +run "test_redis_from_lambda_rule_port" { + command = plan + assert { + condition = aws_security_group_rule.redis_from_lambda.from_port == var.redis_port + error_message = "Redis ingress from Lambda must allow redis port." + } + assert { + condition = aws_security_group_rule.redis_from_lambda.to_port == var.redis_port + error_message = "Redis ingress from Lambda must allow redis port." + } +} + +run "test_redis_from_lambda_rule_type" { + command = plan + assert { + condition = aws_security_group_rule.redis_from_lambda.type == "ingress" + error_message = "Redis ingress from Lambda must be of type ingress." + } +} + + +run "test_ecs_to_vpc_endpoints_rule_port" { + command = plan + variables { + create_vpc_endpoint_rules = true + vpc_endpoint_sg_id = "sg-endpoint123" + } + assert { + condition = aws_security_group_rule.ecs_to_vpc_endpoints[0].from_port == 443 + error_message = "ECS to VPC endpoints rule must use port 443." + } + assert { + condition = aws_security_group_rule.ecs_to_vpc_endpoints[0].to_port == 443 + error_message = "ECS to VPC endpoints rule must use port 443." + } +} + +run "test_ecs_to_vpc_endpoints_rule_type" { + command = plan + variables { + create_vpc_endpoint_rules = true + vpc_endpoint_sg_id = "sg-endpoint123" + } + assert { + condition = aws_security_group_rule.ecs_to_vpc_endpoints[0].type == "egress" + error_message = "ECS to VPC endpoints rule must be of type egress." + } +} + +run "test_frontend_to_vpc_endpoints_rule_port" { + command = plan + variables { + create_vpc_endpoint_rules = true + vpc_endpoint_sg_id = "sg-endpoint123" + } + assert { + condition = aws_security_group_rule.frontend_to_vpc_endpoints[0].from_port == 443 + error_message = "Frontend to VPC endpoints rule must use port 443." + } + assert { + condition = aws_security_group_rule.frontend_to_vpc_endpoints[0].to_port == 443 + error_message = "Frontend to VPC endpoints rule must use port 443." + } +} + +run "test_frontend_to_vpc_endpoints_rule_type" { + command = plan + variables { + create_vpc_endpoint_rules = true + vpc_endpoint_sg_id = "sg-endpoint123" + } + assert { + condition = aws_security_group_rule.frontend_to_vpc_endpoints[0].type == "egress" + error_message = "Frontend to VPC endpoints rule must be of type egress." + } +} + +run "test_lambda_to_vpc_endpoints_rule_port" { + command = plan + variables { + create_vpc_endpoint_rules = true + vpc_endpoint_sg_id = "sg-endpoint123" + } + assert { + condition = aws_security_group_rule.lambda_to_vpc_endpoints[0].from_port == 443 + error_message = "Lambda to VPC endpoints rule must use port 443." + } + assert { + condition = aws_security_group_rule.lambda_to_vpc_endpoints[0].to_port == 443 + error_message = "Lambda to VPC endpoints rule must use port 443." + } +} + +run "test_lambda_to_vpc_endpoints_rule_type" { + command = plan + variables { + create_vpc_endpoint_rules = true + vpc_endpoint_sg_id = "sg-endpoint123" + } + assert { + condition = aws_security_group_rule.lambda_to_vpc_endpoints[0].type == "egress" + error_message = "Lambda to VPC endpoints rule must be of type egress." + } +} diff --git a/infrastructure/modules/security/variables.tf b/infrastructure/modules/security/variables.tf index 0fd1ff7572..3f3d407b2f 100644 --- a/infrastructure/modules/security/variables.tf +++ b/infrastructure/modules/security/variables.tf @@ -19,12 +19,24 @@ variable "create_vpc_endpoint_rules" { variable "db_port" { description = "The port for the RDS database." type = number + + validation { + condition = var.db_port > 0 && var.db_port < 65536 + error_message = "db_port must be between 1 and 65535." + } } variable "default_egress_cidr_blocks" { description = "A list of CIDR blocks to allow for default egress traffic." type = list(string) default = ["0.0.0.0/0"] + + validation { + condition = alltrue([ + for cidr in var.default_egress_cidr_blocks : can(cidrhost(cidr, 0)) + ]) + error_message = "One or more CIDR blocks are invalid." + } } variable "environment" { @@ -40,6 +52,11 @@ variable "project_name" { variable "redis_port" { description = "The port for the Redis cache." type = number + + validation { + condition = var.redis_port > 0 && var.redis_port < 65536 + error_message = "redis_port must be between 1 and 65535." + } } variable "vpc_endpoint_sg_id" {