Repository: kubernetes-sigs/cluster-api-provider-azure
Pull request #1299 (2021-04-09 21:21:18)

Add support for custom egress rules
 <!-- If this is your first PR, welcome! Please make sure you read the [contributing guidelines](../CONTRIBUTING.md). -->

 <!-- Please label this pull request according to what type of issue you are addressing (see ../CONTRIBUTING.md) -->
**What type of PR is this?**

<!--
Add one of the following kinds:
/kind feature
/kind bug
/kind api-change
/kind cleanup
/kind deprecation
/kind design
/kind documentation
/kind failing-test
/kind flake
/kind other
-->

**What this PR does / why we need it**: 

- Renames `ingressRule` to `securityRules` (note there was a missing s in the field name which represented a list of ingress rules) to be more consistent with Azure naming, and to include rules in both directions
- Adds a `direction` field to `SecurityRule` which can be either `Inbound` or `Outbound`. Defaults to `Inbound` to preserve existing behavior.

Feature request came out of [this slack thread](https://kubernetes.slack.com/archives/CEX9HENG7/p1617886293184900?thread_ts=1617708083.161200&cid=CEX9HENG7)

**Which issue(s) this PR fixes** *(optional, in `fixes #<issue number>(, fixes #<issue_number>, ...)` format, will close the issue(s) when PR gets merged)*:
Fixes partly #618  

**Special notes for your reviewer**:

_Please confirm that if this PR changes any image versions, then that's the sole change this PR makes._

**TODOs**:
<!-- Put an "X" character inside the brackets of each completed task. Some may be optional depending on the PR. -->

- [x] squashed commits
- [x] includes documentation
- [x] adds unit 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
Rename ingressRule to securityRules
Add optional direction field to securityRules, defaults to "Inbound"
```


k8s-ci-robot: [APPROVALNOTIFIER] This PR is **NOT APPROVED**

This pull-request has been approved by:
To complete the [pull request process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process), please ask for approval from **cecilerobertmichon** after the PR has been reviewed.

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-azure).

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

- **[OWNERS](https://github.com/kubernetes-sigs/cluster-api-provider-azure/blob/master/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":["cecilerobertmichon"]} -->
k8s-ci-robot: @CecileRobertMichon: The following test **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-cluster-api-provider-azure-apidiff | f3fc5109cbea0f3690e71c8e02e5cfd5b370bdd9 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-azure/1299/pull-cluster-api-provider-azure-apidiff/1380631938888372224) | `/test pull-cluster-api-provider-azure-apidiff`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api-provider-azure&pr=1299). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3ACecileRobertMichon). 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-azure/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: @CecileRobertMichon: The following tests **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-cluster-api-provider-azure-apidiff | f3fc5109cbea0f3690e71c8e02e5cfd5b370bdd9 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-azure/1299/pull-cluster-api-provider-azure-apidiff/1380631938888372224) | `/test pull-cluster-api-provider-azure-apidiff`
pull-cluster-api-provider-azure-coverage | f3fc5109cbea0f3690e71c8e02e5cfd5b370bdd9 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-azure/1299/pull-cluster-api-provider-azure-coverage/1380631938913538048) | `/test pull-cluster-api-provider-azure-coverage`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api-provider-azure&pr=1299). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3ACecileRobertMichon). 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-azure/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: @CecileRobertMichon: The following tests **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-cluster-api-provider-azure-apidiff | f3fc5109cbea0f3690e71c8e02e5cfd5b370bdd9 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-azure/1299/pull-cluster-api-provider-azure-apidiff/1380631938888372224) | `/test pull-cluster-api-provider-azure-apidiff`
pull-cluster-api-provider-azure-coverage | f3fc5109cbea0f3690e71c8e02e5cfd5b370bdd9 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-azure/1299/pull-cluster-api-provider-azure-coverage/1380631938913538048) | `/test pull-cluster-api-provider-azure-coverage`
pull-cluster-api-provider-azure-test | f3fc5109cbea0f3690e71c8e02e5cfd5b370bdd9 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-azure/1299/pull-cluster-api-provider-azure-test/1380631938758348800) | `/test pull-cluster-api-provider-azure-test`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api-provider-azure&pr=1299). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3ACecileRobertMichon). 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-azure/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: @CecileRobertMichon: The following tests **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-cluster-api-provider-azure-coverage | f3fc5109cbea0f3690e71c8e02e5cfd5b370bdd9 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-azure/1299/pull-cluster-api-provider-azure-coverage/1380631938913538048) | `/test pull-cluster-api-provider-azure-coverage`
pull-cluster-api-provider-azure-test | f3fc5109cbea0f3690e71c8e02e5cfd5b370bdd9 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-azure/1299/pull-cluster-api-provider-azure-test/1380631938758348800) | `/test pull-cluster-api-provider-azure-test`
pull-cluster-api-provider-azure-apidiff | 059328439312a0adca05235c807ee6cf514e0d08 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-azure/1299/pull-cluster-api-provider-azure-apidiff/1380638462935306240) | `/test pull-cluster-api-provider-azure-apidiff`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api-provider-azure&pr=1299). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3ACecileRobertMichon). 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-azure/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: @CecileRobertMichon: The following tests **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-cluster-api-provider-azure-test | f3fc5109cbea0f3690e71c8e02e5cfd5b370bdd9 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-azure/1299/pull-cluster-api-provider-azure-test/1380631938758348800) | `/test pull-cluster-api-provider-azure-test`
pull-cluster-api-provider-azure-apidiff | 059328439312a0adca05235c807ee6cf514e0d08 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-azure/1299/pull-cluster-api-provider-azure-apidiff/1380638462935306240) | `/test pull-cluster-api-provider-azure-apidiff`
pull-cluster-api-provider-azure-coverage | 059328439312a0adca05235c807ee6cf514e0d08 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-azure/1299/pull-cluster-api-provider-azure-coverage/1380638462956277760) | `/test pull-cluster-api-provider-azure-coverage`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api-provider-azure&pr=1299). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3ACecileRobertMichon). 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-azure/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: @CecileRobertMichon: The following tests **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-cluster-api-provider-azure-apidiff | 059328439312a0adca05235c807ee6cf514e0d08 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-azure/1299/pull-cluster-api-provider-azure-apidiff/1380638462935306240) | `/test pull-cluster-api-provider-azure-apidiff`
pull-cluster-api-provider-azure-coverage | 059328439312a0adca05235c807ee6cf514e0d08 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-azure/1299/pull-cluster-api-provider-azure-coverage/1380638462956277760) | `/test pull-cluster-api-provider-azure-coverage`
pull-cluster-api-provider-azure-test | 059328439312a0adca05235c807ee6cf514e0d08 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-azure/1299/pull-cluster-api-provider-azure-test/1380638462822060032) | `/test pull-cluster-api-provider-azure-test`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api-provider-azure&pr=1299). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3ACecileRobertMichon). 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-azure/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: @CecileRobertMichon: The following tests **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-cluster-api-provider-azure-apidiff | 059328439312a0adca05235c807ee6cf514e0d08 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-azure/1299/pull-cluster-api-provider-azure-apidiff/1380638462935306240) | `/test pull-cluster-api-provider-azure-apidiff`
pull-cluster-api-provider-azure-test | 059328439312a0adca05235c807ee6cf514e0d08 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-azure/1299/pull-cluster-api-provider-azure-test/1380638462822060032) | `/test pull-cluster-api-provider-azure-test`
pull-cluster-api-provider-azure-coverage | eb98beb93d796aaf0656333c10f3fa551be8c2d4 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-azure/1299/pull-cluster-api-provider-azure-coverage/1380652893983477760) | `/test pull-cluster-api-provider-azure-coverage`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api-provider-azure&pr=1299). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3ACecileRobertMichon). 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-azure/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: @CecileRobertMichon: The following tests **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-cluster-api-provider-azure-test | 059328439312a0adca05235c807ee6cf514e0d08 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-azure/1299/pull-cluster-api-provider-azure-test/1380638462822060032) | `/test pull-cluster-api-provider-azure-test`
pull-cluster-api-provider-azure-coverage | eb98beb93d796aaf0656333c10f3fa551be8c2d4 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-azure/1299/pull-cluster-api-provider-azure-coverage/1380652893983477760) | `/test pull-cluster-api-provider-azure-coverage`
pull-cluster-api-provider-azure-apidiff | eb98beb93d796aaf0656333c10f3fa551be8c2d4 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-azure/1299/pull-cluster-api-provider-azure-apidiff/1380652893941534720) | `/test pull-cluster-api-provider-azure-apidiff`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api-provider-azure&pr=1299). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3ACecileRobertMichon). 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-azure/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: @CecileRobertMichon: The following tests **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-cluster-api-provider-azure-coverage | eb98beb93d796aaf0656333c10f3fa551be8c2d4 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-azure/1299/pull-cluster-api-provider-azure-coverage/1380652893983477760) | `/test pull-cluster-api-provider-azure-coverage`
pull-cluster-api-provider-azure-apidiff | eb98beb93d796aaf0656333c10f3fa551be8c2d4 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-azure/1299/pull-cluster-api-provider-azure-apidiff/1380652893941534720) | `/test pull-cluster-api-provider-azure-apidiff`
pull-cluster-api-provider-azure-test | eb98beb93d796aaf0656333c10f3fa551be8c2d4 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-azure/1299/pull-cluster-api-provider-azure-test/1380652893626961920) | `/test pull-cluster-api-provider-azure-test`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api-provider-azure&pr=1299). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3ACecileRobertMichon). 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-azure/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: @CecileRobertMichon: The following tests **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-cluster-api-provider-azure-coverage | eb98beb93d796aaf0656333c10f3fa551be8c2d4 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-azure/1299/pull-cluster-api-provider-azure-coverage/1380652893983477760) | `/test pull-cluster-api-provider-azure-coverage`
pull-cluster-api-provider-azure-test | eb98beb93d796aaf0656333c10f3fa551be8c2d4 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-azure/1299/pull-cluster-api-provider-azure-test/1380652893626961920) | `/test pull-cluster-api-provider-azure-test`
pull-cluster-api-provider-azure-apidiff | 2a3e814b768587a5f25edd7a17ad7461d1ea6a96 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-azure/1299/pull-cluster-api-provider-azure-apidiff/1380668851041079296) | `/test pull-cluster-api-provider-azure-apidiff`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api-provider-azure&pr=1299). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3ACecileRobertMichon). 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-azure/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: New changes are detected. LGTM label has been removed.
k8s-ci-robot: @CecileRobertMichon: The following test **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-cluster-api-provider-azure-apidiff | 49e9bd481a89186ccd5acd368fb5de21e6207c90 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-azure/1299/pull-cluster-api-provider-azure-apidiff/1381659341639127040) | `/test pull-cluster-api-provider-azure-apidiff`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api-provider-azure&pr=1299). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3ACecileRobertMichon). 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-azure/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: @CecileRobertMichon: The following tests **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-cluster-api-provider-azure-apidiff | 49e9bd481a89186ccd5acd368fb5de21e6207c90 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-azure/1299/pull-cluster-api-provider-azure-apidiff/1381659341639127040) | `/test pull-cluster-api-provider-azure-apidiff`
pull-cluster-api-provider-azure-coverage | 49e9bd481a89186ccd5acd368fb5de21e6207c90 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-azure/1299/pull-cluster-api-provider-azure-coverage/1381659341660098560) | `/test pull-cluster-api-provider-azure-coverage`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api-provider-azure&pr=1299). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3ACecileRobertMichon). 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-azure/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: @CecileRobertMichon: The following tests **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-cluster-api-provider-azure-apidiff | 49e9bd481a89186ccd5acd368fb5de21e6207c90 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-azure/1299/pull-cluster-api-provider-azure-apidiff/1381659341639127040) | `/test pull-cluster-api-provider-azure-apidiff`
pull-cluster-api-provider-azure-coverage | 49e9bd481a89186ccd5acd368fb5de21e6207c90 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-azure/1299/pull-cluster-api-provider-azure-coverage/1381659341660098560) | `/test pull-cluster-api-provider-azure-coverage`
pull-cluster-api-provider-azure-test | 49e9bd481a89186ccd5acd368fb5de21e6207c90 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-azure/1299/pull-cluster-api-provider-azure-test/1381659341530075136) | `/test pull-cluster-api-provider-azure-test`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api-provider-azure&pr=1299). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3ACecileRobertMichon). 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-azure/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: @CecileRobertMichon: The following tests **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-cluster-api-provider-azure-coverage | 49e9bd481a89186ccd5acd368fb5de21e6207c90 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-azure/1299/pull-cluster-api-provider-azure-coverage/1381659341660098560) | `/test pull-cluster-api-provider-azure-coverage`
pull-cluster-api-provider-azure-test | 49e9bd481a89186ccd5acd368fb5de21e6207c90 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-azure/1299/pull-cluster-api-provider-azure-test/1381659341530075136) | `/test pull-cluster-api-provider-azure-test`
pull-cluster-api-provider-azure-apidiff | e2911e71402c9691b6772fbaca8087b015568c59 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-azure/1299/pull-cluster-api-provider-azure-apidiff/1381754481384886272) | `/test pull-cluster-api-provider-azure-apidiff`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api-provider-azure&pr=1299). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3ACecileRobertMichon). 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-azure/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 -->
CecileRobertMichon: /hold for squash
k8s-ci-robot: @CecileRobertMichon: The following tests **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-cluster-api-provider-azure-apidiff | e2911e71402c9691b6772fbaca8087b015568c59 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-azure/1299/pull-cluster-api-provider-azure-apidiff/1381754481384886272) | `/test pull-cluster-api-provider-azure-apidiff`
pull-cluster-api-provider-azure-e2e-windows | e2911e71402c9691b6772fbaca8087b015568c59 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-azure/1299/pull-cluster-api-provider-azure-e2e-windows/1381754481342943232) | `/test pull-cluster-api-provider-azure-e2e-windows`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api-provider-azure&pr=1299). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3ACecileRobertMichon). 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-azure/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 -->

Patch:
diff --git a/api/v1alpha3/azurecluster_conversion.go b/api/v1alpha3/azurecluster_conversion.go
index b8f21f501..c091b24bc 100644
--- a/api/v1alpha3/azurecluster_conversion.go
+++ b/api/v1alpha3/azurecluster_conversion.go
@@ -23,7 +23,6 @@ import (
 
 	apiconversion "k8s.io/apimachinery/pkg/conversion"
 	infrav1alpha4 "sigs.k8s.io/cluster-api-provider-azure/api/v1alpha4"
-	v1alpha4 "sigs.k8s.io/cluster-api-provider-azure/api/v1alpha4"
 	apiv1alpha3 "sigs.k8s.io/cluster-api/api/v1alpha3"
 	apiv1alpha4 "sigs.k8s.io/cluster-api/api/v1alpha4"
 	"sigs.k8s.io/controller-runtime/pkg/conversion"
@@ -58,6 +57,24 @@ func (src *AzureCluster) ConvertTo(dstRaw conversion.Hub) error { // nolint
 	dst.Spec.NetworkSpec.APIServerLB.FrontendIPsCount = restored.Spec.NetworkSpec.APIServerLB.FrontendIPsCount
 	dst.Spec.NetworkSpec.NodeOutboundLB = restored.Spec.NetworkSpec.NodeOutboundLB
 
+	// Here we manually restore outbound security rules. Since v1alpha3 only supports ingress ("Inbound") rules, all v1alpha4 outbound rules are dropped when an AzureCluster
+	// is converted to v1alpha3. We loop through all security group rules. For all previously existing outbound rules we restore the full rule.
+	for _, restoredSubnet := range restored.Spec.NetworkSpec.Subnets {
+		for i, dstSubnet := range dst.Spec.NetworkSpec.Subnets {
+			if dstSubnet.Name == restoredSubnet.Name {
+				var restoredOutboundRules []infrav1alpha4.SecurityRule
+				for _, restoredSecurityRule := range restoredSubnet.SecurityGroup.SecurityRules {
+					if restoredSecurityRule.Direction != infrav1alpha4.SecurityRuleDirectionInbound {
+						// For non-inbound rules which are only supported starting in v1alpha4, we restore the entire rule.
+						restoredOutboundRules = append(restoredOutboundRules, restoredSecurityRule)
+					}
+				}
+				dst.Spec.NetworkSpec.Subnets[i].SecurityGroup.SecurityRules = append(dst.Spec.NetworkSpec.Subnets[i].SecurityGroup.SecurityRules, restoredOutboundRules...)
+				break
+			}
+		}
+	}
+
 	return nil
 }
 
@@ -195,11 +212,13 @@ func Convert_v1alpha4_SecurityGroup_To_v1alpha3_SecurityGroup(in *infrav1alpha4.
 	out.ID = in.ID
 	out.Name = in.Name
 
-	out.IngressRules = make(IngressRules, len(in.IngressRules))
-	for i := range in.IngressRules {
-		out.IngressRules[i] = IngressRule{}
-		if err := Convert_v1alpha4_IngressRule_To_v1alpha3_IngressRule(&in.IngressRules[i], &out.IngressRules[i], s); err != nil {
-			return err
+	out.IngressRules = make(IngressRules, 0)
+	for i, rule := range in.SecurityRules {
+		if rule.Direction == infrav1alpha4.SecurityRuleDirectionInbound { // only inbound rules are supported in v1alpha3.
+			out.IngressRules[i] = IngressRule{}
+			if err := Convert_v1alpha4_SecurityRule_To_v1alpha3_IngressRule(&in.SecurityRules[i], &out.IngressRules[i], s); err != nil {
+				return err
+			}
 		}
 	}
 
@@ -207,14 +226,14 @@ func Convert_v1alpha4_SecurityGroup_To_v1alpha3_SecurityGroup(in *infrav1alpha4.
 	return nil
 }
 
-func Convert_v1alpha3_SecurityGroup_To_v1alpha4_SecurityGroup(in *SecurityGroup, out *infrav1alpha4.SecurityGroup, s apiconversion.Scope) error {
+func Convert_v1alpha3_SecurityGroup_To_v1alpha4_SecurityGroup(in *SecurityGroup, out *infrav1alpha4.SecurityGroup, s apiconversion.Scope) error { //nolint
 	out.ID = in.ID
 	out.Name = in.Name
 
-	out.IngressRules = make(infrav1alpha4.IngressRules, len(in.IngressRules))
+	out.SecurityRules = make(infrav1alpha4.SecurityRules, len(in.IngressRules))
 	for i := range in.IngressRules {
-		out.IngressRules[i] = infrav1alpha4.IngressRule{}
-		if err := Convert_v1alpha3_IngressRule_To_v1alpha4_IngressRule(&in.IngressRules[i], &out.IngressRules[i], s); err != nil {
+		out.SecurityRules[i] = infrav1alpha4.SecurityRule{}
+		if err := Convert_v1alpha3_IngressRule_To_v1alpha4_SecurityRule(&in.IngressRules[i], &out.SecurityRules[i], s); err != nil {
 			return err
 		}
 	}
@@ -223,18 +242,35 @@ func Convert_v1alpha3_SecurityGroup_To_v1alpha4_SecurityGroup(in *SecurityGroup,
 	return nil
 }
 
-// Convert_v1alpha3_IngressRule_To_v1alpha4_IngressRule
-func Convert_v1alpha3_IngressRule_To_v1alpha4_IngressRule(in *IngressRule, out *infrav1alpha4.IngressRule, s apiconversion.Scope) error {
-	return autoConvert_v1alpha3_IngressRule_To_v1alpha4_IngressRule(in, out, s)
+// Convert_v1alpha3_IngressRule_To_v1alpha4_SecurityRule
+func Convert_v1alpha3_IngressRule_To_v1alpha4_SecurityRule(in *IngressRule, out *infrav1alpha4.SecurityRule, _ apiconversion.Scope) error { //nolint
+	out.Name = in.Name
+	out.Description = in.Description
+	out.Protocol = infrav1alpha4.SecurityGroupProtocol(in.Protocol)
+	out.Priority = in.Priority
+	out.SourcePorts = in.SourcePorts
+	out.DestinationPorts = in.DestinationPorts
+	out.Source = in.Source
+	out.Destination = in.Destination
+	out.Direction = infrav1alpha4.SecurityRuleDirectionInbound // all v1alpha3 rules are inbound.
+	return nil
 }
 
-// Convert_v1alpha4_IngressRule_To_v1alpha3_IngressRule
-func Convert_v1alpha4_IngressRule_To_v1alpha3_IngressRule(in *infrav1alpha4.IngressRule, out *IngressRule, s apiconversion.Scope) error {
-	return autoConvert_v1alpha4_IngressRule_To_v1alpha3_IngressRule(in, out, s)
+// Convert_v1alpha4_SecurityRule_To_v1alpha3_IngressRule
+func Convert_v1alpha4_SecurityRule_To_v1alpha3_IngressRule(in *infrav1alpha4.SecurityRule, out *IngressRule, _ apiconversion.Scope) error { //nolint
+	out.Name = in.Name
+	out.Description = in.Description
+	out.Protocol = SecurityGroupProtocol(in.Protocol)
+	out.Priority = in.Priority
+	out.SourcePorts = in.SourcePorts
+	out.DestinationPorts = in.DestinationPorts
+	out.Source = in.Source
+	out.Destination = in.Destination
+	return nil
 }
 
 // Convert_v1alpha4_ManagedDisk_To_v1alpha3_ManagedDisk converts between api versions
-func Convert_v1alpha4_ManagedDisk_To_v1alpha3_ManagedDisk(in *v1alpha4.ManagedDisk, out *ManagedDisk, s apiconversion.Scope) error {
+func Convert_v1alpha4_ManagedDisk_To_v1alpha3_ManagedDisk(in *infrav1alpha4.ManagedDisk, out *ManagedDisk, s apiconversion.Scope) error {
 	return autoConvert_v1alpha4_ManagedDisk_To_v1alpha3_ManagedDisk(in, out, s)
 }
 
@@ -249,11 +285,11 @@ func Convert_v1alpha4_APIEndpoint_To_v1alpha3_APIEndpoint(in *apiv1alpha4.APIEnd
 }
 
 // Convert_v1alpha3_VnetSpec_To_v1alpha4_VnetSpec is an autogenerated conversion function.
-func Convert_v1alpha3_VnetSpec_To_v1alpha4_VnetSpec(in *VnetSpec, out *v1alpha4.VnetSpec, s apiconversion.Scope) error {
+func Convert_v1alpha3_VnetSpec_To_v1alpha4_VnetSpec(in *VnetSpec, out *infrav1alpha4.VnetSpec, s apiconversion.Scope) error {
 	return autoConvert_v1alpha3_VnetSpec_To_v1alpha4_VnetSpec(in, out, s)
 }
 
 // Convert_v1alpha4_LoadBalancerSpec_To_v1alpha3_LoadBalancerSpec is an autogenerated conversion function.
-func Convert_v1alpha4_LoadBalancerSpec_To_v1alpha3_LoadBalancerSpec(in *v1alpha4.LoadBalancerSpec, out *LoadBalancerSpec, s apiconversion.Scope) error {
+func Convert_v1alpha4_LoadBalancerSpec_To_v1alpha3_LoadBalancerSpec(in *infrav1alpha4.LoadBalancerSpec, out *LoadBalancerSpec, s apiconversion.Scope) error {
 	return autoConvert_v1alpha4_LoadBalancerSpec_To_v1alpha3_LoadBalancerSpec(in, out, s)
 }
diff --git a/api/v1alpha3/zz_generated.conversion.go b/api/v1alpha3/zz_generated.conversion.go
index 0ceda5fa4..85a4632d4 100644
--- a/api/v1alpha3/zz_generated.conversion.go
+++ b/api/v1alpha3/zz_generated.conversion.go
@@ -365,8 +365,8 @@ func RegisterConversions(s *runtime.Scheme) error {
 	}); err != nil {
 		return err
 	}
-	if err := s.AddConversionFunc((*IngressRule)(nil), (*v1alpha4.IngressRule)(nil), func(a, b interface{}, scope conversion.Scope) error {
-		return Convert_v1alpha3_IngressRule_To_v1alpha4_IngressRule(a.(*IngressRule), b.(*v1alpha4.IngressRule), scope)
+	if err := s.AddConversionFunc((*IngressRule)(nil), (*v1alpha4.SecurityRule)(nil), func(a, b interface{}, scope conversion.Scope) error {
+		return Convert_v1alpha3_IngressRule_To_v1alpha4_SecurityRule(a.(*IngressRule), b.(*v1alpha4.SecurityRule), scope)
 	}); err != nil {
 		return err
 	}
@@ -415,11 +415,6 @@ func RegisterConversions(s *runtime.Scheme) error {
 	}); err != nil {
 		return err
 	}
-	if err := s.AddConversionFunc((*v1alpha4.IngressRule)(nil), (*IngressRule)(nil), func(a, b interface{}, scope conversion.Scope) error {
-		return Convert_v1alpha4_IngressRule_To_v1alpha3_IngressRule(a.(*v1alpha4.IngressRule), b.(*IngressRule), scope)
-	}); err != nil {
-		return err
-	}
 	if err := s.AddConversionFunc((*v1alpha4.LoadBalancerSpec)(nil), (*LoadBalancerSpec)(nil), func(a, b interface{}, scope conversion.Scope) error {
 		return Convert_v1alpha4_LoadBalancerSpec_To_v1alpha3_LoadBalancerSpec(a.(*v1alpha4.LoadBalancerSpec), b.(*LoadBalancerSpec), scope)
 	}); err != nil {
@@ -440,6 +435,11 @@ func RegisterConversions(s *runtime.Scheme) error {
 	}); err != nil {
 		return err
 	}
+	if err := s.AddConversionFunc((*v1alpha4.SecurityRule)(nil), (*IngressRule)(nil), func(a, b interface{}, scope conversion.Scope) error {
+		return Convert_v1alpha4_SecurityRule_To_v1alpha3_IngressRule(a.(*v1alpha4.SecurityRule), b.(*IngressRule), scope)
+	}); err != nil {
+		return err
+	}
 	if err := s.AddConversionFunc((*v1alpha4.SubnetSpec)(nil), (*SubnetSpec)(nil), func(a, b interface{}, scope conversion.Scope) error {
 		return Convert_v1alpha4_SubnetSpec_To_v1alpha3_SubnetSpec(a.(*v1alpha4.SubnetSpec), b.(*SubnetSpec), scope)
 	}); err != nil {
@@ -1218,30 +1218,6 @@ func Convert_v1alpha4_Image_To_v1alpha3_Image(in *v1alpha4.Image, out *Image, s
 	return autoConvert_v1alpha4_Image_To_v1alpha3_Image(in, out, s)
 }
 
-func autoConvert_v1alpha3_IngressRule_To_v1alpha4_IngressRule(in *IngressRule, out *v1alpha4.IngressRule, s conversion.Scope) error {
-	out.Name = in.Name
-	out.Description = in.Description
-	out.Protocol = v1alpha4.SecurityGroupProtocol(in.Protocol)
-	out.Priority = in.Priority
-	out.SourcePorts = (*string)(unsafe.Pointer(in.SourcePorts))
-	out.DestinationPorts = (*string)(unsafe.Pointer(in.DestinationPorts))
-	out.Source = (*string)(unsafe.Pointer(in.Source))
-	out.Destination = (*string)(unsafe.Pointer(in.Destination))
-	return nil
-}
-
-func autoConvert_v1alpha4_IngressRule_To_v1alpha3_IngressRule(in *v1alpha4.IngressRule, out *IngressRule, s conversion.Scope) error {
-	out.Name = in.Name
-	out.Description = in.Description
-	out.Protocol = SecurityGroupProtocol(in.Protocol)
-	out.Priority = in.Priority
-	out.SourcePorts = (*string)(unsafe.Pointer(in.SourcePorts))
-	out.DestinationPorts = (*string)(unsafe.Pointer(in.DestinationPorts))
-	out.Source = (*string)(unsafe.Pointer(in.Source))
-	out.Destination = (*string)(unsafe.Pointer(in.Destination))
-	return nil
-}
-
 func autoConvert_v1alpha3_LoadBalancerSpec_To_v1alpha4_LoadBalancerSpec(in *LoadBalancerSpec, out *v1alpha4.LoadBalancerSpec, s conversion.Scope) error {
 	out.ID = in.ID
 	out.Name = in.Name
@@ -1406,17 +1382,7 @@ func Convert_v1alpha4_RouteTable_To_v1alpha3_RouteTable(in *v1alpha4.RouteTable,
 func autoConvert_v1alpha3_SecurityGroup_To_v1alpha4_SecurityGroup(in *SecurityGroup, out *v1alpha4.SecurityGroup, s conversion.Scope) error {
 	out.ID = in.ID
 	out.Name = in.Name
-	if in.IngressRules != nil {
-		in, out := &in.IngressRules, &out.IngressRules
-		*out = make(v1alpha4.IngressRules, len(*in))
-		for i := range *in {
-			if err := Convert_v1alpha3_IngressRule_To_v1alpha4_IngressRule(&(*in)[i], &(*out)[i], s); err != nil {
-				return err
-			}
-		}
-	} else {
-		out.IngressRules = nil
-	}
+	// WARNING: in.IngressRules requires manual conversion: does not exist in peer-type
 	out.Tags = *(*v1alpha4.Tags)(unsafe.Pointer(&in.Tags))
 	return nil
 }
@@ -1424,17 +1390,7 @@ func autoConvert_v1alpha3_SecurityGroup_To_v1alpha4_SecurityGroup(in *SecurityGr
 func autoConvert_v1alpha4_SecurityGroup_To_v1alpha3_SecurityGroup(in *v1alpha4.SecurityGroup, out *SecurityGroup, s conversion.Scope) error {
 	out.ID = in.ID
 	out.Name = in.Name
-	if in.IngressRules != nil {
-		in, out := &in.IngressRules, &out.IngressRules
-		*out = make(IngressRules, len(*in))
-		for i := range *in {
-			if err := Convert_v1alpha4_IngressRule_To_v1alpha3_IngressRule(&(*in)[i], &(*out)[i], s); err != nil {
-				return err
-			}
-		}
-	} else {
-		out.IngressRules = nil
-	}
+	// WARNING: in.SecurityRules requires manual conversion: does not exist in peer-type
 	out.Tags = *(*Tags)(unsafe.Pointer(&in.Tags))
 	return nil
 }
diff --git a/api/v1alpha4/azurecluster_default.go b/api/v1alpha4/azurecluster_default.go
index aa52270f3..11d5f9aef 100644
--- a/api/v1alpha4/azurecluster_default.go
+++ b/api/v1alpha4/azurecluster_default.go
@@ -94,6 +94,7 @@ func (c *AzureCluster) setSubnetDefaults() {
 	if cpSubnet.SecurityGroup.Name == "" {
 		cpSubnet.SecurityGroup.Name = generateControlPlaneSecurityGroupName(c.ObjectMeta.Name)
 	}
+	setSecurityRuleDefaults(&cpSubnet.SecurityGroup)
 
 	if nodeSubnet.Name == "" {
 		nodeSubnet.Name = generateNodeSubnetName(c.ObjectMeta.Name)
@@ -104,6 +105,7 @@ func (c *AzureCluster) setSubnetDefaults() {
 	if nodeSubnet.SecurityGroup.Name == "" {
 		nodeSubnet.SecurityGroup.Name = generateNodeSecurityGroupName(c.ObjectMeta.Name)
 	}
+	setSecurityRuleDefaults(&nodeSubnet.SecurityGroup)
 	if nodeSubnet.RouteTable.Name == "" {
 		nodeSubnet.RouteTable.Name = generateNodeRouteTableName(c.ObjectMeta.Name)
 	}
@@ -112,6 +114,14 @@ func (c *AzureCluster) setSubnetDefaults() {
 	c.Spec.NetworkSpec.UpdateNodeSubnet(nodeSubnet)
 }
 
+func setSecurityRuleDefaults(sg *SecurityGroup) {
+	for i := range sg.SecurityRules {
+		if sg.SecurityRules[i].Direction == "" {
+			sg.SecurityRules[i].Direction = SecurityRuleDirectionInbound
+		}
+	}
+}
+
 func (c *AzureCluster) setAPIServerLBDefaults() {
 	lb := &c.Spec.NetworkSpec.APIServerLB
 	if lb.Type == "" {
diff --git a/api/v1alpha4/azurecluster_validation.go b/api/v1alpha4/azurecluster_validation.go
index 4a36970b5..877298a02 100644
--- a/api/v1alpha4/azurecluster_validation.go
+++ b/api/v1alpha4/azurecluster_validation.go
@@ -41,6 +41,10 @@ const (
 	loadBalancerRegex = `^[-\w\._]+$`
 	// MaxLoadBalancerOutboundIPs is the maximum number of outbound IPs in a Standard LoadBalancer frontend configuration
 	MaxLoadBalancerOutboundIPs = 16
+	// Network security rules should be a number between 100 and 4096.
+	// https://docs.microsoft.com/en-us/azure/virtual-network/network-security-groups-overview#security-rules
+	minRulePriority = 100
+	maxRulePriority = 4096
 )
 
 // validateCluster validates a cluster
@@ -151,10 +155,10 @@ func validateSubnets(subnets Subnets, fldPath *field.Path) field.ErrorList {
 				requiredSubnetRoles[role] = true
 			}
 		}
-		for _, ingressRule := range subnet.SecurityGroup.IngressRules {
-			if err := validateIngressRule(
-				ingressRule,
-				fldPath.Index(i).Child("securityGroup").Child("ingressRules").Index(i),
+		for _, rule := range subnet.SecurityGroup.SecurityRules {
+			if err := validateSecurityRule(
+				rule,
+				fldPath.Index(i).Child("securityGroup").Child("securityRules").Index(i),
 			); err != nil {
 				allErrs = append(allErrs, err)
 			}
@@ -204,10 +208,10 @@ func validateInternalLBIPAddress(address string, cidrs []string, fldPath *field.
 		fmt.Sprintf("Internal LB IP address needs to be in control plane subnet range (%s)", cidrs))
 }
 
-// validateIngressRule validates an IngressRule
-func validateIngressRule(ingressRule IngressRule, fldPath *field.Path) *field.Error {
-	if ingressRule.Priority < 100 || ingressRule.Priority > 4096 {
-		return field.Invalid(fldPath, ingressRule.Priority, "ingress priorities should be between 100 and 4096")
+// validateSecurityRule validates a SecurityRule
+func validateSecurityRule(rule SecurityRule, fldPath *field.Path) *field.Error {
+	if rule.Priority < minRulePriority || rule.Priority > maxRulePriority {
+		return field.Invalid(fldPath, rule.Priority, fmt.Sprintf("security rule priorities should be between %d and %d", minRulePriority, maxRulePriority))
 	}
 
 	return nil
diff --git a/api/v1alpha4/types.go b/api/v1alpha4/types.go
index d649a400f..b57b6cdc4 100644
--- a/api/v1alpha4/types.go
+++ b/api/v1alpha4/types.go
@@ -99,10 +99,10 @@ type Subnets []SubnetSpec
 
 // SecurityGroup defines an Azure security group.
 type SecurityGroup struct {
-	ID           string       `json:"id,omitempty"`
-	Name         string       `json:"name,omitempty"`
-	IngressRules IngressRules `json:"ingressRule,omitempty"`
-	Tags         Tags         `json:"tags,omitempty"`
+	ID            string        `json:"id,omitempty"`
+	Name          string        `json:"name,omitempty"`
+	SecurityRules SecurityRules `json:"securityRules,omitempty"`
+	Tags          Tags          `json:"tags,omitempty"`
 }
 
 // RouteTable defines an Azure route table.
@@ -115,40 +115,53 @@ type RouteTable struct {
 type SecurityGroupProtocol string
 
 const (
-	// SecurityGroupProtocolAll is a wildcard for all IP protocols
+	// SecurityGroupProtocolAll is a wildcard for all IP protocols.
 	SecurityGroupProtocolAll = SecurityGroupProtocol("*")
-
-	// SecurityGroupProtocolTCP represents the TCP protocol in ingress rules
+	// SecurityGroupProtocolTCP represents the TCP protocol.
 	SecurityGroupProtocolTCP = SecurityGroupProtocol("Tcp")
-
-	// SecurityGroupProtocolUDP represents the UDP protocol in ingress rules
+	// SecurityGroupProtocolUDP represents the UDP protocol.
 	SecurityGroupProtocolUDP = SecurityGroupProtocol("Udp")
+	// SecurityGroupProtocolICMP represents the ICMP protocol.
+	SecurityGroupProtocolICMP = SecurityGroupProtocol("Icmp")
 )
 
-// IngressRule defines an Azure ingress rule for security groups.
-type IngressRule struct {
-	Name        string                `json:"name"`
-	Description string                `json:"description"`
-	Protocol    SecurityGroupProtocol `json:"protocol"`
+// SecurityRuleDirection defines the direction type for a security group rule.
+type SecurityRuleDirection string
 
-	// Priority - A number between 100 and 4096. Each rule should have a unique value for priority. Rules are processed in priority order, with lower numbers processed before higher numbers. Once traffic matches a rule, processing stops.
-	Priority int32 `json:"priority,omitempty"`
+const (
+	// SecurityRuleDirectionInbound defines an ingress security rule.
+	SecurityRuleDirectionInbound = SecurityRuleDirection("Inbound")
 
-	// SourcePorts - The source port or range. Integer or range between 0 and 65535. Asterix '*' can also be used to match all ports.
-	SourcePorts *string `json:"sourcePorts,omitempty"`
+	// SecurityRuleDirectionOutbound defines an egress security rule.
+	SecurityRuleDirectionOutbound = SecurityRuleDirection("Outbound")
+)
 
-	// DestinationPorts - The destination port or range. Integer or range between 0 and 65535. Asterix '*' can also be used to match all ports.
+// SecurityRule defines an Azure security rule for security groups.
+type SecurityRule struct {
+	// Name is a unique name within the network security group.
+	Name string `json:"name"`
+	// A description for this rule. Restricted to 140 chars.
+	Description string `json:"description"`
+	// Protocol specifies the protocol type. "Tcp", "Udp", "Icmp", or "*".
+	// +kubebuilder:validation:Enum=Tcp;Udp;Icmp;*
+	Protocol SecurityGroupProtocol `json:"protocol"`
+	// Direction indicates whether the rule applies to inbound, or outbound traffic. "Inbound" or "Outbound".
+	// +kubebuilder:validation:Enum=Inbound;Outbound
+	Direction SecurityRuleDirection `json:"direction"`
+	// Priority is a number between 100 and 4096. Each rule should have a unique value for priority. Rules are processed in priority order, with lower numbers processed before higher numbers. Once traffic matches a rule, processing stops.
+	Priority int32 `json:"priority,omitempty"`
+	// SourcePorts specifies source port or range. Integer or range between 0 and 65535. Asterix '*' can also be used to match all ports.
+	SourcePorts *string `json:"sourcePorts,omitempty"`
+	// DestinationPorts specifies the destination port or range. Integer or range between 0 and 65535. Asterix '*' can also be used to match all ports.
 	DestinationPorts *string `json:"destinationPorts,omitempty"`
-
-	// Source - The CIDR or source IP range. Asterix '*' can also be used to match all source IPs. Default tags such as 'VirtualNetwork', 'AzureLoadBalancer' and 'Internet' can also be used. If this is an ingress rule, specifies where network traffic originates from.
+	// Source specifies the CIDR or source IP range. Asterix '*' can also be used to match all source IPs. Default tags such as 'VirtualNetwork', 'AzureLoadBalancer' and 'Internet' can also be used. If this is an ingress rule, specifies where network traffic originates from.
 	Source *string `json:"source,omitempty"`
-
-	// Destination - The destination address prefix. CIDR or destination IP range. Asterix '*' can also be used to match all source IPs. Default tags such as 'VirtualNetwork', 'AzureLoadBalancer' and 'Internet' can also be used.
+	// Destination is the destination address prefix. CIDR or destination IP range. Asterix '*' can also be used to match all source IPs. Default tags such as 'VirtualNetwork', 'AzureLoadBalancer' and 'Internet' can also be used.
 	Destination *string `json:"destination,omitempty"`
 }
 
-// IngressRules is a slice of Azure ingress rules for security groups.
-type IngressRules []IngressRule
+// SecurityRules is a slice of Azure security rules for security groups.
+type SecurityRules []SecurityRule
 
 // LoadBalancerSpec defines an Azure load balancer.
 type LoadBalancerSpec struct {
diff --git a/api/v1alpha4/zz_generated.deepcopy.go b/api/v1alpha4/zz_generated.deepcopy.go
index 4cbe32a0e..3feac1275 100644
--- a/api/v1alpha4/zz_generated.deepcopy.go
+++ b/api/v1alpha4/zz_generated.deepcopy.go
@@ -699,62 +699,6 @@ func (in *Image) DeepCopy() *Image {
 	return out
 }
 
-// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
-func (in *IngressRule) DeepCopyInto(out *IngressRule) {
-	*out = *in
-	if in.SourcePorts != nil {
-		in, out := &in.SourcePorts, &out.SourcePorts
-		*out = new(string)
-		**out = **in
-	}
-	if in.DestinationPorts != nil {
-		in, out := &in.DestinationPorts, &out.DestinationPorts
-		*out = new(string)
-		**out = **in
-	}
-	if in.Source != nil {
-		in, out := &in.Source, &out.Source
-		*out = new(string)
-		**out = **in
-	}
-	if in.Destination != nil {
-		in, out := &in.Destination, &out.Destination
-		*out = new(string)
-		**out = **in
-	}
-}
-
-// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IngressRule.
-func (in *IngressRule) DeepCopy() *IngressRule {
-	if in == nil {
-		return nil
-	}
-	out := new(IngressRule)
-	in.DeepCopyInto(out)
-	return out
-}
-
-// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
-func (in IngressRules) DeepCopyInto(out *IngressRules) {
-	{
-		in := &in
-		*out = make(IngressRules, len(*in))
-		for i := range *in {
-			(*in)[i].DeepCopyInto(&(*out)[i])
-		}
-	}
-}
-
-// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IngressRules.
-func (in IngressRules) DeepCopy() IngressRules {
-	if in == nil {
-		return nil
-	}
-	out := new(IngressRules)
-	in.DeepCopyInto(out)
-	return *out
-}
-
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *LoadBalancerSpec) DeepCopyInto(out *LoadBalancerSpec) {
 	*out = *in
@@ -885,9 +829,9 @@ func (in *RouteTable) DeepCopy() *RouteTable {
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *SecurityGroup) DeepCopyInto(out *SecurityGroup) {
 	*out = *in
-	if in.IngressRules != nil {
-		in, out := &in.IngressRules, &out.IngressRules
-		*out = make(IngressRules, len(*in))
+	if in.SecurityRules != nil {
+		in, out := &in.SecurityRules, &out.SecurityRules
+		*out = make(SecurityRules, len(*in))
 		for i := range *in {
 			(*in)[i].DeepCopyInto(&(*out)[i])
 		}
@@ -931,6 +875,62 @@ func (in *SecurityProfile) DeepCopy() *SecurityProfile {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *SecurityRule) DeepCopyInto(out *SecurityRule) {
+	*out = *in
+	if in.SourcePorts != nil {
+		in, out := &in.SourcePorts, &out.SourcePorts
+		*out = new(string)
+		**out = **in
+	}
+	if in.DestinationPorts != nil {
+		in, out := &in.DestinationPorts, &out.DestinationPorts
+		*out = new(string)
+		**out = **in
+	}
+	if in.Source != nil {
+		in, out := &in.Source, &out.Source
+		*out = new(string)
+		**out = **in
+	}
+	if in.Destination != nil {
+		in, out := &in.Destination, &out.Destination
+		*out = new(string)
+		**out = **in
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecurityRule.
+func (in *SecurityRule) DeepCopy() *SecurityRule {
+	if in == nil {
+		return nil
+	}
+	out := new(SecurityRule)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in SecurityRules) DeepCopyInto(out *SecurityRules) {
+	{
+		in := &in
+		*out = make(SecurityRules, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecurityRules.
+func (in SecurityRules) DeepCopy() SecurityRules {
+	if in == nil {
+		return nil
+	}
+	out := new(SecurityRules)
+	in.DeepCopyInto(out)
+	return *out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *SpotVMOptions) DeepCopyInto(out *SpotVMOptions) {
 	*out = *in
diff --git a/azure/converters/rules.go b/azure/converters/rules.go
index 810bfd1ba..df4a8acd1 100644
--- a/azure/converters/rules.go
+++ b/azure/converters/rules.go
@@ -22,54 +22,38 @@ import (
 	infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1alpha4"
 )
 
-// IngresstoSecurityRule converts a CAPI ingress rule to an Azure network security rule.
-func IngresstoSecurityRule(ingress infrav1.IngressRule) network.SecurityRule {
+// SecurityRuleToSDK converts a CAPZ security rule to an Azure network security rule.
+func SecurityRuleToSDK(rule infrav1.SecurityRule) network.SecurityRule {
 	secRule := network.SecurityRule{
-		Name: to.StringPtr(ingress.Name),
+		Name: to.StringPtr(rule.Name),
 		SecurityRulePropertiesFormat: &network.SecurityRulePropertiesFormat{
-			Description:              to.StringPtr(ingress.Description),
-			SourceAddressPrefix:      ingress.Source,
-			SourcePortRange:          ingress.SourcePorts,
-			DestinationAddressPrefix: ingress.Destination,
-			DestinationPortRange:     ingress.DestinationPorts,
+			Description:              to.StringPtr(rule.Description),
+			SourceAddressPrefix:      rule.Source,
+			SourcePortRange:          rule.SourcePorts,
+			DestinationAddressPrefix: rule.Destination,
+			DestinationPortRange:     rule.DestinationPorts,
 			Access:                   network.SecurityRuleAccessAllow,
-			Direction:                network.SecurityRuleDirectionInbound,
-			Priority:                 to.Int32Ptr(ingress.Priority),
+			Priority:                 to.Int32Ptr(rule.Priority),
 		},
 	}
 
-	switch ingress.Protocol {
+	switch rule.Protocol {
 	case infrav1.SecurityGroupProtocolAll:
 		secRule.Protocol = network.SecurityRuleProtocolAsterisk
 	case infrav1.SecurityGroupProtocolTCP:
 		secRule.Protocol = network.SecurityRuleProtocolTCP
 	case infrav1.SecurityGroupProtocolUDP:
 		secRule.Protocol = network.SecurityRuleProtocolUDP
+	case infrav1.SecurityGroupProtocolICMP:
+		secRule.Protocol = network.SecurityRuleProtocolIcmp
 	}
 
-	return secRule
-}
-
-// SecuritytoIngressRule converts an Azure network security rule to a CAPI ingress rule.
-func SecuritytoIngressRule(rule network.SecurityRule) infrav1.IngressRule {
-	ingRule := infrav1.IngressRule{
-		Name:             to.String(rule.Name),
-		Description:      to.String(rule.Description),
-		Priority:         to.Int32(rule.Priority),
-		SourcePorts:      rule.SourcePortRange,
-		DestinationPorts: rule.DestinationPortRange,
-		Source:           rule.SourceAddressPrefix,
-		Destination:      rule.DestinationAddressPrefix,
-	}
-
-	switch rule.Protocol {
-	case network.SecurityRuleProtocolAsterisk:
-		ingRule.Protocol = infrav1.SecurityGroupProtocolAll
-	case network.SecurityRuleProtocolTCP:
-		ingRule.Protocol = infrav1.SecurityGroupProtocolTCP
-	case network.SecurityRuleProtocolUDP:
-		ingRule.Protocol = infrav1.SecurityGroupProtocolUDP
+	switch rule.Direction {
+	case infrav1.SecurityRuleDirectionOutbound:
+		secRule.Direction = network.SecurityRuleDirectionOutbound
+	case infrav1.SecurityRuleDirectionInbound:
+		secRule.Direction = network.SecurityRuleDirectionInbound
 	}
 
-	return ingRule
+	return secRule
 }
diff --git a/azure/scope/cluster.go b/azure/scope/cluster.go
index e264ae15c..a1d54eb2b 100644
--- a/azure/scope/cluster.go
+++ b/azure/scope/cluster.go
@@ -224,12 +224,12 @@ func (s *ClusterScope) RouteTableSpecs() []azure.RouteTableSpec {
 func (s *ClusterScope) NSGSpecs() []azure.NSGSpec {
 	return []azure.NSGSpec{
 		{
-			Name:         s.ControlPlaneSubnet().SecurityGroup.Name,
-			IngressRules: s.ControlPlaneSubnet().SecurityGroup.IngressRules,
+			Name:          s.ControlPlaneSubnet().SecurityGroup.Name,
+			SecurityRules: s.ControlPlaneSubnet().SecurityGroup.SecurityRules,
 		},
 		{
-			Name:         s.NodeSubnet().SecurityGroup.Name,
-			IngressRules: s.NodeSubnet().SecurityGroup.IngressRules,
+			Name:          s.NodeSubnet().SecurityGroup.Name,
+			SecurityRules: s.NodeSubnet().SecurityGroup.SecurityRules,
 		},
 	}
 }
@@ -510,26 +510,29 @@ func (s *ClusterScope) SetFailureDomain(id string, spec clusterv1.FailureDomainS
 	s.AzureCluster.Status.FailureDomains[id] = spec
 }
 
-// SetControlPlaneIngressRules will set the ingress rules or the control plane subnet
-func (s *ClusterScope) SetControlPlaneIngressRules() {
-	if s.ControlPlaneSubnet().SecurityGroup.IngressRules == nil {
+// SetControlPlaneSecurityRules sets the default security rules of the control plane subnet.
+// Note that this is not done in a webhook as it requires a valid Cluster object to exist to get the API Server port.
+func (s *ClusterScope) SetControlPlaneSecurityRules() {
+	if s.ControlPlaneSubnet().SecurityGroup.SecurityRules == nil {
 		subnet := s.ControlPlaneSubnet()
-		subnet.SecurityGroup.IngressRules = infrav1.IngressRules{
-			infrav1.IngressRule{
+		subnet.SecurityGroup.SecurityRules = infrav1.SecurityRules{
+			infrav1.SecurityRule{
 				Name:             "allow_ssh",
 				Description:      "Allow SSH",
 				Priority:         2200,
 				Protocol:         infrav1.SecurityGroupProtocolTCP,
+				Direction:        infrav1.SecurityRuleDirectionInbound,
 				Source:           to.StringPtr("*"),
 				SourcePorts:      to.StringPtr("*"),
 				Destination:      to.StringPtr("*"),
 				DestinationPorts: to.StringPtr("22"),
 			},
-			infrav1.IngressRule{
+			infrav1.SecurityRule{
 				Name:             "allow_apiserver",
 				Description:      "Allow K8s API Server",
 				Priority:         2201,
 				Protocol:         infrav1.SecurityGroupProtocolTCP,
+				Direction:        infrav1.SecurityRuleDirectionInbound,
 				Source:           to.StringPtr("*"),
 				SourcePorts:      to.StringPtr("*"),
 				Destination:      to.StringPtr("*"),
@@ -541,6 +544,7 @@ func (s *ClusterScope) SetControlPlaneIngressRules() {
 }
 
 // SetDNSName sets the API Server public IP DNS name.
+// Note: this logic exists only for purposes of ensuring backwards compatibility for old clusters created without an APIServerLB, and should be removed in the future.
 func (s *ClusterScope) SetDNSName() {
 	// for back compat, set the old API Server defaults if no API Server Spec has been set by new webhooks.
 	lb := s.APIServerLB()
@@ -564,6 +568,7 @@ func (s *ClusterScope) SetDNSName() {
 		lb.DeepCopyInto(s.APIServerLB())
 	}
 	// Generate valid FQDN if not set.
+	// Note: this function uses the AzureCluster subscription ID.
 	if !s.IsAPIServerPrivate() && s.APIServerPublicIP().DNSName == "" {
 		s.APIServerPublicIP().DNSName = s.GenerateFQDN(s.APIServerPublicIP().Name)
 	}
diff --git a/azure/services/securitygroups/securitygroups.go b/azure/services/securitygroups/securitygroups.go
index 06135c3e7..09be058e8 100644
--- a/azure/services/securitygroups/securitygroups.go
+++ b/azure/services/securitygroups/securitygroups.go
@@ -77,10 +77,11 @@ func (s *Service) Reconcile(ctx context.Context) error {
 			// Check if the expected rules are present
 			update := false
 			securityRules = *existingNSG.SecurityRules
-			for _, rule := range nsgSpec.IngressRules {
-				if !ruleExists(securityRules, converters.IngresstoSecurityRule(rule)) {
+			for _, rule := range nsgSpec.SecurityRules {
+				sdkRule := converters.SecurityRuleToSDK(rule)
+				if !ruleExists(securityRules, sdkRule) {
 					update = true
-					securityRules = append(securityRules, converters.IngresstoSecurityRule(rule))
+					securityRules = append(securityRules, sdkRule)
 				}
 			}
 			if !update {
@@ -90,8 +91,8 @@ func (s *Service) Reconcile(ctx context.Context) error {
 			}
 		default:
 			s.Scope.V(2).Info("creating security group", "security group", nsgSpec.Name)
-			for _, rule := range nsgSpec.IngressRules {
-				securityRules = append(securityRules, converters.IngresstoSecurityRule(rule))
+			for _, rule := range nsgSpec.SecurityRules {
+				securityRules = append(securityRules, converters.SecurityRuleToSDK(rule))
 			}
 
 		}
diff --git a/azure/types.go b/azure/types.go
index 654425bab..aaeb080d1 100644
--- a/azure/types.go
+++ b/azure/types.go
@@ -118,8 +118,8 @@ const (
 
 // NSGSpec defines the specification for a Security Group.
 type NSGSpec struct {
-	Name         string
-	IngressRules infrav1.IngressRules
+	Name          string
+	SecurityRules infrav1.SecurityRules
 }
 
 // VMSpec defines the specification for a Virtual Machine.
diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_azureclusters.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_azureclusters.yaml
index 73d7995a7..c6733bf84 100644
--- a/config/crd/bases/infrastructure.cluster.x-k8s.io_azureclusters.yaml
+++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_azureclusters.yaml
@@ -529,42 +529,56 @@ spec:
                           properties:
                             id:
                               type: string
-                            ingressRule:
-                              description: IngressRules is a slice of Azure ingress rules for security groups.
+                            name:
+                              type: string
+                            securityRules:
+                              description: SecurityRules is a slice of Azure security rules for security groups.
                               items:
-                                description: IngressRule defines an Azure ingress rule for security groups.
+                                description: SecurityRule defines an Azure security rule for security groups.
                                 properties:
                                   description:
+                                    description: A description for this rule. Restricted to 140 chars.
                                     type: string
                                   destination:
-                                    description: Destination - The destination address prefix. CIDR or destination IP range. Asterix '*' can also be used to match all source IPs. Default tags such as 'VirtualNetwork', 'AzureLoadBalancer' and 'Internet' can also be used.
+                                    description: Destination is the destination address prefix. CIDR or destination IP range. Asterix '*' can also be used to match all source IPs. Default tags such as 'VirtualNetwork', 'AzureLoadBalancer' and 'Internet' can also be used.
                                     type: string
                                   destinationPorts:
-                                    description: DestinationPorts - The destination port or range. Integer or range between 0 and 65535. Asterix '*' can also be used to match all ports.
+                                    description: DestinationPorts specifies the destination port or range. Integer or range between 0 and 65535. Asterix '*' can also be used to match all ports.
+                                    type: string
+                                  direction:
+                                    description: Direction indicates whether the rule applies to inbound, or outbound traffic. "Inbound" or "Outbound".
+                                    enum:
+                                    - Inbound
+                                    - Outbound
                                     type: string
                                   name:
+                                    description: Name is a unique name within the network security group.
                                     type: string
                                   priority:
-                                    description: Priority - A number between 100 and 4096. Each rule should have a unique value for priority. Rules are processed in priority order, with lower numbers processed before higher numbers. Once traffic matches a rule, processing stops.
+                                    description: Priority is a number between 100 and 4096. Each rule should have a unique value for priority. Rules are processed in priority order, with lower numbers processed before higher numbers. Once traffic matches a rule, processing stops.
                                     format: int32
                                     type: integer
                                   protocol:
-                                    description: SecurityGroupProtocol defines the protocol type for a security group rule.
+                                    description: Protocol specifies the protocol type. "Tcp", "Udp", "Icmp", or "*".
+                                    enum:
+                                    - Tcp
+                                    - Udp
+                                    - Icmp
+                                    - '*'
                                     type: string
                                   source:
-                                    description: Source - The CIDR or source IP range. Asterix '*' can also be used to match all source IPs. Default tags such as 'VirtualNetwork', 'AzureLoadBalancer' and 'Internet' can also be used. If this is an ingress rule, specifies where network traffic originates from.
+                                    description: Source specifies the CIDR or source IP range. Asterix '*' can also be used to match all source IPs. Default tags such as 'VirtualNetwork', 'AzureLoadBalancer' and 'Internet' can also be used. If this is an ingress rule, specifies where network traffic originates from.
                                     type: string
                                   sourcePorts:
-                                    description: SourcePorts - The source port or range. Integer or range between 0 and 65535. Asterix '*' can also be used to match all ports.
+                                    description: SourcePorts specifies source port or range. Integer or range between 0 and 65535. Asterix '*' can also be used to match all ports.
                                     type: string
                                 required:
                                 - description
+                                - direction
                                 - name
                                 - protocol
                                 type: object
                               type: array
-                            name:
-                              type: string
                             tags:
                               additionalProperties:
                                 type: string
diff --git a/controllers/azurecluster_reconciler.go b/controllers/azurecluster_reconciler.go
index 642162ed2..436e78201 100644
--- a/controllers/azurecluster_reconciler.go
+++ b/controllers/azurecluster_reconciler.go
@@ -83,7 +83,7 @@ func (s *azureClusterService) Reconcile(ctx context.Context) error {
 	}
 
 	s.scope.SetDNSName()
-	s.scope.SetControlPlaneIngressRules()
+	s.scope.SetControlPlaneSecurityRules()
 
 	if err := s.groupsSvc.Reconcile(ctx); err != nil {
 		return errors.Wrap(err, "failed to reconcile resource group")
@@ -163,6 +163,8 @@ func (s *azureClusterService) Delete(ctx context.Context) error {
 	return nil
 }
 
+// setFailureDomainsForLocation sets the AzureCluster Status failure domains based on which Azure Availability Zones are available in the cluster location.
+// Note that this is not done in a webhook as it requires API calls to fetch the availability zones.
 func (s *azureClusterService) setFailureDomainsForLocation(ctx context.Context) error {
 	zones, err := s.skuCache.GetZones(ctx, s.scope.Location())
 	if err != nil {
diff --git a/docs/book/src/topics/custom-vnet.md b/docs/book/src/topics/custom-vnet.md
index 6cb3bc541..4bb31f7d0 100644
--- a/docs/book/src/topics/custom-vnet.md
+++ b/docs/book/src/topics/custom-vnet.md
@@ -63,13 +63,21 @@ If no CIDR block is provided, `10.0.0.0/8` will be used by default, with default
 
 Whenever using custom vnet and subnet names and/or a different vnet resource group, please make sure to update the `azure.json` content part of both the nodes and control planes' `kubeadmConfigSpec` accordingly before creating the cluster.
 
-### Custom Ingress Rules
+### Custom Security Rules
 
-Ingress rules can also be customized as part of the subnet specification in a custom network spec.
-Note that ingress rules for the Kubernetes API Server port (default 6443) and SSH (22) are automatically added to the controlplane subnet only if Ingress Rules aren't specified.
-It is the responsibility of the user to supply those rules themselves if using custom ingresses.
+<aside class="note">
 
-Here is an illustrative example of customizing ingresses that builds on the one above by adding an ingress rule to the control plane nodes:
+<h1> Note </h1>
+
+Security Rules were previously known as `ingressRule` in v1alpha3.
+
+</aside>
+
+Security rules can also be customized as part of the subnet specification in a custom network spec.
+Note that ingress rules for the Kubernetes API Server port (default 6443) and SSH (22) are automatically added to the controlplane subnet only if security rules aren't specified.
+It is the responsibility of the user to supply those rules themselves if using custom rules.
+
+Here is an illustrative example of customizing rules that builds on the one above by adding an egress rule to the control plane nodes:
 
 ```yaml
 apiVersion: infrastructure.cluster.x-k8s.io/v1alpha4
@@ -91,9 +99,10 @@ spec:
           - 10.0.1.0/24
         securityGroup:
           name: my-subnet-cp-nsg
-          ingressRule:
+          securityRules:
             - name: "allow_ssh"
               description: "allow SSH"
+              direction: "Inbound"
               priority: 2200
               protocol: "*"
               destination: "*"
@@ -102,6 +111,7 @@ spec:
               sourcePorts: "*"
             - name: "allow_apiserver"
               description: "Allow K8s API Server"
+              direction: "Inbound"
               priority: 2201
               protocol: "*"
               destination: "*"
@@ -110,8 +120,9 @@ spec:
               sourcePorts: "*"
             - name: "allow_port_50000"
               description: "allow port 50000"
+              direction: "Outbound"
               priority: 2202
-              protocol: "*"
+              protocol: "Tcp"
               destination: "*"
               destinationPorts: "50000"
               source: "*"


Test patch:
diff --git a/api/v1alpha3/conversion_test.go b/api/v1alpha3/conversion_test.go
index 2b9aec048..b5dc2f66a 100644
--- a/api/v1alpha3/conversion_test.go
+++ b/api/v1alpha3/conversion_test.go
@@ -17,10 +17,9 @@ limitations under the License.
 package v1alpha3
 
 import (
-	"testing"
-
 	fuzz "github.com/google/gofuzz"
 	. "github.com/onsi/gomega"
+	"testing"
 
 	"k8s.io/apimachinery/pkg/api/apitesting/fuzzer"
 	"k8s.io/apimachinery/pkg/runtime"
diff --git a/api/v1alpha4/azurecluster_default_test.go b/api/v1alpha4/azurecluster_default_test.go
index 464193ebd..830997383 100644
--- a/api/v1alpha4/azurecluster_default_test.go
+++ b/api/v1alpha4/azurecluster_default_test.go
@@ -21,6 +21,8 @@ import (
 	"reflect"
 	"testing"
 
+	"github.com/Azure/go-autorest/autorest/to"
+
 	"k8s.io/utils/pointer"
 
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -527,6 +529,78 @@ func TestSubnetDefaults(t *testing.T) {
 				},
 			},
 		},
+		{
+			name: "subnets with custom security group",
+			cluster: &AzureCluster{
+				ObjectMeta: v1.ObjectMeta{
+					Name: "cluster-test",
+				},
+				Spec: AzureClusterSpec{
+					NetworkSpec: NetworkSpec{
+						Subnets: Subnets{
+							{
+								Name: "cluster-test-controlplane-subnet",
+								Role: "control-plane",
+								SecurityGroup: SecurityGroup{
+									Name: "my-custom-sg",
+									SecurityRules: []SecurityRule{
+										{
+											Name:             "allow_port_50000",
+											Description:      "allow port 50000",
+											Protocol:         "*",
+											Priority:         2202,
+											SourcePorts:      to.StringPtr("*"),
+											DestinationPorts: to.StringPtr("*"),
+											Source:           to.StringPtr("*"),
+											Destination:      to.StringPtr("*"),
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+			output: &AzureCluster{
+				ObjectMeta: v1.ObjectMeta{
+					Name: "cluster-test",
+				},
+				Spec: AzureClusterSpec{
+					NetworkSpec: NetworkSpec{
+						Subnets: Subnets{
+							{
+								Name:       "cluster-test-controlplane-subnet",
+								Role:       "control-plane",
+								CIDRBlocks: []string{DefaultControlPlaneSubnetCIDR},
+								SecurityGroup: SecurityGroup{
+									Name: "my-custom-sg",
+									SecurityRules: []SecurityRule{
+										{
+											Name:             "allow_port_50000",
+											Description:      "allow port 50000",
+											Protocol:         "*",
+											Priority:         2202,
+											SourcePorts:      to.StringPtr("*"),
+											DestinationPorts: to.StringPtr("*"),
+											Source:           to.StringPtr("*"),
+											Destination:      to.StringPtr("*"),
+											Direction:        SecurityRuleDirectionInbound,
+										},
+									},
+								},
+							},
+							{
+								Role:          SubnetNode,
+								Name:          "cluster-test-node-subnet",
+								CIDRBlocks:    []string{DefaultNodeSubnetCIDR},
+								SecurityGroup: SecurityGroup{Name: "cluster-test-node-nsg"},
+								RouteTable:    RouteTable{Name: "cluster-test-node-routetable"},
+							},
+						},
+					},
+				},
+			},
+		},
 	}
 
 	for _, c := range cases {
diff --git a/api/v1alpha4/azurecluster_validation_test.go b/api/v1alpha4/azurecluster_validation_test.go
index 677017758..2b3c2e328 100644
--- a/api/v1alpha4/azurecluster_validation_test.go
+++ b/api/v1alpha4/azurecluster_validation_test.go
@@ -507,17 +507,17 @@ func TestSubnetNameInvalid(t *testing.T) {
 	})
 }
 
-func TestIngressRules(t *testing.T) {
+func TestValidateSecurityRule(t *testing.T) {
 	g := NewWithT(t)
 
 	tests := []struct {
 		name      string
-		validRule IngressRule
+		validRule SecurityRule
 		wantErr   bool
 	}{
 		{
-			name: "ingressRule - valid priority",
-			validRule: IngressRule{
+			name: "security rule - valid priority",
+			validRule: SecurityRule{
 				Name:        "allow_apiserver",
 				Description: "Allow K8s API Server",
 				Priority:    101,
@@ -525,8 +525,8 @@ func TestIngressRules(t *testing.T) {
 			wantErr: false,
 		},
 		{
-			name: "ingressRule - invalid low priority",
-			validRule: IngressRule{
+			name: "security rule - invalid low priority",
+			validRule: SecurityRule{
 				Name:        "allow_apiserver",
 				Description: "Allow K8s API Server",
 				Priority:    99,
@@ -534,8 +534,8 @@ func TestIngressRules(t *testing.T) {
 			wantErr: true,
 		},
 		{
-			name: "ingressRule - invalid high priority",
-			validRule: IngressRule{
+			name: "security rule - invalid high priority",
+			validRule: SecurityRule{
 				Name:        "allow_apiserver",
 				Description: "Allow K8s API Server",
 				Priority:    5000,
@@ -547,9 +547,9 @@ func TestIngressRules(t *testing.T) {
 		testCase := testCase
 		t.Run(testCase.name, func(t *testing.T) {
 			t.Parallel()
-			err := validateIngressRule(
+			err := validateSecurityRule(
 				testCase.validRule,
-				field.NewPath("spec").Child("networkSpec").Child("subnets").Index(0).Child("securityGroup").Child("ingressRules").Index(0),
+				field.NewPath("spec").Child("networkSpec").Child("subnets").Index(0).Child("securityGroup").Child("securityRules").Index(0),
 			)
 			if testCase.wantErr {
 				g.Expect(err).To(HaveOccurred())
diff --git a/azure/scope/cluster_test.go b/azure/scope/cluster_test.go
index 0e920fa83..a5f61c50c 100644
--- a/azure/scope/cluster_test.go
+++ b/azure/scope/cluster_test.go
@@ -31,7 +31,7 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/client/fake"
 )
 
-func TestGettingIngressRules(t *testing.T) {
+func TestGettingSecurityRules(t *testing.T) {
 	g := NewWithT(t)
 	scheme := runtime.NewScheme()
 	_ = clusterv1.AddToScheme(scheme)
@@ -83,9 +83,9 @@ func TestGettingIngressRules(t *testing.T) {
 	})
 	g.Expect(err).ToNot(HaveOccurred())
 
-	clusterScope.SetControlPlaneIngressRules()
+	clusterScope.SetControlPlaneSecurityRules()
 
 	subnet, err := clusterScope.AzureCluster.Spec.NetworkSpec.GetControlPlaneSubnet()
 	g.Expect(err).ToNot(HaveOccurred())
-	g.Expect(len(subnet.SecurityGroup.IngressRules)).To(Equal(2))
+	g.Expect(len(subnet.SecurityGroup.SecurityRules)).To(Equal(2))
 }
diff --git a/azure/services/securitygroups/securitygroups_test.go b/azure/services/securitygroups/securitygroups_test.go
index d0aad2690..41cdb4f45 100644
--- a/azure/services/securitygroups/securitygroups_test.go
+++ b/azure/services/securitygroups/securitygroups_test.go
@@ -45,32 +45,34 @@ func TestReconcileSecurityGroups(t *testing.T) {
 				s.NSGSpecs().Return([]azure.NSGSpec{
 					{
 						Name: "nsg-one",
-						IngressRules: infrav1.IngressRules{
+						SecurityRules: infrav1.SecurityRules{
 							{
 								Name:             "first-rule",
 								Description:      "a test rule",
-								Protocol:         "*",
+								Protocol:         infrav1.SecurityGroupProtocolAll,
 								Priority:         400,
 								SourcePorts:      to.StringPtr("*"),
 								DestinationPorts: to.StringPtr("*"),
 								Source:           to.StringPtr("*"),
 								Destination:      to.StringPtr("*"),
+								Direction:        infrav1.SecurityRuleDirectionInbound,
 							},
 							{
 								Name:             "second-rule",
 								Description:      "another test rule",
-								Protocol:         "*",
+								Protocol:         infrav1.SecurityGroupProtocolAll,
 								Priority:         450,
 								SourcePorts:      to.StringPtr("*"),
 								DestinationPorts: to.StringPtr("*"),
 								Source:           to.StringPtr("*"),
 								Destination:      to.StringPtr("*"),
+								Direction:        infrav1.SecurityRuleDirectionInbound,
 							},
 						},
 					},
 					{
-						Name:         "nsg-two",
-						IngressRules: infrav1.IngressRules{},
+						Name:          "nsg-two",
+						SecurityRules: infrav1.SecurityRules{},
 					},
 				})
 				s.IsVnetManaged().Return(true)
@@ -129,7 +131,7 @@ func TestReconcileSecurityGroups(t *testing.T) {
 				s.NSGSpecs().Return([]azure.NSGSpec{
 					{
 						Name: "nsg-one",
-						IngressRules: infrav1.IngressRules{
+						SecurityRules: infrav1.SecurityRules{
 							{
 								Name:             "first-rule",
 								Description:      "a test rule",
@@ -139,12 +141,13 @@ func TestReconcileSecurityGroups(t *testing.T) {
 								DestinationPorts: to.StringPtr("*"),
 								Source:           to.StringPtr("*"),
 								Destination:      to.StringPtr("*"),
+								Direction:        infrav1.SecurityRuleDirectionOutbound,
 							},
 						},
 					},
 					{
-						Name:         "nsg-two",
-						IngressRules: infrav1.IngressRules{},
+						Name:          "nsg-two",
+						SecurityRules: infrav1.SecurityRules{},
 					},
 				})
 				s.IsVnetManaged().AnyTimes().Return(true)
@@ -200,7 +203,7 @@ func TestReconcileSecurityGroups(t *testing.T) {
 									SourceAddressPrefix:      to.StringPtr("*"),
 									DestinationAddressPrefix: to.StringPtr("*"),
 									Protocol:                 "*",
-									Direction:                "Inbound",
+									Direction:                "Outbound",
 									Access:                   "Allow",
 									Priority:                 to.Int32Ptr(400),
 								},
@@ -263,7 +266,7 @@ func TestDeleteSecurityGroups(t *testing.T) {
 				s.NSGSpecs().Return([]azure.NSGSpec{
 					{
 						Name: "nsg-one",
-						IngressRules: infrav1.IngressRules{
+						SecurityRules: infrav1.SecurityRules{
 							{
 								Name:             "first-rule",
 								Description:      "a test rule",
@@ -273,12 +276,13 @@ func TestDeleteSecurityGroups(t *testing.T) {
 								DestinationPorts: to.StringPtr("*"),
 								Source:           to.StringPtr("*"),
 								Destination:      to.StringPtr("*"),
+								Direction:        infrav1.SecurityRuleDirectionInbound,
 							},
 						},
 					},
 					{
-						Name:         "nsg-two",
-						IngressRules: infrav1.IngressRules{},
+						Name:          "nsg-two",
+						SecurityRules: infrav1.SecurityRules{},
 					},
 				})
 				s.ResourceGroup().AnyTimes().Return("my-rg")
@@ -293,12 +297,12 @@ func TestDeleteSecurityGroups(t *testing.T) {
 			expect: func(s *mock_securitygroups.MockNSGScopeMockRecorder, m *mock_securitygroups.MockclientMockRecorder) {
 				s.NSGSpecs().Return([]azure.NSGSpec{
 					{
-						Name:         "nsg-one",
-						IngressRules: infrav1.IngressRules{},
+						Name:          "nsg-one",
+						SecurityRules: infrav1.SecurityRules{},
 					},
 					{
-						Name:         "nsg-two",
-						IngressRules: infrav1.IngressRules{},
+						Name:          "nsg-two",
+						SecurityRules: infrav1.SecurityRules{},
 					},
 				})
 				s.ResourceGroup().AnyTimes().Return("my-rg")