diff --git a/compute/ecs_cluster/README.md b/compute/ecs_cluster/README.md index 55fafe85..5db87802 100644 --- a/compute/ecs_cluster/README.md +++ b/compute/ecs_cluster/README.md @@ -263,6 +263,7 @@ module "api_service" { | public_alb_ssl_policy | SSL policy for HTTPS | `string` | `"ELBSecurityPolicy-TLS13-1-2-2021-06"` | no | | public_alb_idle_timeout | Idle timeout in seconds | `number` | `60` | no | | public_alb_ingress_cidr_blocks | Allowed IPv4 CIDR blocks | `list(string)` | `["0.0.0.0/0"]` | no | +| public_alb_ingress_security_group_ids | Security group IDs allowed to access the public ALB | `list(string)` | `[]` | no | | public_alb_access_logs_enabled | Enable access logs | `bool` | `false` | no | | public_alb_access_logs_bucket_arn | S3 bucket ARN for access logs | `string` | `null` | no | | public_alb_web_acl_arn | WAFv2 Web ACL ARN | `string` | `null` | no | @@ -277,6 +278,7 @@ module "api_service" { | private_alb_ssl_policy | SSL policy for HTTPS | `string` | `"ELBSecurityPolicy-TLS13-1-2-2021-06"` | no | | private_alb_idle_timeout | Idle timeout in seconds | `number` | `60` | no | | private_alb_ingress_cidr_blocks | Allowed IPv4 CIDR blocks | `list(string)` | `["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"]` | no | +| private_alb_ingress_security_group_ids | Security group IDs allowed to access the private ALB (e.g., CloudFront VPC origins) | `list(string)` | `[]` | no | | private_alb_access_logs_enabled | Enable access logs | `bool` | `false` | no | | private_alb_access_logs_bucket_arn | S3 bucket ARN for access logs | `string` | `null` | no | diff --git a/compute/ecs_cluster/load_balancers.tf b/compute/ecs_cluster/load_balancers.tf index a7b40da5..5ff09c5b 100644 --- a/compute/ecs_cluster/load_balancers.tf +++ b/compute/ecs_cluster/load_balancers.tf @@ -28,8 +28,9 @@ module "public_alb" { deletion_protection_enabled = var.load_balancer_deletion_protection_enabled # Security - ingress_cidr_blocks = var.public_alb_ingress_cidr_blocks - ingress_ipv6_cidr_blocks = var.public_alb_ingress_ipv6_cidr_blocks + ingress_cidr_blocks = var.public_alb_ingress_cidr_blocks + ingress_ipv6_cidr_blocks = var.public_alb_ingress_ipv6_cidr_blocks + ingress_security_group_ids = var.public_alb_ingress_security_group_ids # Access logs access_logs_enabled = var.public_alb_access_logs_enabled @@ -69,8 +70,9 @@ module "private_alb" { deletion_protection_enabled = var.load_balancer_deletion_protection_enabled # Security - ingress_cidr_blocks = var.private_alb_ingress_cidr_blocks - ingress_ipv6_cidr_blocks = var.private_alb_ingress_ipv6_cidr_blocks + ingress_cidr_blocks = var.private_alb_ingress_cidr_blocks + ingress_ipv6_cidr_blocks = var.private_alb_ingress_ipv6_cidr_blocks + ingress_security_group_ids = var.private_alb_ingress_security_group_ids # Access logs access_logs_enabled = var.private_alb_access_logs_enabled diff --git a/compute/ecs_cluster/rvn-ecs-cluster-definition.yml b/compute/ecs_cluster/rvn-ecs-cluster-definition.yml index a808060b..240e1079 100644 --- a/compute/ecs_cluster/rvn-ecs-cluster-definition.yml +++ b/compute/ecs_cluster/rvn-ecs-cluster-definition.yml @@ -3,8 +3,8 @@ definition: name: ECS Cluster description: Production-ready AWS ECS cluster with Fargate, Fargate Spot, optional EC2 capacity, and shared load balancers. release: - version: 0.5.0 - description: "BREAKING: Rename ec2_managed_termination_protection to ec2_managed_termination_protection_enabled and ec2_managed_scaling_status to ec2_managed_scaling_enabled; both inputs now accept booleans instead of ENABLED or DISABLED strings." + version: 0.5.2 + description: "Add private_alb_ingress_security_group_ids input (and public_alb_ingress_security_group_ids Terraform variable) to allow ALB ingress rules that reference source security groups, such as the CloudFront VPC origins security group." module: inputs: - id: network @@ -342,6 +342,15 @@ module: show_when: private_alb_enabled: true type: string_array + - add_button_label: Add security group + collapsible: true + description: Security groups whose members can access the private ALB. Useful for sources without static CIDRs, such as CloudFront VPC origins. + id: private_alb_ingress_security_group_ids + label: Ingress security groups + placeholder: sg-... + show_when: + private_alb_enabled: true + type: string_array - collapsible: true description: Idle timeout for the private ALB. id: private_alb_idle_timeout @@ -671,6 +680,7 @@ module: private_alb_idle_timeout: << module.input.private_alb_idle_timeout >> private_alb_ingress_cidr_blocks: << module.input.private_alb_ingress_cidr_blocks >> private_alb_ingress_ipv6_cidr_blocks: << module.input.private_alb_ingress_ipv6_cidr_blocks >> + private_alb_ingress_security_group_ids: << module.input.private_alb_ingress_security_group_ids >> private_alb_ssl_policy: << module.input.private_alb_ssl_policy >> private_nlb_access_logs_bucket_arn: << module.input.private_nlb_access_logs_bucket_arn >> private_nlb_elastic_ip_allocation_ids: << module.input.private_nlb_elastic_ip_allocation_ids >> diff --git a/compute/ecs_cluster/tests/basic.tftest.hcl b/compute/ecs_cluster/tests/basic.tftest.hcl index b1a84d2b..5a660463 100644 --- a/compute/ecs_cluster/tests/basic.tftest.hcl +++ b/compute/ecs_cluster/tests/basic.tftest.hcl @@ -747,6 +747,47 @@ run "private_alb_custom_settings" { } } +# Test 28b: Private ALB ingress rules referencing source security groups +run "private_alb_ingress_security_groups" { + command = plan + + variables { + private_alb_enabled = true + private_alb_https_enabled = true + private_alb_certificate_arns = ["arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012"] + private_alb_ingress_security_group_ids = ["sg-0123456789abcdef0"] + } + + assert { + condition = length([ + for rule in module.private_alb[0].module.security_group.aws_vpc_security_group_ingress_rule.this : rule + if rule.referenced_security_group_id == "sg-0123456789abcdef0" + ]) == 2 + error_message = "Private ALB should have HTTP and HTTPS ingress rules referencing the source security group" + } +} + +# Test 28c: Public ALB ingress rules referencing source security groups +run "public_alb_ingress_security_groups" { + command = plan + + variables { + public_alb_enabled = true + public_alb_https_enabled = true + public_alb_certificate_arns = ["arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012"] + public_subnet_ids = ["subnet-public1", "subnet-public2"] + public_alb_ingress_security_group_ids = ["sg-0123456789abcdef0"] + } + + assert { + condition = length([ + for rule in module.public_alb[0].module.security_group.aws_vpc_security_group_ingress_rule.this : rule + if rule.referenced_security_group_id == "sg-0123456789abcdef0" + ]) == 2 + error_message = "Public ALB should have HTTP and HTTPS ingress rules referencing the source security group" + } +} + ################################################################################ # Combined Configuration Tests ################################################################################ diff --git a/compute/ecs_cluster/variables.tf b/compute/ecs_cluster/variables.tf index eb7777ed..fff9d656 100644 --- a/compute/ecs_cluster/variables.tf +++ b/compute/ecs_cluster/variables.tf @@ -453,6 +453,17 @@ variable "public_alb_ingress_ipv6_cidr_blocks" { default = ["::/0"] } +variable "public_alb_ingress_security_group_ids" { + type = list(string) + description = "Security group IDs whose members are allowed to access the public ALB." + default = [] + + validation { + condition = alltrue([for sg in var.public_alb_ingress_security_group_ids : can(regex("^sg-", sg))]) + error_message = "All public_alb_ingress_security_group_ids must be valid security group IDs starting with 'sg-'." + } +} + variable "public_alb_access_logs_enabled" { type = bool description = "Enable access logging for the public ALB." @@ -542,6 +553,17 @@ variable "private_alb_ingress_ipv6_cidr_blocks" { default = [] } +variable "private_alb_ingress_security_group_ids" { + type = list(string) + description = "Security group IDs whose members are allowed to access the private ALB. Useful for sources without static CIDRs, such as CloudFront VPC origins." + default = [] + + validation { + condition = alltrue([for sg in var.private_alb_ingress_security_group_ids : can(regex("^sg-", sg))]) + error_message = "All private_alb_ingress_security_group_ids must be valid security group IDs starting with 'sg-'." + } +} + variable "private_alb_access_logs_enabled" { type = bool description = "Enable access logging for the private ALB." diff --git a/networking/alb/README.md b/networking/alb/README.md index d3bf1c0c..4ac36d19 100644 --- a/networking/alb/README.md +++ b/networking/alb/README.md @@ -291,6 +291,7 @@ spec: |------|-------------|------|---------|----------| | ingress_cidr_blocks | IPv4 CIDR blocks allowed to access the ALB | `list(string)` | `["0.0.0.0/0"]` | no | | ingress_ipv6_cidr_blocks | IPv6 CIDR blocks allowed to access the ALB | `list(string)` | `["::/0"]` | no | +| ingress_security_group_ids | Security group IDs whose members are allowed to access the ALB | `list(string)` | `[]` | no | ### Access Logs diff --git a/networking/alb/security_group.tf b/networking/alb/security_group.tf index 965ebb53..9b7ad9bf 100644 --- a/networking/alb/security_group.tf +++ b/networking/alb/security_group.tf @@ -53,6 +53,26 @@ module "security_group" { ip_protocol = "tcp" cidr_ipv6 = cidr } + ] : [], + # HTTP ingress (security groups) + local.create_http_listener ? [ + for sg in var.ingress_security_group_ids : { + description = "Allow HTTP traffic from security group ${sg}" + from_port = var.http_listener_port + to_port = var.http_listener_port + ip_protocol = "tcp" + referenced_security_group_id = sg + } + ] : [], + # HTTPS ingress (security groups) + local.create_https_listener ? [ + for sg in var.ingress_security_group_ids : { + description = "Allow HTTPS traffic from security group ${sg}" + from_port = var.https_listener_port + to_port = var.https_listener_port + ip_protocol = "tcp" + referenced_security_group_id = sg + } ] : [] ) } diff --git a/networking/alb/tests/basic.tftest.hcl b/networking/alb/tests/basic.tftest.hcl index fe3572d5..edb4c548 100644 --- a/networking/alb/tests/basic.tftest.hcl +++ b/networking/alb/tests/basic.tftest.hcl @@ -561,3 +561,43 @@ run "waf_fail_open" { error_message = "ALB should have WAF fail open enabled" } } + +# Test 29: Ingress rules referencing source security groups +run "ingress_security_groups" { + command = plan + + variables { + https_listener_enabled = true + certificate_arns = ["arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012"] + ingress_security_group_ids = ["sg-0123456789abcdef0"] + } + + assert { + condition = length([ + for rule in module.security_group.aws_vpc_security_group_ingress_rule.this : rule + if rule.referenced_security_group_id == "sg-0123456789abcdef0" + ]) == 2 + error_message = "Should create HTTP and HTTPS ingress rules referencing the source security group" + } + + assert { + condition = length([ + for rule in module.security_group.aws_vpc_security_group_ingress_rule.this : rule + if rule.referenced_security_group_id == "sg-0123456789abcdef0" && rule.from_port == 443 + ]) == 1 + error_message = "Should create an HTTPS ingress rule referencing the source security group" + } +} + +# Test 30: No security group referenced ingress rules by default +run "no_ingress_security_groups_by_default" { + command = plan + + assert { + condition = length([ + for rule in module.security_group.aws_vpc_security_group_ingress_rule.this : rule + if rule.referenced_security_group_id != null + ]) == 0 + error_message = "Should not create security group referenced ingress rules by default" + } +} diff --git a/networking/alb/tests/validation.tftest.hcl b/networking/alb/tests/validation.tftest.hcl new file mode 100644 index 00000000..2fbc6607 --- /dev/null +++ b/networking/alb/tests/validation.tftest.hcl @@ -0,0 +1,26 @@ +# ALB variable validation tests + +mock_provider "aws" { + override_resource { + target = aws_lb.this + values = { + arn = "arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/test-alb/1234567890123456" + } + } +} + +variables { + name = "test-alb" + vpc_id = "vpc-12345678" + subnet_ids = ["subnet-12345678", "subnet-87654321"] +} + +run "duplicate_ingress_security_groups" { + command = plan + + variables { + ingress_security_group_ids = ["sg-0123456789abcdef0", "sg-0123456789abcdef0"] + } + + expect_failures = [var.ingress_security_group_ids] +} diff --git a/networking/alb/variables.tf b/networking/alb/variables.tf index f06dd89a..8e204771 100644 --- a/networking/alb/variables.tf +++ b/networking/alb/variables.tf @@ -243,6 +243,17 @@ variable "ingress_ipv6_cidr_blocks" { default = null } +variable "ingress_security_group_ids" { + type = list(string) + description = "A list of security group IDs whose members are allowed to access the ALB. Ingress rules are created on each enabled listener port." + default = [] + + validation { + condition = length(distinct(var.ingress_security_group_ids)) == length(var.ingress_security_group_ids) && alltrue([for sg in var.ingress_security_group_ids : can(regex("^sg-", sg))]) + error_message = "All ingress_security_group_ids must be unique, valid security group IDs starting with 'sg-'." + } +} + ################################################################################ # Access Logs ################################################################################