diff --git a/spinifex/handlers/elbv2/service_impl.go b/spinifex/handlers/elbv2/service_impl.go index 977e650bf..641e063f2 100644 --- a/spinifex/handlers/elbv2/service_impl.go +++ b/spinifex/handlers/elbv2/service_impl.go @@ -611,7 +611,7 @@ func (s *ELBv2ServiceImpl) CreateLoadBalancer(input *elbv2.CreateLoadBalancerInp vpcID := "" if s.VPCService != nil && len(subnets) > 0 { for _, subnetID := range subnets { - eniOut, eniErr := s.VPCService.CreateNetworkInterface(&ec2.CreateNetworkInterfaceInput{ + eniIn := &ec2.CreateNetworkInterfaceInput{ SubnetId: aws.String(subnetID), Description: aws.String(fmt.Sprintf("ELB %s/%s/%s", arnPathSegment, name, lbID)), TagSpecifications: []*ec2.TagSpecification{ @@ -623,7 +623,15 @@ func (s *ELBv2ServiceImpl) CreateLoadBalancer(input *elbv2.CreateLoadBalancerInp }, }, }, - }, accountID) + } + // SG enforcement gates inbound traffic by the ENI's port-group + // membership, which is set at ENI creation. Empty Groups + // intentionally falls back to the VPC default SG inside + // eni.CreateNetworkInterface. + if len(securityGroups) > 0 { + eniIn.Groups = aws.StringSlice(securityGroups) + } + eniOut, eniErr := s.VPCService.CreateNetworkInterface(eniIn, accountID) if eniErr != nil { // Rollback: delete any ENIs already created for _, rollbackENI := range eniIDs { diff --git a/spinifex/handlers/elbv2/service_impl_vpc_test.go b/spinifex/handlers/elbv2/service_impl_vpc_test.go index 105f475aa..97026a6e5 100644 --- a/spinifex/handlers/elbv2/service_impl_vpc_test.go +++ b/spinifex/handlers/elbv2/service_impl_vpc_test.go @@ -679,3 +679,84 @@ func TestCreateLoadBalancer_AMIResolverError_ReturnsServerInternal(t *testing.T) require.NoError(t, err) assert.Empty(t, lbs, "no LB record should be persisted when AMI resolver errors") } + +// TestCreateLoadBalancer_AttachesSpecifiedSecurityGroupsToENI verifies that +// when the caller supplies SecurityGroups on the LB input, the underlying ALB +// ENI is created with those SGs (not the VPC default SG). Without this the +// ENI's OVN port-group never gains the user's allow rules and inbound traffic +// is dropped by the SG enforcement default-deny ACL. +func TestCreateLoadBalancer_AttachesSpecifiedSecurityGroupsToENI(t *testing.T) { + svc, vpcSvc := setupTestServiceWithVPC(t) + + subnets, err := vpcSvc.DescribeSubnets(&ec2.DescribeSubnetsInput{}, testAccountID) + require.NoError(t, err) + require.NotEmpty(t, subnets.Subnets) + subnetID := *subnets.Subnets[0].SubnetId + vpcID := *subnets.Subnets[0].VpcId + + sgOut, err := vpcSvc.CreateSecurityGroup(&ec2.CreateSecurityGroupInput{ + GroupName: aws.String("alb-sg"), + Description: aws.String("ALB ingress"), + VpcId: aws.String(vpcID), + }, testAccountID) + require.NoError(t, err) + sgID := *sgOut.GroupId + + _, err = svc.CreateLoadBalancer(&elbv2.CreateLoadBalancerInput{ + Name: aws.String("sg-attach-alb"), + Subnets: []*string{aws.String(subnetID)}, + SecurityGroups: []*string{aws.String(sgID)}, + }, testAccountID) + require.NoError(t, err) + + eniDesc, err := vpcSvc.DescribeNetworkInterfaces(&ec2.DescribeNetworkInterfacesInput{}, testAccountID) + require.NoError(t, err) + + var albENI *ec2.NetworkInterface + for _, eni := range eniDesc.NetworkInterfaces { + if eni.RequesterManaged != nil && *eni.RequesterManaged { + albENI = eni + break + } + } + require.NotNil(t, albENI, "ELB-managed ENI must be created") + + require.Len(t, albENI.Groups, 1, "ALB ENI must join exactly the specified SG, not the default SG") + assert.Equal(t, sgID, *albENI.Groups[0].GroupId) +} + +// TestCreateLoadBalancer_NoSecurityGroupsFallsBackToDefault preserves the +// AWS-semantics fallback: an ALB created without SecurityGroups joins the +// VPC's default SG (so the default-deny ACL has at least one allow source). +func TestCreateLoadBalancer_NoSecurityGroupsFallsBackToDefault(t *testing.T) { + svc, vpcSvc := setupTestServiceWithVPC(t) + + subnets, err := vpcSvc.DescribeSubnets(&ec2.DescribeSubnetsInput{}, testAccountID) + require.NoError(t, err) + subnetID := *subnets.Subnets[0].SubnetId + vpcID := *subnets.Subnets[0].VpcId + + defaultSGID, err := vpcSvc.FindDefaultSGForVPC(testAccountID, vpcID) + require.NoError(t, err) + require.NotEmpty(t, defaultSGID, "VPC must have a default SG provisioned") + + _, err = svc.CreateLoadBalancer(&elbv2.CreateLoadBalancerInput{ + Name: aws.String("no-sg-alb"), + Subnets: []*string{aws.String(subnetID)}, + }, testAccountID) + require.NoError(t, err) + + eniDesc, err := vpcSvc.DescribeNetworkInterfaces(&ec2.DescribeNetworkInterfacesInput{}, testAccountID) + require.NoError(t, err) + + var albENI *ec2.NetworkInterface + for _, eni := range eniDesc.NetworkInterfaces { + if eni.RequesterManaged != nil && *eni.RequesterManaged { + albENI = eni + break + } + } + require.NotNil(t, albENI) + require.Len(t, albENI.Groups, 1) + assert.Equal(t, defaultSGID, *albENI.Groups[0].GroupId) +}