Repository: kubernetes-sigs/cluster-api-provider-aws
Pull request #4304 (2023-06-01 10:11:57)

Allow customization of ingress rules in control plane LB security group
**What type of PR is this?**

/kind feature

**What this PR does / why we need it**:

We would like to customize the ingress rules that are included in the security group created for the control plane Load Balancer. Currently, capa always adds an ingress rule to allow traffic towards the k8s api from everywhere (`0.0.0.0/0`). But we would like to have more control over this. We would like to only allow traffic coming from our VPNs.

With the changes in this PR, when no additional ingress rules are defined, we add the ingress rule to allow traffic from everywhere. That way we are backwards compatible. But when additional rules are defined, these will be used instead.

**Checklist**:
<!-- Put an "X" character inside the brackets of each completed task. Some may be optional depending on the PR in which case these can be deleted -->

- [X] squashed commits
- [ ] includes documentation
- [X] adds unit tests
- [ ] adds or updates e2e tests

**Release note**:
<!--  Write your release note:
1. Enter your extended release note in the below block. If the PR requires additional action from users switching to the new release, include the string "action required".
2. If no release note is required, just write "NONE".
-->
```release-note
Allow customization of ingress rules in control plane Load Balancer security group
```


k8s-ci-robot: Hi @fiunchinho. Thanks for your PR.

I'm waiting for a [kubernetes-sigs](https://github.com/orgs/kubernetes-sigs/people) member to verify that this patch is reasonable to test. If it is, they should reply with `/ok-to-test` on its own line. Until that is done, I will not automatically test new commits in this PR, but the usual testing commands by org members will still work. Regular contributors should [join the org](https://git.k8s.io/community/community-membership.md#member) to skip this step.

Once the patch is verified, the new status will be reflected by the `ok-to-test` label.

I understand the commands that are listed [here](https://go.k8s.io/bot-commands?repo=kubernetes-sigs%2Fcluster-api-provider-aws).

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository.
</details>

k8s-ci-robot: Welcome @fiunchinho! <br><br>It looks like this is your first PR to <a href='https://github.com/kubernetes-sigs/cluster-api-provider-aws'>kubernetes-sigs/cluster-api-provider-aws</a> 🎉. Please refer to our [pull request process documentation](https://git.k8s.io/community/contributors/guide/pull-requests.md) to help your PR have a smooth ride to approval. <br><br>You will be prompted by a bot to use commands during the review process. Do not be afraid to follow the prompts! It is okay to experiment. [Here is the bot commands documentation](https://go.k8s.io/bot-commands). <br><br>You can also check if kubernetes-sigs/cluster-api-provider-aws has [its own contribution guidelines](https://github.com/kubernetes-sigs/cluster-api-provider-aws/tree/master/CONTRIBUTING.md). <br><br>You may want to refer to our [testing guide](https://git.k8s.io/community/contributors/devel/sig-testing/testing.md) if you run into trouble with your tests not passing. <br><br>If you are having difficulty getting your pull request seen, please follow the [recommended escalation practices](https://github.com/kubernetes/community/blob/master/contributors/guide/pull-requests.md#why-is-my-pull-request-not-getting-reviewed). Also, for tips and tricks in the contribution process you may want to read the [Kubernetes contributor cheat sheet](https://git.k8s.io/community/contributors/guide/contributor-cheatsheet/README.md). We want to make sure your contribution gets all the attention it needs! <br><br>Thank you, and welcome to Kubernetes. :smiley:
k8s-ci-robot: [APPROVALNOTIFIER] This PR is **NOT APPROVED**

This pull-request has been approved by:
**Once this PR has been reviewed and has the lgtm label**, please assign richardcase for approval. For more information see [the Kubernetes Code Review Process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process).

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=kubernetes-sigs%2Fcluster-api-provider-aws).

<details open>
Needs approval from an approver in each of these files:

- **[OWNERS](https://github.com/kubernetes-sigs/cluster-api-provider-aws/blob/main/OWNERS)**

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":["richardcase"]} -->
fiunchinho: Adding @richardcase and @alexander-demicev as we talked about this in Slack.
alexander-demicev: /ok-to-test
alexander-demicev: /test pull-cluster-api-provider-aws-e2e-eks
/test pull-cluster-api-provider-aws-e2e
k8s-ci-robot: @fiunchinho: The following test **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-cluster-api-provider-aws-test | 256bacfd24fe6cf77c857ec45457e0a051c0a49a | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-aws/4304/pull-cluster-api-provider-aws-test/1664272709195927552) | true | `/test pull-cluster-api-provider-aws-test`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api-provider-aws&pr=4304). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Afiunchinho). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/cluster-api-provider-aws/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
fiunchinho: > Also, what about the IPv6 rules below?

I've changed the logic so that when no additional rules are defined and we want to use the default rules, we either use IPv6 or IPv4 depending on whether IPv6 is enabled or not.
fiunchinho: I pushed a new change to rename the field from `additionalIngressRules` to simply `IngressRules`. The name `CustomIngressRules` seem redundant to me, because we have many fields where we use a default when it's not set, and we don't call it `CustomSomething`. So `IngressRules` made sense to me. What do you think?
fiunchinho: /retest
fiunchinho: > Looks good to me, there is also a small piece of documentation that needs to be updated https://cluster-api-aws.sigs.k8s.io/topics/bring-your-own-aws-infrastructure.html?highlight=ingress#control-plane-load-balancer

Updated.
fiunchinho: /retest
/test pull-cluster-api-provider-aws-e2e-eks
/test pull-cluster-api-provider-aws-e2e
k8s-ci-robot: @fiunchinho: The following test **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-cluster-api-provider-aws-test | 9241ebc19f003e719cf546804e9058bba442e076 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-aws/4304/pull-cluster-api-provider-aws-test/1665719661422972928) | true | `/test pull-cluster-api-provider-aws-test`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api-provider-aws&pr=4304). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Afiunchinho). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/cluster-api-provider-aws/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
k8s-ci-robot: @fiunchinho: The following test **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-cluster-api-provider-aws-e2e-eks | a72a71dd3859b7af350c48be542e23fd15fa90c0 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-aws/4304/pull-cluster-api-provider-aws-e2e-eks/1665751856837562368) | false | `/test pull-cluster-api-provider-aws-e2e-eks`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api-provider-aws&pr=4304). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Afiunchinho). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/cluster-api-provider-aws/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
fiunchinho: /test pull-cluster-api-provider-aws-e2e-eks
AverageMarcus: /lgtm

Just needs approval from a maintainer :) 

/assign @richardcase @Ankitasw @Skarlso 

Skarlso: Looks okay except for that small refactor. :) Then it should be fine. :)

