diff --git a/pkg/cloud/services/securitygroup/securitygroups.go b/pkg/cloud/services/securitygroup/securitygroups.go index a7fcaa1b19..19d1ca6dfe 100644 --- a/pkg/cloud/services/securitygroup/securitygroups.go +++ b/pkg/cloud/services/securitygroup/securitygroups.go @@ -52,6 +52,10 @@ const ( // IPProtocolICMPv6 is how EC2 represents the ICMPv6 protocol in ingress rules. IPProtocolICMPv6 = "58" + + // AWSLoadBalancerControllerTagKey is a marker in rule descriptions that indicates + // the rule is managed by the AWS Load Balancer Controller and should be ignored by CAPA. + AWSLoadBalancerControllerTagKey = "elbv2.k8s.aws/targetGroupBinding=shared" ) // ReconcileSecurityGroups will reconcile security groups against the Service object. @@ -158,7 +162,10 @@ func (s *Service) ReconcileSecurityGroups() error { // skip rule reconciliation, as we expect the in-cluster cloud integration to manage them continue } - current := sg.IngressRules + + // Filter out rules managed by external controllers (e.g., AWS Load Balancer Controller) + // These rules should not be revoked by CAPA as they are managed by other components. + current := filterIgnoredIngressRules(sg.IngressRules) specRules, err := s.getSecurityGroupIngressRules(role) if err != nil { @@ -908,6 +915,19 @@ func ingressRulesFromSDKType(v *ec2.IpPermission) (res infrav1.IngressRules) { return res } +// filterIgnoredIngressRules removes ingress rules that should be ignored by CAPA +// reconciliation. Rules with specific markers in their description indicate they +// are managed by external controllers and should not be revoked. +func filterIgnoredIngressRules(rules infrav1.IngressRules) infrav1.IngressRules { + filtered := make(infrav1.IngressRules, 0, len(rules)) + for _, rule := range rules { + if !strings.Contains(rule.Description, AWSLoadBalancerControllerTagKey) { + filtered = append(filtered, rule) + } + } + return filtered +} + func ingressRuleFromSDKProtocol(v *ec2.IpPermission) infrav1.IngressRule { // Ports are only well-defined for TCP and UDP protocols, but EC2 overloads the port range // in the case of ICMP(v6) traffic to indicate which codes are allowed. For all other protocols, diff --git a/pkg/cloud/services/securitygroup/securitygroups_test.go b/pkg/cloud/services/securitygroup/securitygroups_test.go index 4f39ac2624..9e224617a3 100644 --- a/pkg/cloud/services/securitygroup/securitygroups_test.go +++ b/pkg/cloud/services/securitygroup/securitygroups_test.go @@ -1074,6 +1074,18 @@ func TestReconcileSecurityGroups(t *testing.T) { }, }, }, + // AWS Load Balancer Controller managed rule - should be ignored by CAPA + { + FromPort: aws.Int64(8080), + IpProtocol: aws.String("tcp"), + ToPort: aws.Int64(8080), + UserIdGroupPairs: []*ec2.UserIdGroupPair{ + { + Description: aws.String("elbv2.k8s.aws/targetGroupBinding=shared"), + GroupId: aws.String("sg-alb"), + }, + }, + }, }, } @@ -1105,6 +1117,11 @@ func TestReconcileSecurityGroups(t *testing.T) { }, })).Times(1) + // Ensure no revoke calls for sg-node to make sure we are filtering rules added by external controllers i.e., AWS LB Controller + m.RevokeSecurityGroupIngressWithContext(context.TODO(), gomock.Eq(&ec2.RevokeSecurityGroupIngressInput{ + GroupId: aws.String("sg-node"), + })).Times(0) + m.AuthorizeSecurityGroupIngressWithContext(context.TODO(), gomock.AssignableToTypeOf(&ec2.AuthorizeSecurityGroupIngressInput{ GroupId: aws.String("sg-bastion"), IpPermissions: []*ec2.IpPermission{