Patch:
diff --git a/Makefile b/Makefile
index b6918c0c9..6f58a1e14 100644
--- a/Makefile
+++ b/Makefile
@@ -602,7 +602,6 @@ release-binary: $(RELEASE_DIR) versions.mk build-toolchain ## Release binary
 		-v "$$(pwd):/workspace$(DOCKER_VOL_OPTS)" \
 		-w /workspace \
 		$(TOOLCHAIN_IMAGE) \
-		git config --global --add safe.directory /workspace; \
 		go build -ldflags '$(LDFLAGS) -extldflags "-static"' \
 		-o $(RELEASE_DIR)/$(notdir $(RELEASE_BINARY))-$(GOOS)-$(GOARCH)$(EXT) $(RELEASE_BINARY)
 
diff --git a/api/v1beta1/awscluster_conversion.go b/api/v1beta1/awscluster_conversion.go
index 616a58532..92f0e96a9 100644
--- a/api/v1beta1/awscluster_conversion.go
+++ b/api/v1beta1/awscluster_conversion.go
@@ -74,7 +74,7 @@ func restoreControlPlaneLoadBalancer(restored, dst *infrav2.AWSLoadBalancerSpec)
 	dst.LoadBalancerType = restored.LoadBalancerType
 	dst.DisableHostsRewrite = restored.DisableHostsRewrite
 	dst.PreserveClientIP = restored.PreserveClientIP
-	dst.AdditionalIngressRules = restored.AdditionalIngressRules
+	dst.IngressRules = restored.IngressRules
 }
 
 // ConvertFrom converts the v1beta1 AWSCluster receiver to a v1beta1 AWSCluster.
diff --git a/api/v1beta1/zz_generated.conversion.go b/api/v1beta1/zz_generated.conversion.go
index c04b82ab0..6a5e3effa 100644
--- a/api/v1beta1/zz_generated.conversion.go
+++ b/api/v1beta1/zz_generated.conversion.go
@@ -1219,7 +1219,7 @@ func autoConvert_v1beta2_AWSLoadBalancerSpec_To_v1beta1_AWSLoadBalancerSpec(in *
 	out.Subnets = *(*[]string)(unsafe.Pointer(&in.Subnets))
 	out.HealthCheckProtocol = (*ClassicELBProtocol)(unsafe.Pointer(in.HealthCheckProtocol))
 	out.AdditionalSecurityGroups = *(*[]string)(unsafe.Pointer(&in.AdditionalSecurityGroups))
-	// WARNING: in.AdditionalIngressRules requires manual conversion: does not exist in peer-type
+	// WARNING: in.IngressRules requires manual conversion: does not exist in peer-type
 	// WARNING: in.LoadBalancerType requires manual conversion: does not exist in peer-type
 	// WARNING: in.DisableHostsRewrite requires manual conversion: does not exist in peer-type
 	// WARNING: in.PreserveClientIP requires manual conversion: does not exist in peer-type
diff --git a/api/v1beta2/awscluster_types.go b/api/v1beta2/awscluster_types.go
index 203dafcc0..92c3e0fa9 100644
--- a/api/v1beta2/awscluster_types.go
+++ b/api/v1beta2/awscluster_types.go
@@ -208,10 +208,9 @@ type AWSLoadBalancerSpec struct {
 	// +optional
 	AdditionalSecurityGroups []string `json:"additionalSecurityGroups,omitempty"`
 
-	// AdditionalIngressRules sets the additional ingress rules for the control plane load balancer. If no source security group ids are specified, the
-	// default control plane security group will be used.
+	// IngressRules sets the ingress rules for the control plane load balancer.
 	// +optional
-	AdditionalIngressRules []IngressRule `json:"additionalIngressRules,omitempty"`
+	IngressRules []IngressRule `json:"ingressRules,omitempty"`
 
 	// LoadBalancerType sets the type for a load balancer. The default type is classic.
 	// +kubebuilder:default=classic
diff --git a/api/v1beta2/awscluster_webhook.go b/api/v1beta2/awscluster_webhook.go
index 0c7831ef5..51d35f18b 100644
--- a/api/v1beta2/awscluster_webhook.go
+++ b/api/v1beta2/awscluster_webhook.go
@@ -199,9 +199,9 @@ func (r *AWSCluster) validateAdditionalIngressRules() field.ErrorList {
 		return allErrs
 	}
 
-	for _, rule := range r.Spec.ControlPlaneLoadBalancer.AdditionalIngressRules {
+	for _, rule := range r.Spec.ControlPlaneLoadBalancer.IngressRules {
 		if (rule.CidrBlocks != nil || rule.IPv6CidrBlocks != nil) && (rule.SourceSecurityGroupIDs != nil || rule.SourceSecurityGroupRoles != nil) {
-			allErrs = append(allErrs, field.Invalid(field.NewPath("additionalIngressRules"), r.Spec.ControlPlaneLoadBalancer.AdditionalIngressRules, "CIDR blocks and security group IDs or security group roles cannot be used together"))
+			allErrs = append(allErrs, field.Invalid(field.NewPath("additionalIngressRules"), r.Spec.ControlPlaneLoadBalancer.IngressRules, "CIDR blocks and security group IDs or security group roles cannot be used together"))
 		}
 	}
 
diff --git a/api/v1beta2/zz_generated.deepcopy.go b/api/v1beta2/zz_generated.deepcopy.go
index 9d14c7a8c..80cb1064f 100644
--- a/api/v1beta2/zz_generated.deepcopy.go
+++ b/api/v1beta2/zz_generated.deepcopy.go
@@ -574,8 +574,8 @@ func (in *AWSLoadBalancerSpec) DeepCopyInto(out *AWSLoadBalancerSpec) {
 		*out = make([]string, len(*in))
 		copy(*out, *in)
 	}
-	if in.AdditionalIngressRules != nil {
-		in, out := &in.AdditionalIngressRules, &out.AdditionalIngressRules
+	if in.IngressRules != nil {
+		in, out := &in.IngressRules, &out.IngressRules
 		*out = make([]IngressRule, len(*in))
 		for i := range *in {
 			(*in)[i].DeepCopyInto(&(*out)[i])
diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsclusters.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsclusters.yaml
index bb02f3d90..609fad733 100644
--- a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsclusters.yaml
+++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsclusters.yaml
@@ -966,11 +966,43 @@ spec:
                 description: ControlPlaneLoadBalancer is optional configuration for
                   customizing control plane behavior.
                 properties:
-                  additionalIngressRules:
-                    description: AdditionalIngressRules sets the additional ingress
-                      rules for the control plane load balancer. If no source security
-                      group ids are specified, the default control plane security
-                      group will be used.
+                  additionalSecurityGroups:
+                    description: AdditionalSecurityGroups sets the security groups
+                      used by the load balancer. Expected to be security group IDs
+                      This is optional - if not provided new security groups will
+                      be created for the load balancer
+                    items:
+                      type: string
+                    type: array
+                  crossZoneLoadBalancing:
+                    description: "CrossZoneLoadBalancing enables the classic ELB cross
+                      availability zone balancing. \n With cross-zone load balancing,
+                      each load balancer node for your Classic Load Balancer distributes
+                      requests evenly across the registered instances in all enabled
+                      Availability Zones. If cross-zone load balancing is disabled,
+                      each load balancer node distributes requests evenly across the
+                      registered instances in its Availability Zone only. \n Defaults
+                      to false."
+                    type: boolean
+                  disableHostsRewrite:
+                    description: DisableHostsRewrite disabled the hair pinning issue
+                      solution that adds the NLB's address as 127.0.0.1 to the hosts
+                      file of each instance. This is by default, false.
+                    type: boolean
+                  healthCheckProtocol:
+                    description: HealthCheckProtocol sets the protocol type for ELB
+                      health check target default value is ELBProtocolSSL
+                    enum:
+                    - TCP
+                    - SSL
+                    - HTTP
+                    - HTTPS
+                    - TLS
+                    - UDP
+                    type: string
+                  ingressRules:
+                    description: IngressRules sets the ingress rules for the control
+                      plane load balancer.
                     items:
                       description: IngressRule defines an AWS ingress rule for security
                         groups.
@@ -1040,40 +1072,6 @@ spec:
                       - toPort
                       type: object
                     type: array
-                  additionalSecurityGroups:
-                    description: AdditionalSecurityGroups sets the security groups
-                      used by the load balancer. Expected to be security group IDs
-                      This is optional - if not provided new security groups will
-                      be created for the load balancer
-                    items:
-                      type: string
-                    type: array
-                  crossZoneLoadBalancing:
-                    description: "CrossZoneLoadBalancing enables the classic ELB cross
-                      availability zone balancing. \n With cross-zone load balancing,
-                      each load balancer node for your Classic Load Balancer distributes
-                      requests evenly across the registered instances in all enabled
-                      Availability Zones. If cross-zone load balancing is disabled,
-                      each load balancer node distributes requests evenly across the
-                      registered instances in its Availability Zone only. \n Defaults
-                      to false."
-                    type: boolean
-                  disableHostsRewrite:
-                    description: DisableHostsRewrite disabled the hair pinning issue
-                      solution that adds the NLB's address as 127.0.0.1 to the hosts
-                      file of each instance. This is by default, false.
-                    type: boolean
-                  healthCheckProtocol:
-                    description: HealthCheckProtocol sets the protocol type for ELB
-                      health check target default value is ELBProtocolSSL
-                    enum:
-                    - TCP
-                    - SSL
-                    - HTTP
-                    - HTTPS
-                    - TLS
-                    - UDP
-                    type: string
                   loadBalancerType:
                     default: classic
                     description: LoadBalancerType sets the type for a load balancer.
diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsclustertemplates.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsclustertemplates.yaml
index cc410f814..66f3927be 100644
--- a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsclustertemplates.yaml
+++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsclustertemplates.yaml
@@ -559,11 +559,45 @@ spec:
                         description: ControlPlaneLoadBalancer is optional configuration
                           for customizing control plane behavior.
                         properties:
-                          additionalIngressRules:
-                            description: AdditionalIngressRules sets the additional
-                              ingress rules for the control plane load balancer. If
-                              no source security group ids are specified, the default
-                              control plane security group will be used.
+                          additionalSecurityGroups:
+                            description: AdditionalSecurityGroups sets the security
+                              groups used by the load balancer. Expected to be security
+                              group IDs This is optional - if not provided new security
+                              groups will be created for the load balancer
+                            items:
+                              type: string
+                            type: array
+                          crossZoneLoadBalancing:
+                            description: "CrossZoneLoadBalancing enables the classic
+                              ELB cross availability zone balancing. \n With cross-zone
+                              load balancing, each load balancer node for your Classic
+                              Load Balancer distributes requests evenly across the
+                              registered instances in all enabled Availability Zones.
+                              If cross-zone load balancing is disabled, each load
+                              balancer node distributes requests evenly across the
+                              registered instances in its Availability Zone only.
+                              \n Defaults to false."
+                            type: boolean
+                          disableHostsRewrite:
+                            description: DisableHostsRewrite disabled the hair pinning
+                              issue solution that adds the NLB's address as 127.0.0.1
+                              to the hosts file of each instance. This is by default,
+                              false.
+                            type: boolean
+                          healthCheckProtocol:
+                            description: HealthCheckProtocol sets the protocol type
+                              for ELB health check target default value is ELBProtocolSSL
+                            enum:
+                            - TCP
+                            - SSL
+                            - HTTP
+                            - HTTPS
+                            - TLS
+                            - UDP
+                            type: string
+                          ingressRules:
+                            description: IngressRules sets the ingress rules for the
+                              control plane load balancer.
                             items:
                               description: IngressRule defines an AWS ingress rule
                                 for security groups.
@@ -634,42 +668,6 @@ spec:
                               - toPort
                               type: object
                             type: array
-                          additionalSecurityGroups:
-                            description: AdditionalSecurityGroups sets the security
-                              groups used by the load balancer. Expected to be security
-                              group IDs This is optional - if not provided new security
-                              groups will be created for the load balancer
-                            items:
-                              type: string
-                            type: array
-                          crossZoneLoadBalancing:
-                            description: "CrossZoneLoadBalancing enables the classic
-                              ELB cross availability zone balancing. \n With cross-zone
-                              load balancing, each load balancer node for your Classic
-                              Load Balancer distributes requests evenly across the
-                              registered instances in all enabled Availability Zones.
-                              If cross-zone load balancing is disabled, each load
-                              balancer node distributes requests evenly across the
-                              registered instances in its Availability Zone only.
-                              \n Defaults to false."
-                            type: boolean
-                          disableHostsRewrite:
-                            description: DisableHostsRewrite disabled the hair pinning
-                              issue solution that adds the NLB's address as 127.0.0.1
-                              to the hosts file of each instance. This is by default,
-                              false.
-                            type: boolean
-                          healthCheckProtocol:
-                            description: HealthCheckProtocol sets the protocol type
-                              for ELB health check target default value is ELBProtocolSSL
-                            enum:
-                            - TCP
-                            - SSL
-                            - HTTP
-                            - HTTPS
-                            - TLS
-                            - UDP
-                            type: string
                           loadBalancerType:
                             default: classic
                             description: LoadBalancerType sets the type for a load
diff --git a/docs/book/src/topics/bring-your-own-aws-infrastructure.md b/docs/book/src/topics/bring-your-own-aws-infrastructure.md
index cf3237a3d..51b4e5237 100644
--- a/docs/book/src/topics/bring-your-own-aws-infrastructure.md
+++ b/docs/book/src/topics/bring-your-own-aws-infrastructure.md
@@ -163,11 +163,12 @@ It's also possible to specify custom ingress rules for the control plane load ba
 
 ```yaml
 spec:
-  additionalIngressRules:
-    - description: "example ingress rule"
-      protocol: "-1" # all
-      fromPort: 7777
-      toPort: 7777
+  controlPlaneLoadBalancer:
+    ingressRules:
+      - description: "example ingress rule"
+        protocol: "-1" # all
+        fromPort: 7777
+        toPort: 7777
 ```
 
 > **WARNING:** Using an existing Classic ELB is an advanced feature. **If you use an existing Classic ELB, you must correctly configure it, and attach subnets to it.**
diff --git a/go.mod b/go.mod
index 1dfc7da67..cba1047b7 100644
--- a/go.mod
+++ b/go.mod
@@ -7,7 +7,7 @@ replace sigs.k8s.io/cluster-api => sigs.k8s.io/cluster-api v1.4.2
 require (
 	github.com/alessio/shellescape v1.4.1
 	github.com/apparentlymart/go-cidr v1.1.0
-	github.com/aws/amazon-vpc-cni-k8s v1.13.0
+	github.com/aws/amazon-vpc-cni-k8s v1.12.5
 	github.com/aws/aws-lambda-go v1.41.0
 	github.com/aws/aws-sdk-go v1.44.213
 	github.com/awslabs/goformation/v4 v4.19.5
@@ -32,7 +32,7 @@ require (
 	k8s.io/api v0.26.1
 	k8s.io/apiextensions-apiserver v0.26.1
 	k8s.io/apimachinery v0.26.1
-	k8s.io/cli-runtime v0.26.0
+	k8s.io/cli-runtime v0.25.5
 	k8s.io/client-go v0.26.1
 	k8s.io/component-base v0.26.1
 	k8s.io/klog/v2 v2.100.1
@@ -47,7 +47,7 @@ require (
 
 require (
 	github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
-	github.com/BurntSushi/toml v1.2.1 // indirect
+	github.com/BurntSushi/toml v1.1.0 // indirect
 	github.com/MakeNowJust/heredoc v1.0.0 // indirect
 	github.com/Masterminds/goutils v1.1.1 // indirect
 	github.com/Masterminds/semver/v3 v3.2.0 // indirect
@@ -69,7 +69,7 @@ require (
 	github.com/docker/go-connections v0.4.0 // indirect
 	github.com/docker/go-units v0.4.0 // indirect
 	github.com/drone/envsubst/v2 v2.0.0-20210730161058-179042472c46 // indirect
-	github.com/emicklei/go-restful/v3 v3.10.1 // indirect
+	github.com/emicklei/go-restful/v3 v3.9.0 // indirect
 	github.com/evanphx/json-patch v5.6.0+incompatible // indirect
 	github.com/evanphx/json-patch/v5 v5.6.0 // indirect
 	github.com/fatih/color v1.15.0 // indirect
@@ -111,24 +111,25 @@ require (
 	github.com/mitchellh/mapstructure v1.5.0 // indirect
 	github.com/mitchellh/reflectwalk v1.0.2 // indirect
 	github.com/moby/spdystream v0.2.0 // indirect
-	github.com/moby/term v0.0.0-20221205130635-1aeaba878587 // indirect
+	github.com/moby/term v0.0.0-20220808134915-39b0c02b01ae // indirect
 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
 	github.com/modern-go/reflect2 v1.0.2 // indirect
 	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
 	github.com/olekukonko/tablewriter v0.0.5 // indirect
 	github.com/opencontainers/go-digest v1.0.0 // indirect
-	github.com/opencontainers/image-spec v1.1.0-rc2 // indirect
+	github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 // indirect
 	github.com/pelletier/go-toml v1.9.5 // indirect
 	github.com/pelletier/go-toml/v2 v2.0.6 // indirect
 	github.com/prometheus/client_model v0.3.0 // indirect
 	github.com/prometheus/common v0.42.0 // indirect
 	github.com/prometheus/procfs v0.9.0 // indirect
 	github.com/rivo/uniseg v0.4.2 // indirect
+	github.com/russross/blackfriday v1.5.2 // indirect
 	github.com/russross/blackfriday/v2 v2.1.0 // indirect
 	github.com/sanathkr/go-yaml v0.0.0-20170819195128-ed9d249f429b // indirect
 	github.com/sanathkr/yaml v0.0.0-20170819201035-0056894fa522 // indirect
 	github.com/shopspring/decimal v1.3.1 // indirect
-	github.com/sirupsen/logrus v1.9.0 // indirect
+	github.com/sirupsen/logrus v1.8.1 // indirect
 	github.com/spf13/afero v1.9.3 // indirect
 	github.com/spf13/cast v1.5.0 // indirect
 	github.com/spf13/jwalterweatherman v1.1.0 // indirect
@@ -157,8 +158,8 @@ require (
 	k8s.io/apiserver v0.26.1 // indirect
 	k8s.io/cluster-bootstrap v0.25.0 // indirect
 	k8s.io/kube-openapi v0.0.0-20230109183929-3758b55a6596 // indirect
-	k8s.io/kubectl v0.26.0 // indirect
-	sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
+	k8s.io/kubectl v0.25.2 // indirect
+	sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect
 	sigs.k8s.io/kind v0.18.0 // indirect
 	sigs.k8s.io/kustomize/kyaml v0.14.2 // indirect
 	sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
diff --git a/go.sum b/go.sum
index 2d9783866..2f22e7cf0 100644
--- a/go.sum
+++ b/go.sum
@@ -41,8 +41,8 @@ github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOEl
 github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/BurntSushi/toml v1.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
-github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
-github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
+github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I=
+github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
 github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
 github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
@@ -71,8 +71,8 @@ github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj
 github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
 github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 h1:4daAzAu0S6Vi7/lbWECcX0j45yZReDZ56BQsrVBOEEY=
 github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=
-github.com/aws/amazon-vpc-cni-k8s v1.13.0 h1:Phuy+6mOo4w6aHG2gl0RVl2m8k+2FmYqTSa8qsn04zg=
-github.com/aws/amazon-vpc-cni-k8s v1.13.0/go.mod h1:K4vG8QceHMeMR0j1sRAMc4RaXCVXpxlgpbXXp13QA9o=
+github.com/aws/amazon-vpc-cni-k8s v1.12.5 h1:dlJ7kjboSz74LytRps1QlCVkcwNGqRdSKmvy5zalDY4=
+github.com/aws/amazon-vpc-cni-k8s v1.12.5/go.mod h1:prNfjtqkRPC5gJfgemME58SyrJa3qo5O+yZgJPWtaaA=
 github.com/aws/aws-lambda-go v1.41.0 h1:l/5fyVb6Ud9uYd411xdHZzSf2n86TakxzpvIoz7l+3Y=
 github.com/aws/aws-lambda-go v1.41.0/go.mod h1:jwFe2KmMsHmffA1X2R09hH6lFzJQxzI8qK17ewzbQMM=
 github.com/aws/aws-sdk-go v1.8.39/go.mod h1:ZRmQr0FajVIyZ4ZzBYKG5P3ZqPz9IHG41ZoMu1ADI3k=
@@ -124,7 +124,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t
 github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
 github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
 github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
-github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
+github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw=
+github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -142,8 +143,8 @@ github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3
 github.com/drone/envsubst/v2 v2.0.0-20210730161058-179042472c46 h1:7QPwrLT79GlD5sizHf27aoY2RTvw62mO6x7mxkScNk0=
 github.com/drone/envsubst/v2 v2.0.0-20210730161058-179042472c46/go.mod h1:esf2rsHFNlZlxsqsZDojNBcnNs5REqIvRrWRHqX0vEU=
 github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153 h1:yUdfgN0XgIJw7foRItutHYUIhlcKzcSf5vDpdhQAKTc=
-github.com/emicklei/go-restful/v3 v3.10.1 h1:rc42Y5YTp7Am7CS630D7JmhRjq4UlEUuEKfrDac4bSQ=
-github.com/emicklei/go-restful/v3 v3.10.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
+github.com/emicklei/go-restful/v3 v3.9.0 h1:XwGDlfxEnQZzuopoqxwSEllNcCOM9DhhFyhFIIGKwxE=
+github.com/emicklei/go-restful/v3 v3.9.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
 github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
@@ -404,8 +405,8 @@ github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zx
 github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
 github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8=
 github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c=
-github.com/moby/term v0.0.0-20221205130635-1aeaba878587 h1:HfkjXDfhgVaN5rmueG8cL8KKeFNecRCXFhaJ2qZ5SKA=
-github.com/moby/term v0.0.0-20221205130635-1aeaba878587/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
+github.com/moby/term v0.0.0-20220808134915-39b0c02b01ae h1:O4SWKdcHVCvYqyDV+9CJA1fcDN2L11Bule0iFy3YlAI=
+github.com/moby/term v0.0.0-20220808134915-39b0c02b01ae/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw=
 github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -436,8 +437,8 @@ github.com/onsi/gomega v1.27.7 h1:fVih9JD6ogIiHUN6ePK7HJidyEDpWGVB5mzM7cWNXoU=
 github.com/onsi/gomega v1.27.7/go.mod h1:1p8OOlwo2iUUDsHnOrjE5UKYJ+e3W8eQ3qSlRahPmr4=
 github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
-github.com/opencontainers/image-spec v1.1.0-rc2 h1:2zx/Stx4Wc5pIPDvIxHXvXtQFW/7XWJGmnM7r3wg034=
-github.com/opencontainers/image-spec v1.1.0-rc2/go.mod h1:3OVijpioIKYWTqjiG0zfF6wvoJ4fAXGbjdZuI2NgsRQ=
+github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 h1:rc3tiVYb5z54aKaDfakKn0dDjIyPpTtszkjuMzyt7ec=
+github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
 github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
 github.com/pborman/uuid v0.0.0-20170612153648-e790cca94e6c/go.mod h1:VyrYX9gd7irzKovcSS6BIIEwPRkP2Wm2m9ufcdFSJ34=
 github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
@@ -480,6 +481,8 @@ github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6So
 github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
 github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
+github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
+github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
 github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
@@ -499,8 +502,8 @@ github.com/sigma/bdoor v0.0.0-20160202064022-babf2a4017b0/go.mod h1:WBu7REWbxC/s
 github.com/sigma/vmw-guestinfo v0.0.0-20160204083807-95dd4126d6e8/go.mod h1:JrRFFC0veyh0cibh0DAhriSY7/gV3kDdNaVUOmfx01U=
 github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
 github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
-github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
-github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
+github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
+github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
 github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
 github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
 github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
@@ -760,7 +763,6 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -804,6 +806,7 @@ golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBn
 golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
 golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
 golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
 golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
 golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
@@ -990,6 +993,7 @@ gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C
 gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
 gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0=
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
@@ -1006,8 +1010,8 @@ k8s.io/apimachinery v0.26.1 h1:8EZ/eGJL+hY/MYCNwhmDzVqq2lPl3N3Bo8rvweJwXUQ=
 k8s.io/apimachinery v0.26.1/go.mod h1:tnPmbONNJ7ByJNz9+n9kMjNP8ON+1qoAIIC70lztu74=
 k8s.io/apiserver v0.26.1 h1:6vmnAqCDO194SVCPU3MU8NcDgSqsUA62tBUSWrFXhsc=
 k8s.io/apiserver v0.26.1/go.mod h1:wr75z634Cv+sifswE9HlAo5FQ7UoUauIICRlOE+5dCg=
-k8s.io/cli-runtime v0.26.0 h1:aQHa1SyUhpqxAw1fY21x2z2OS5RLtMJOCj7tN4oq8mw=
-k8s.io/cli-runtime v0.26.0/go.mod h1:o+4KmwHzO/UK0wepE1qpRk6l3o60/txUZ1fEXWGIKTY=
+k8s.io/cli-runtime v0.25.5 h1:5Q37ITYtPtSw2JQcN6EBsdOQBnGvvo/D1g93Da4ceYI=
+k8s.io/cli-runtime v0.25.5/go.mod h1:o7lT2rFyfbLrQOzTFsV828OyxKsTE/FmVc3ag1nx0IU=
 k8s.io/client-go v0.26.1 h1:87CXzYJnAMGaa/IDDfRdhTzxk/wzGZ+/HUQpqgVSZXU=
 k8s.io/client-go v0.26.1/go.mod h1:IWNSglg+rQ3OcvDkhY6+QLeasV4OYHDjdqeWkDQZwGE=
 k8s.io/cluster-bootstrap v0.25.0 h1:KJ2/r0dV+bLfTK5EBobAVKvjGel3N4Qqh3bvnzh9qPk=
@@ -1018,8 +1022,8 @@ k8s.io/klog/v2 v2.100.1 h1:7WCHKK6K8fNhTqfBhISHQ97KrnJNFZMcQvKp7gP/tmg=
 k8s.io/klog/v2 v2.100.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=
 k8s.io/kube-openapi v0.0.0-20230109183929-3758b55a6596 h1:8cNCQs+WqqnSpZ7y0LMQPKD+RZUHU17VqLPMW3qxnxc=
 k8s.io/kube-openapi v0.0.0-20230109183929-3758b55a6596/go.mod h1:/BYxry62FuDzmI+i9B+X2pqfySRmSOW2ARmj5Zbqhj0=
-k8s.io/kubectl v0.26.0 h1:xmrzoKR9CyNdzxBmXV7jW9Ln8WMrwRK6hGbbf69o4T0=
-k8s.io/kubectl v0.26.0/go.mod h1:eInP0b+U9XUJWSYeU9XZnTA+cVYuWyl3iYPGtru0qhQ=
+k8s.io/kubectl v0.25.2 h1:2993lTeVimxKSWx/7z2PiJxUILygRa3tmC4QhFaeioA=
+k8s.io/kubectl v0.25.2/go.mod h1:eoBGJtKUj7x38KXelz+dqVtbtbKwCqyKzJWmBHU0prg=
 k8s.io/utils v0.0.0-20221128185143-99ec85e7a448 h1:KTgPnR10d5zhztWptI952TNtt/4u5h3IzDXkdIMuo2Y=
 k8s.io/utils v0.0.0-20221128185143-99ec85e7a448/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
 rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
@@ -1033,8 +1037,8 @@ sigs.k8s.io/cluster-api/test v1.4.2 h1:uHFtn0SFOFOxIbdahLoYo4kz84yLqCmhbVLV4vsk1
 sigs.k8s.io/cluster-api/test v1.4.2/go.mod h1:/64ycj3YFMW1BGVtCtfwmlVAXGN0DFTZEkIClh68Svo=
 sigs.k8s.io/controller-runtime v0.14.5 h1:6xaWFqzT5KuAQ9ufgUaj1G/+C4Y1GRkhrxl+BJ9i+5s=
 sigs.k8s.io/controller-runtime v0.14.5/go.mod h1:WqIdsAY6JBsjfc/CqO0CORmNtoCtE4S6qbPc9s68h+0=
-sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
-sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
+sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 h1:iXTIw73aPyC+oRdyqqvVJuloN1p0AC/kzH07hu3NE+k=
+sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
 sigs.k8s.io/kind v0.18.0 h1:ahgZdVV1pdhXlYe1f+ztISakT23KdrBl/NFY9JMygzs=
 sigs.k8s.io/kind v0.18.0/go.mod h1:Qqp8AiwOlMZmJWs37Hgs31xcbiYXjtXlRBSftcnZXQk=
 sigs.k8s.io/kustomize/api v0.13.4 h1:E38Hfx0G9R9v7vRgKshviPotJQETG0S2gD3JdHLCAsI=
diff --git a/hack/tools/go.mod b/hack/tools/go.mod
index df22df169..4f9c40527 100644
--- a/hack/tools/go.mod
+++ b/hack/tools/go.mod
@@ -6,7 +6,7 @@ require (
 	github.com/a8m/envsubst v1.4.2
 	github.com/ahmetb/gen-crd-api-reference-docs v0.3.0
 	github.com/golang/mock v1.6.0
-	github.com/itchyny/gojq v0.12.13
+	github.com/itchyny/gojq v0.12.12
 	github.com/joelanford/go-apidiff v0.6.0
 	github.com/mikefarah/yq/v4 v4.34.1
 	github.com/spf13/pflag v1.0.5
@@ -204,7 +204,7 @@ require (
 	github.com/mailru/easyjson v0.7.7 // indirect
 	github.com/matryer/is v1.4.0 // indirect
 	github.com/mattn/go-colorable v0.1.13 // indirect
-	github.com/mattn/go-isatty v0.0.19 // indirect
+	github.com/mattn/go-isatty v0.0.17 // indirect
 	github.com/mattn/go-runewidth v0.0.14 // indirect
 	github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
 	github.com/miekg/pkcs11 v1.1.1 // indirect
diff --git a/hack/tools/go.sum b/hack/tools/go.sum
index 98e5c5772..bd306838d 100644
--- a/hack/tools/go.sum
+++ b/hack/tools/go.sum
@@ -910,8 +910,8 @@ github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANyt
 github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
 github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
-github.com/itchyny/gojq v0.12.13 h1:IxyYlHYIlspQHHTE0f3cJF0NKDMfajxViuhBLnHd/QU=
-github.com/itchyny/gojq v0.12.13/go.mod h1:JzwzAqenfhrPUuwbmEz3nu3JQmFLlQTQMUcOdnu/Sf4=
+github.com/itchyny/gojq v0.12.12 h1:x+xGI9BXqKoJQZkr95ibpe3cdrTbY8D9lonrK433rcA=
+github.com/itchyny/gojq v0.12.12/go.mod h1:j+3sVkjxwd7A7Z5jrbKibgOLn0ZfLWkV+Awxr/pyzJE=
 github.com/itchyny/timefmt-go v0.1.5 h1:G0INE2la8S6ru/ZI5JecgyzbbJNs5lG1RcBqa7Jm6GE=
 github.com/itchyny/timefmt-go v0.1.5/go.mod h1:nEP7L+2YmAbT2kZ2HfSs1d8Xtw9LY8D2stDBckWakZ8=
 github.com/jarcoal/httpmock v1.0.5/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik=
@@ -1037,8 +1037,8 @@ github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd
 github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
 github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
 github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
-github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
-github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
+github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
 github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
 github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
 github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
@@ -1902,7 +1902,6 @@ golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
 golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
diff --git a/pkg/cloud/services/securitygroup/securitygroups.go b/pkg/cloud/services/securitygroup/securitygroups.go
index f4a760c3a..451fb0ac4 100644
--- a/pkg/cloud/services/securitygroup/securitygroups.go
+++ b/pkg/cloud/services/securitygroup/securitygroups.go
@@ -520,18 +520,18 @@ func (s *Service) getSecurityGroupIngressRules(role infrav1.SecurityGroupRole) (
 			rules = append(rules, s.defaultSSHIngressRule(s.scope.SecurityGroups()[infrav1.SecurityGroupBastion].ID))
 		}
 		if s.scope.ControlPlaneLoadBalancer() != nil {
-			additionalIngressRules := s.scope.ControlPlaneLoadBalancer().AdditionalIngressRules
-			for i := range additionalIngressRules {
-				if additionalIngressRules[i].SourceSecurityGroupIDs == nil && additionalIngressRules[i].SourceSecurityGroupRoles == nil { // if the rule doesn't have a source security group, use the control plane security group
-					additionalIngressRules[i].SourceSecurityGroupIDs = []string{s.scope.SecurityGroups()[infrav1.SecurityGroupControlPlane].ID}
+			ingressRules := s.scope.ControlPlaneLoadBalancer().IngressRules
+			for i := range ingressRules {
+				if ingressRules[i].SourceSecurityGroupIDs == nil && ingressRules[i].SourceSecurityGroupRoles == nil { // if the rule doesn't have a source security group, use the control plane security group
+					ingressRules[i].SourceSecurityGroupIDs = []string{s.scope.SecurityGroups()[infrav1.SecurityGroupControlPlane].ID}
 					continue
 				}
 
-				for _, sourceSGRole := range additionalIngressRules[i].SourceSecurityGroupRoles {
-					additionalIngressRules[i].SourceSecurityGroupIDs = append(additionalIngressRules[i].SourceSecurityGroupIDs, s.scope.SecurityGroups()[sourceSGRole].ID)
+				for _, sourceSGRole := range ingressRules[i].SourceSecurityGroupRoles {
+					ingressRules[i].SourceSecurityGroupIDs = append(ingressRules[i].SourceSecurityGroupIDs, s.scope.SecurityGroups()[sourceSGRole].ID)
 				}
 			}
-			rules = append(rules, s.scope.ControlPlaneLoadBalancer().AdditionalIngressRules...)
+			rules = append(rules, s.scope.ControlPlaneLoadBalancer().IngressRules...)
 		}
 		return append(cniRules, rules...), nil
 
@@ -577,23 +577,9 @@ func (s *Service) getSecurityGroupIngressRules(role infrav1.SecurityGroupRole) (
 		}
 		return infrav1.IngressRules{}, nil
 	case infrav1.SecurityGroupAPIServerLB:
-		rules := infrav1.IngressRules{
-			{
-				Description: "Kubernetes API",
-				Protocol:    infrav1.SecurityGroupProtocolTCP,
-				FromPort:    int64(s.scope.APIServerPort()),
-				ToPort:      int64(s.scope.APIServerPort()),
-				CidrBlocks:  cidrBlocks,
-			},
-		}
-		if s.scope.VPC().IsIPv6Enabled() {
-			rules = append(rules, infrav1.IngressRule{
-				Description:    "Kubernetes API IPv6",
-				Protocol:       infrav1.SecurityGroupProtocolTCP,
-				FromPort:       int64(s.scope.APIServerPort()),
-				ToPort:         int64(s.scope.APIServerPort()),
-				IPv6CidrBlocks: []string{services.AnyIPv6CidrBlock},
-			})
+		rules := s.getDefaultIngressRulesForControlPlaneLB()
+		if s.scope.ControlPlaneLoadBalancer() != nil && len(s.scope.ControlPlaneLoadBalancer().IngressRules) > 0 {
+			rules = s.scope.ControlPlaneLoadBalancer().IngressRules
 		}
 		return rules, nil
 	case infrav1.SecurityGroupLB:
@@ -799,3 +785,27 @@ func ingressRulesFromSDKType(v *ec2.IpPermission) (res infrav1.IngressRules) {
 
 	return res
 }
+
+func (s *Service) getDefaultIngressRulesForControlPlaneLB() infrav1.IngressRules {
+	if s.scope.VPC().IsIPv6Enabled() {
+		return infrav1.IngressRules{
+			{
+				Description:    "Kubernetes API IPv6",
+				Protocol:       infrav1.SecurityGroupProtocolTCP,
+				FromPort:       int64(s.scope.APIServerPort()),
+				ToPort:         int64(s.scope.APIServerPort()),
+				IPv6CidrBlocks: []string{services.AnyIPv6CidrBlock},
+			},
+		}
+	}
+
+	return infrav1.IngressRules{
+		{
+			Description: "Kubernetes API",
+			Protocol:    infrav1.SecurityGroupProtocolTCP,
+			FromPort:    int64(s.scope.APIServerPort()),
+			ToPort:      int64(s.scope.APIServerPort()),
+			CidrBlocks:  []string{services.AnyIPv4CidrBlock},
+		},
+	}
+}


Test patch:
diff --git a/api/v1beta2/awscluster_webhook_test.go b/api/v1beta2/awscluster_webhook_test.go
index 30b20c16f..dfce2b2c7 100644
--- a/api/v1beta2/awscluster_webhook_test.go
+++ b/api/v1beta2/awscluster_webhook_test.go
@@ -252,11 +252,11 @@ func TestAWSClusterValidateCreate(t *testing.T) {
 			wantErr: true,
 		},
 		{
-			name: "rejects additional ingress rules with cidr block and source security group id",
+			name: "rejects ingress rules with cidr block and source security group id",
 			cluster: &AWSCluster{
 				Spec: AWSClusterSpec{
 					ControlPlaneLoadBalancer: &AWSLoadBalancerSpec{
-						AdditionalIngressRules: []IngressRule{
+						IngressRules: []IngressRule{
 							{
 								Protocol:               SecurityGroupProtocolTCP,
 								CidrBlocks:             []string{"test"},
@@ -269,11 +269,11 @@ func TestAWSClusterValidateCreate(t *testing.T) {
 			wantErr: true,
 		},
 		{
-			name: "rejects additional ingress rules with cidr block and source security group id and role",
+			name: "rejects ingress rules with cidr block and source security group id and role",
 			cluster: &AWSCluster{
 				Spec: AWSClusterSpec{
 					ControlPlaneLoadBalancer: &AWSLoadBalancerSpec{
-						AdditionalIngressRules: []IngressRule{
+						IngressRules: []IngressRule{
 							{
 								Protocol:                 SecurityGroupProtocolTCP,
 								IPv6CidrBlocks:           []string{"test"},
@@ -287,11 +287,11 @@ func TestAWSClusterValidateCreate(t *testing.T) {
 			wantErr: true,
 		},
 		{
-			name: "accepts additional ingress rules with cidr block",
+			name: "accepts ingress rules with cidr block",
 			cluster: &AWSCluster{
 				Spec: AWSClusterSpec{
 					ControlPlaneLoadBalancer: &AWSLoadBalancerSpec{
-						AdditionalIngressRules: []IngressRule{
+						IngressRules: []IngressRule{
 							{
 								Protocol:   SecurityGroupProtocolTCP,
 								CidrBlocks: []string{"test"},
@@ -303,11 +303,11 @@ func TestAWSClusterValidateCreate(t *testing.T) {
 			wantErr: false,
 		},
 		{
-			name: "accepts additional ingress rules with source security group role",
+			name: "accepts ingress rules with source security group role",
 			cluster: &AWSCluster{
 				Spec: AWSClusterSpec{
 					ControlPlaneLoadBalancer: &AWSLoadBalancerSpec{
-						AdditionalIngressRules: []IngressRule{
+						IngressRules: []IngressRule{
 							{
 								Protocol:                 SecurityGroupProtocolTCP,
 								SourceSecurityGroupRoles: []SecurityGroupRole{SecurityGroupBastion},
@@ -319,11 +319,11 @@ func TestAWSClusterValidateCreate(t *testing.T) {
 			wantErr: false,
 		},
 		{
-			name: "accepts additional ingress rules with source security group id and role",
+			name: "accepts ingress rules with source security group id and role",
 			cluster: &AWSCluster{
 				Spec: AWSClusterSpec{
 					ControlPlaneLoadBalancer: &AWSLoadBalancerSpec{
-						AdditionalIngressRules: []IngressRule{
+						IngressRules: []IngressRule{
 							{
 								Protocol:                 SecurityGroupProtocolTCP,
 								SourceSecurityGroupIDs:   []string{"test"},
diff --git a/pkg/cloud/services/securitygroup/securitygroups_test.go b/pkg/cloud/services/securitygroup/securitygroups_test.go
index af2c9e8f0..aec340719 100644
--- a/pkg/cloud/services/securitygroup/securitygroups_test.go
+++ b/pkg/cloud/services/securitygroup/securitygroups_test.go
@@ -625,7 +625,7 @@ func TestAdditionalControlPlaneSecurityGroup(t *testing.T) {
 		{
 			name: "default control plane security group is used",
 			controlPlaneLBSpec: &infrav1.AWSLoadBalancerSpec{
-				AdditionalIngressRules: []infrav1.IngressRule{
+				IngressRules: []infrav1.IngressRule{
 					{
 						Description: "test",
 						Protocol:    infrav1.SecurityGroupProtocolTCP,
@@ -645,7 +645,7 @@ func TestAdditionalControlPlaneSecurityGroup(t *testing.T) {
 		{
 			name: "custom security group id is used",
 			controlPlaneLBSpec: &infrav1.AWSLoadBalancerSpec{
-				AdditionalIngressRules: []infrav1.IngressRule{
+				IngressRules: []infrav1.IngressRule{
 					{
 						Description:            "test",
 						Protocol:               infrav1.SecurityGroupProtocolTCP,
@@ -666,7 +666,7 @@ func TestAdditionalControlPlaneSecurityGroup(t *testing.T) {
 		{
 			name: "another security group role is used",
 			controlPlaneLBSpec: &infrav1.AWSLoadBalancerSpec{
-				AdditionalIngressRules: []infrav1.IngressRule{
+				IngressRules: []infrav1.IngressRule{
 					{
 						Description:              "test",
 						Protocol:                 infrav1.SecurityGroupProtocolTCP,
@@ -687,7 +687,7 @@ func TestAdditionalControlPlaneSecurityGroup(t *testing.T) {
 		{
 			name: "another security group role and a custom security group id is used",
 			controlPlaneLBSpec: &infrav1.AWSLoadBalancerSpec{
-				AdditionalIngressRules: []infrav1.IngressRule{
+				IngressRules: []infrav1.IngressRule{
 					{
 						Description:              "test",
 						Protocol:                 infrav1.SecurityGroupProtocolTCP,
@@ -773,6 +773,109 @@ func TestAdditionalControlPlaneSecurityGroup(t *testing.T) {
 	}
 }
 
+func TestControlPlaneLoadBalancerIngressRules(t *testing.T) {
+	scheme := runtime.NewScheme()
+	_ = infrav1.AddToScheme(scheme)
+
+	testCases := []struct {
+		name                string
+		controlPlaneLBSpec  *infrav1.AWSLoadBalancerSpec
+		useIPV6             bool
+		expectedIngresRules infrav1.IngressRules
+	}{
+		{
+			name:               "when no ingress rules are passed the default is set",
+			controlPlaneLBSpec: &infrav1.AWSLoadBalancerSpec{},
+			useIPV6:            false,
+			expectedIngresRules: infrav1.IngressRules{
+				infrav1.IngressRule{
+					Description: "Kubernetes API",
+					Protocol:    infrav1.SecurityGroupProtocolTCP,
+					FromPort:    6443,
+					ToPort:      6443,
+					CidrBlocks:  []string{services.AnyIPv4CidrBlock},
+				},
+			},
+		},
+		{
+			name:               "when no ingress rules are passed and when using ipv6, the default for ipv6 is set",
+			controlPlaneLBSpec: &infrav1.AWSLoadBalancerSpec{},
+			useIPV6:            true,
+			expectedIngresRules: infrav1.IngressRules{
+				infrav1.IngressRule{
+					Description:    "Kubernetes API IPv6",
+					Protocol:       infrav1.SecurityGroupProtocolTCP,
+					FromPort:       6443,
+					ToPort:         6443,
+					IPv6CidrBlocks: []string{services.AnyIPv6CidrBlock},
+				},
+			},
+		},
+		{
+			name: "defined rules are used",
+			controlPlaneLBSpec: &infrav1.AWSLoadBalancerSpec{
+				IngressRules: infrav1.IngressRules{
+					{
+						Description: "My custom ingress rule",
+						Protocol:    infrav1.SecurityGroupProtocolTCP,
+						FromPort:    1234,
+						ToPort:      1234,
+						CidrBlocks:  []string{"172.126.1.1/0"},
+					},
+				},
+			},
+			useIPV6: false,
+			expectedIngresRules: infrav1.IngressRules{
+				infrav1.IngressRule{
+					Description: "My custom ingress rule",
+					Protocol:    infrav1.SecurityGroupProtocolTCP,
+					FromPort:    1234,
+					ToPort:      1234,
+					CidrBlocks:  []string{"172.126.1.1/0"},
+				},
+			},
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			vpcSpec := infrav1.VPCSpec{}
+			if tc.useIPV6 {
+				vpcSpec = infrav1.VPCSpec{
+					IPv6: &infrav1.IPv6{},
+				}
+			}
+
+			cs, err := scope.NewClusterScope(scope.ClusterScopeParams{
+				Client: fake.NewClientBuilder().WithScheme(scheme).Build(),
+				Cluster: &clusterv1.Cluster{
+					ObjectMeta: metav1.ObjectMeta{Name: "test-cluster"},
+				},
+				AWSCluster: &infrav1.AWSCluster{
+					Spec: infrav1.AWSClusterSpec{
+						ControlPlaneLoadBalancer: tc.controlPlaneLBSpec,
+						NetworkSpec: infrav1.NetworkSpec{
+							VPC: vpcSpec,
+						},
+					},
+				},
+			})
+			if err != nil {
+				t.Fatalf("Failed to create test context: %v", err)
+			}
+
+			s := NewService(cs, testSecurityGroupRoles)
+			rules, err := s.getSecurityGroupIngressRules(infrav1.SecurityGroupAPIServerLB)
+			if err != nil {
+				t.Fatalf("Failed to lookup controlplane load balancer security group ingress rules: %v", err)
+			}
+
+			g := NewGomegaWithT(t)
+			g.Expect(rules).To(Equal(tc.expectedIngresRules))
+		})
+	}
+}
+
 func TestDeleteSecurityGroups(t *testing.T) {
 	mockCtrl := gomock.NewController(t)
 	defer mockCtrl.Finish()