Repository: kubernetes-sigs/cluster-api-provider-aws
Pull request #4384 (2023-07-07 16:46:12)

IMDSv2 on AWSManagedMachinePool
<!-- Thanks for this PR! If this is your first PR please read the [contributing guide](../CONTRIBUTING.md) -->
<!-- If this PR is still work-in-progress and is being open for visibility please prefix the title with `WIP:` -->

**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 regression
/kind support
-->
/kind feature

**What this PR does / why we need it**:

<!-- Enter a description of the change and why this change is needed -->

IMDSv2 is currently only supported on AWSMachine clusters. This means that we cannot ensure authenticated calls to EC2 Metadata Service on AWSManagedClusters which use AWS's managed node groups.
This change enables adding InstanceMetadataOptions to LaunchTemplates, which is the only currently supported way to enforced IMDSv2 on EKS clusters with managed node groups.

**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 #

**Special notes for your reviewer**:

**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
- [x] 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
Added support for enforcing IMDSv2 on managed node groups with AWSManagedMachinePool
```


k8s-ci-robot: Adding the "do-not-merge/release-note-label-needed" label because no release-note block was detected, please follow our [release note process](https://git.k8s.io/community/contributors/guide/release-notes.md) to remove it.

<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: Hi @stefanmcshane. 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: Adding label `do-not-merge/contains-merge-commits` because PR contains merge commits, which are not allowed in this repository.
Use `git rebase` to reapply your commits on top of the target branch. Detailed instructions for doing so can be found [here](https://git.k8s.io/community/contributors/guide/github-workflow.md#4-keep-your-branch-in-sync).

<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: [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 [skarlso](https://github.com/skarlso) 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":["skarlso"]} -->
stefanmcshane: I believe that this is ready to go. I will squash commits after addressing any feedback that comes up.
richardcase: /milestone v2.3.0
stefanmcshane: @richardcase is there anything that I need to add to this in order to get it merged ahead of the next release?
k8s-ci-robot: Adding label `do-not-merge/contains-merge-commits` because PR contains merge commits, which are not allowed in this repository.
Use `git rebase` to reapply your commits on top of the target branch. Detailed instructions for doing so can be found [here](https://git.k8s.io/community/contributors/guide/github-workflow.md#4-keep-your-branch-in-sync).

<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: @stefanmcshane: 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-verify | 9b656e0282e3de0a58de2e2d951aa1aac867f5d2 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-aws/4384/pull-cluster-api-provider-aws-verify/1690036654988857344) | true | `/test pull-cluster-api-provider-aws-verify`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api-provider-aws&pr=4384). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Astefanmcshane). 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 -->
Skarlso: /test ?
k8s-ci-robot: @Skarlso: The following commands are available to trigger required jobs:
* `/test pull-cluster-api-provider-aws-build`
* `/test pull-cluster-api-provider-aws-test`
* `/test pull-cluster-api-provider-aws-verify`

The following commands are available to trigger optional jobs:
* `/test pull-cluster-api-provider-aws-apidiff-main`
* `/test pull-cluster-api-provider-aws-e2e`
* `/test pull-cluster-api-provider-aws-e2e-blocking`
* `/test pull-cluster-api-provider-aws-e2e-clusterclass`
* `/test pull-cluster-api-provider-aws-e2e-conformance`
* `/test pull-cluster-api-provider-aws-e2e-conformance-with-ci-artifacts`
* `/test pull-cluster-api-provider-aws-e2e-eks`
* `/test pull-cluster-api-provider-aws-e2e-eks-gc`
* `/test pull-cluster-api-provider-aws-e2e-eks-testing`

Use `/test all` to run the following jobs that were automatically triggered:
* `pull-cluster-api-provider-aws-apidiff-main`
* `pull-cluster-api-provider-aws-build`
* `pull-cluster-api-provider-aws-test`
* `pull-cluster-api-provider-aws-verify`



<details>

In response to [this](https://github.com/kubernetes-sigs/cluster-api-provider-aws/pull/4384#issuecomment-1675236812):

>/test ?


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>
Skarlso: /test pull-cluster-api-provider-aws-e2e
/test pull-cluster-api-provider-aws-e2e-eks
k8s-ci-robot: @stefanmcshane: 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 | fd80105a8221f0255a10106a15355d53102f6b69 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-aws/4384/pull-cluster-api-provider-aws-e2e/1690076800585043968) | false | `/test pull-cluster-api-provider-aws-e2e`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api-provider-aws&pr=4384). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Astefanmcshane). 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 -->
stefanmcshane: /retest

This doesnt seem related to the changeset
k8s-ci-robot: @stefanmcshane: 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 | fd80105a8221f0255a10106a15355d53102f6b69 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-aws/4384/pull-cluster-api-provider-aws-e2e/1690105292798300160) | false | `/test pull-cluster-api-provider-aws-e2e`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api-provider-aws&pr=4384). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Astefanmcshane). 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 -->
stefanmcshane: /retest
k8s-ci-robot: @stefanmcshane: 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 | fd80105a8221f0255a10106a15355d53102f6b69 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-aws/4384/pull-cluster-api-provider-aws-e2e/1691122941036597248) | false | `/test pull-cluster-api-provider-aws-e2e`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api-provider-aws&pr=4384). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Astefanmcshane). 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 -->
stefanmcshane: /retest
k8s-ci-robot: @stefanmcshane: 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 | fd80105a8221f0255a10106a15355d53102f6b69 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-aws/4384/pull-cluster-api-provider-aws-e2e/1691846686621569024) | false | `/test pull-cluster-api-provider-aws-e2e`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api-provider-aws&pr=4384). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Astefanmcshane). 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 -->
richardcase: /test pull-cluster-api-provider-aws-e2e
richardcase: > @richardcase is there anything that I need to add to this in order to get it merged ahead of the next release?

Sorry @stefanmcshane i've been off on PTO. I just kicked off the e2e tests again to see if they pass.
k8s-ci-robot: @stefanmcshane: 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 | fd80105a8221f0255a10106a15355d53102f6b69 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-aws/4384/pull-cluster-api-provider-aws-e2e/1692068219252117504) | false | `/test pull-cluster-api-provider-aws-e2e`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api-provider-aws&pr=4384). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Astefanmcshane). 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 -->
richardcase: Ah that rate limiting, a PITA.

/test pull-cluster-api-provider-aws-e2e
k8s-ci-robot: @stefanmcshane: 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 | fd80105a8221f0255a10106a15355d53102f6b69 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api-provider-aws/4384/pull-cluster-api-provider-aws-e2e/1692100005826924544) | false | `/test pull-cluster-api-provider-aws-e2e`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api-provider-aws&pr=4384). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Astefanmcshane). 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 -->
stefanmcshane: /retest

@richardcase I hope that you had a great PTO. I've just rebased the changes since they last passed and have been hitting that issue since.
Skarlso: /lgtm
/approve
k8s-ci-robot: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubernetes-sigs/cluster-api-provider-aws/pull/4384#issuecomment-1696892525" title="Approved">Skarlso</a>*

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

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

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

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

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":[]} -->
k8s-ci-robot: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubernetes-sigs/cluster-api-provider-aws/pull/4384#issuecomment-1698626360" title="Approved">Skarlso</a>*

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

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

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

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

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":[]} -->
k8s-ci-robot: New changes are detected. LGTM label has been removed.
k8s-ci-robot: [APPROVALNOTIFIER] This PR is **NOT APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubernetes-sigs/cluster-api-provider-aws/pull/4384#issuecomment-1698626360" title="Approved">Skarlso</a>*


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:


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":[]} -->

Patch:
diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachinepools.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachinepools.yaml
index 6a0ba2d7e..f49fa9af8 100644
--- a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachinepools.yaml
+++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachinepools.yaml
@@ -623,6 +623,59 @@ spec:
                     description: ImageLookupOrg is the AWS Organization ID to use
                       for image lookup if AMI is not set.
                     type: string
+                  instanceMetadataOptions:
+                    description: InstanceMetadataOptions defines the behavior for
+                      applying metadata to instances.
+                    properties:
+                      httpEndpoint:
+                        default: enabled
+                        description: "Enables or disables the HTTP metadata endpoint
+                          on your instances. \n If you specify a value of disabled,
+                          you cannot access your instance metadata. \n Default: enabled"
+                        enum:
+                        - enabled
+                        - disabled
+                        type: string
+                      httpPutResponseHopLimit:
+                        default: 1
+                        description: "The desired HTTP PUT response hop limit for
+                          instance metadata requests. The larger the number, the further
+                          instance metadata requests can travel. \n Default: 1"
+                        format: int64
+                        maximum: 64
+                        minimum: 1
+                        type: integer
+                      httpTokens:
+                        default: optional
+                        description: "The state of token usage for your instance metadata
+                          requests. \n If the state is optional, you can choose to
+                          retrieve instance metadata with or without a session token
+                          on your request. If you retrieve the IAM role credentials
+                          without a token, the version 1.0 role credentials are returned.
+                          If you retrieve the IAM role credentials using a valid session
+                          token, the version 2.0 role credentials are returned. \n
+                          If the state is required, you must send a session token
+                          with any instance metadata retrieval requests. In this state,
+                          retrieving the IAM role credentials always returns the version
+                          2.0 credentials; the version 1.0 credentials are not available.
+                          \n Default: optional"
+                        enum:
+                        - optional
+                        - required
+                        type: string
+                      instanceMetadataTags:
+                        default: disabled
+                        description: "Set to enabled to allow access to instance tags
+                          from the instance metadata. Set to disabled to turn off
+                          access to instance tags from the instance metadata. For
+                          more information, see Work with instance tags using the
+                          instance metadata (https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/Using_Tags.html#work-with-tags-in-IMDS).
+                          \n Default: disabled"
+                        enum:
+                        - enabled
+                        - disabled
+                        type: string
+                    type: object
                   instanceType:
                     description: 'InstanceType is the type of instance to create.
                       Example: m4.xlarge'
diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmanagedmachinepools.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmanagedmachinepools.yaml
index 0363f9906..0ddc44155 100644
--- a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmanagedmachinepools.yaml
+++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmanagedmachinepools.yaml
@@ -611,6 +611,59 @@ spec:
                     description: ImageLookupOrg is the AWS Organization ID to use
                       for image lookup if AMI is not set.
                     type: string
+                  instanceMetadataOptions:
+                    description: InstanceMetadataOptions defines the behavior for
+                      applying metadata to instances.
+                    properties:
+                      httpEndpoint:
+                        default: enabled
+                        description: "Enables or disables the HTTP metadata endpoint
+                          on your instances. \n If you specify a value of disabled,
+                          you cannot access your instance metadata. \n Default: enabled"
+                        enum:
+                        - enabled
+                        - disabled
+                        type: string
+                      httpPutResponseHopLimit:
+                        default: 1
+                        description: "The desired HTTP PUT response hop limit for
+                          instance metadata requests. The larger the number, the further
+                          instance metadata requests can travel. \n Default: 1"
+                        format: int64
+                        maximum: 64
+                        minimum: 1
+                        type: integer
+                      httpTokens:
+                        default: optional
+                        description: "The state of token usage for your instance metadata
+                          requests. \n If the state is optional, you can choose to
+                          retrieve instance metadata with or without a session token
+                          on your request. If you retrieve the IAM role credentials
+                          without a token, the version 1.0 role credentials are returned.
+                          If you retrieve the IAM role credentials using a valid session
+                          token, the version 2.0 role credentials are returned. \n
+                          If the state is required, you must send a session token
+                          with any instance metadata retrieval requests. In this state,
+                          retrieving the IAM role credentials always returns the version
+                          2.0 credentials; the version 1.0 credentials are not available.
+                          \n Default: optional"
+                        enum:
+                        - optional
+                        - required
+                        type: string
+                      instanceMetadataTags:
+                        default: disabled
+                        description: "Set to enabled to allow access to instance tags
+                          from the instance metadata. Set to disabled to turn off
+                          access to instance tags from the instance metadata. For
+                          more information, see Work with instance tags using the
+                          instance metadata (https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/Using_Tags.html#work-with-tags-in-IMDS).
+                          \n Default: disabled"
+                        enum:
+                        - enabled
+                        - disabled
+                        type: string
+                    type: object
                   instanceType:
                     description: 'InstanceType is the type of instance to create.
                       Example: m4.xlarge'
diff --git a/docs/book/src/topics/instance-metadata.md b/docs/book/src/topics/instance-metadata.md
index bd0dd9862..c1970b942 100644
--- a/docs/book/src/topics/instance-metadata.md
+++ b/docs/book/src/topics/instance-metadata.md
@@ -2,8 +2,8 @@
 
 Instance metadata is data about your instance that you can use to configure or manage the running instance which you can access from a running instance using one of the following methods:
 
-* Instance Metadata Service Version 1 (IMDSv1) – a request/response method
-* Instance Metadata Service Version 2 (IMDSv2) – a session-oriented method
+- Instance Metadata Service Version 1 (IMDSv1) – a request/response method
+- Instance Metadata Service Version 2 (IMDSv2) – a session-oriented method
 
 CAPA defaults to use IMDSv2 as optional property when creating instances.
 
@@ -12,6 +12,7 @@ CAPA expose options to configure IMDSv2 as required when creating instances, as
 It is possible to configure the instance metadata options using the field called `instanceMetadataOptions` in the `AWSMachineTemplate`.
 
 Example:
+
 ```yaml
 ---
 apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
@@ -31,8 +32,66 @@ spec:
 To use IMDSv2, simply set `httpTokens` value to `required` (in other words, set the use of IMDSv2 to required).
 To use IMDSv2, please also set `httpPutResponseHopLimit` value to `2`, as it is recommended in container environment according to [AWS document](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instancedata-data-retrieval.html#imds-considerations).
 
+Similarly, this can be done with `AWSManagedMachinePool` for use with EKS Managed Nodegroups. One slight difference here is that you [must use Launch Templates to configure IMDSv2 with Autoscaling Groups](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-metadata-transition-to-version-2.html). In order to configure the LaunchTemplate, you must use a custom AMI type according to the AWS API. This can be done by setting `AWSManagedMachinePool.spec.amiType` to `CUSTOM`. This change means that you must also specify a bootstrapping script to the worker node, which allows it to be joined to the EKS cluster. The default AWS Managed Node Group bootstrap script can be found [here on Github](https://github.com/awslabs/amazon-eks-ami/blob/master/files/bootstrap.sh).
+
+The following example will use the default Amazon EKS Worker Node AMI which includes the default EKS Bootstrapping script. This must be installed on the management cluster as a Secret, under the key `value`. The secret's name must then be included in your `MachinePool` manifest at `MachinePool.spec.template.spec.bootstrap.dataSecretName`. Some assumptions are made for this example:
+
+- Your cluster name is `capi-imds`, which CAPA renames to `default_capi-imds-control-plane` automatically
+- Your cluster is Kubernetes Version `v1.25.9`
+- Your `AWSManagedCluster` is deployed in the `default` namespace along with the bootstrap secret `eks-bootstrap`
+
+```yaml
+kind: Secret
+apiVersion: v1
+type: Opaque
+data:
+  value: IyEvYmluL2Jhc2ggLXhlCi9ldGMvZWtzL2Jvb3RzdHJhcC5zaCBkZWZhdWx0X2NhcGktaW1kcy1jb250cm9sLXBsYW5l
+metadata:
+  name: eks-bootstrap
+---
+apiVersion: infrastructure.cluster.x-k8s.io/v1beta2
+kind: AWSManagedMachinePool
+metadata:
+  name: "capi-imds-pool-launchtemplate"
+spec:
+  amiType: CUSTOM
+  awsLaunchTemplate:
+    name: my-aws-launch-template
+    instanceType: t3.nano
+    metadataOptions:
+      httpTokens: required
+      httpPutResponseHopLimit: 2
+---
+apiVersion: cluster.x-k8s.io/v1beta1
+kind: MachinePool
+metadata:
+  name: "capi-imds-pool-1"
+spec:
+  clusterName: "capi-imds"
+  replicas: 1
+  template:
+    spec:
+      version: v1.25.9
+      clusterName: "capi-imds"
+      bootstrap:
+        dataSecretName: "eks-bootstrap"
+      infrastructureRef:
+        name: "capi-imds-pool-launchtemplate"
+        apiVersion: infrastructure.cluster.x-k8s.io/v1beta2
+        kind: AWSManagedMachinePool
+```
+
+`IyEvYmluL2Jhc2ggLXhlCi9ldGMvZWtzL2Jvb3RzdHJhcC5zaCBkZWZhdWx0X2NhcGktaW1kcy1jb250cm9sLXBsYW5l` in the above secret is a Base64 encoded version of the following script:
+
+```bash
+#!/bin/bash -xe
+/etc/eks/bootstrap.sh default_capi-imds-control-plane
+```
+
+If your cluster is not named `default_capi-imds-control-plane` in the AWS EKS console, you must update the name and store it as a Secret again.
+
 See [the CLI command reference](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/ec2/modify-instance-metadata-options.html) for more information.
 
-Before you decide to use IMDSv2 for the cluster instances, please make sure all your applications are compatible to IMDSv2. 
+Before you decide to use IMDSv2 for the cluster instances, please make sure all your applications are compatible with IMDSv2.
 
 See the [transition guide](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-metadata-transition-to-version-2.html#recommended-path-for-requiring-imdsv2) for more information.
diff --git a/exp/api/v1beta1/conversion.go b/exp/api/v1beta1/conversion.go
index a018bdc11..1e4304eb1 100644
--- a/exp/api/v1beta1/conversion.go
+++ b/exp/api/v1beta1/conversion.go
@@ -44,6 +44,9 @@ func (src *AWSMachinePool) ConvertTo(dstRaw conversion.Hub) error {
 	if dst.Spec.RefreshPreferences != nil && restored.Spec.RefreshPreferences != nil {
 		dst.Spec.RefreshPreferences.Disable = restored.Spec.RefreshPreferences.Disable
 	}
+	if restored.Spec.AWSLaunchTemplate.InstanceMetadataOptions != nil {
+		dst.Spec.AWSLaunchTemplate.InstanceMetadataOptions = restored.Spec.AWSLaunchTemplate.InstanceMetadataOptions
+	}
 
 	return nil
 }
@@ -78,6 +81,18 @@ func (src *AWSManagedMachinePool) ConvertTo(dstRaw conversion.Hub) error {
 	if err := Convert_v1beta1_AWSManagedMachinePool_To_v1beta2_AWSManagedMachinePool(src, dst, nil); err != nil {
 		return err
 	}
+	// Manually restore data.
+	restored := &infrav1exp.AWSManagedMachinePool{}
+	if ok, err := utilconversion.UnmarshalData(src, restored); err != nil || !ok {
+		return err
+	}
+
+	if restored.Spec.AWSLaunchTemplate != nil {
+		if dst.Spec.AWSLaunchTemplate == nil {
+			dst.Spec.AWSLaunchTemplate = restored.Spec.AWSLaunchTemplate
+		}
+		dst.Spec.AWSLaunchTemplate.InstanceMetadataOptions = restored.Spec.AWSLaunchTemplate.InstanceMetadataOptions
+	}
 
 	return nil
 }
@@ -90,7 +105,7 @@ func (r *AWSManagedMachinePool) ConvertFrom(srcRaw conversion.Hub) error {
 		return err
 	}
 
-	return nil
+	return utilconversion.MarshalData(src, r)
 }
 
 // Convert_v1beta2_AWSManagedMachinePoolSpec_To_v1beta1_AWSManagedMachinePoolSpec is a conversion function.
diff --git a/exp/api/v1beta1/zz_generated.conversion.go b/exp/api/v1beta1/zz_generated.conversion.go
index 14a33e888..1e924b46c 100644
--- a/exp/api/v1beta1/zz_generated.conversion.go
+++ b/exp/api/v1beta1/zz_generated.conversion.go
@@ -406,6 +406,7 @@ func autoConvert_v1beta2_AWSLaunchTemplate_To_v1beta1_AWSLaunchTemplate(in *v1be
 	out.VersionNumber = (*int64)(unsafe.Pointer(in.VersionNumber))
 	out.AdditionalSecurityGroups = *(*[]apiv1beta2.AWSResourceReference)(unsafe.Pointer(&in.AdditionalSecurityGroups))
 	out.SpotMarketOptions = (*apiv1beta2.SpotMarketOptions)(unsafe.Pointer(in.SpotMarketOptions))
+	// WARNING: in.InstanceMetadataOptions requires manual conversion: does not exist in peer-type
 	return nil
 }
 
diff --git a/exp/api/v1beta2/types.go b/exp/api/v1beta2/types.go
index e0c14ce12..6c527f6b1 100644
--- a/exp/api/v1beta2/types.go
+++ b/exp/api/v1beta2/types.go
@@ -122,6 +122,10 @@ type AWSLaunchTemplate struct {
 
 	// SpotMarketOptions are options for configuring AWSMachinePool instances to be run using AWS Spot instances.
 	SpotMarketOptions *infrav1.SpotMarketOptions `json:"spotMarketOptions,omitempty"`
+
+	// InstanceMetadataOptions defines the behavior for applying metadata to instances.
+	// +optional
+	InstanceMetadataOptions *infrav1.InstanceMetadataOptions `json:"instanceMetadataOptions,omitempty"`
 }
 
 // Overrides are used to override the instance type specified by the launch template with multiple
@@ -217,10 +221,8 @@ type AutoScalingGroup struct {
 // ASGStatus is a status string returned by the autoscaling API.
 type ASGStatus string
 
-var (
-	// ASGStatusDeleteInProgress is the string representing an ASG that is currently deleting.
-	ASGStatusDeleteInProgress = ASGStatus("Delete in progress")
-)
+// ASGStatusDeleteInProgress is the string representing an ASG that is currently deleting.
+var ASGStatusDeleteInProgress = ASGStatus("Delete in progress")
 
 // TaintEffect is the effect for a Kubernetes taint.
 type TaintEffect string
diff --git a/exp/api/v1beta2/zz_generated.deepcopy.go b/exp/api/v1beta2/zz_generated.deepcopy.go
index daa47353f..0c71e13d5 100644
--- a/exp/api/v1beta2/zz_generated.deepcopy.go
+++ b/exp/api/v1beta2/zz_generated.deepcopy.go
@@ -118,6 +118,11 @@ func (in *AWSLaunchTemplate) DeepCopyInto(out *AWSLaunchTemplate) {
 		*out = new(apiv1beta2.SpotMarketOptions)
 		(*in).DeepCopyInto(*out)
 	}
+	if in.InstanceMetadataOptions != nil {
+		in, out := &in.InstanceMetadataOptions, &out.InstanceMetadataOptions
+		*out = new(apiv1beta2.InstanceMetadataOptions)
+		**out = **in
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AWSLaunchTemplate.
diff --git a/pkg/cloud/services/ec2/launchtemplate.go b/pkg/cloud/services/ec2/launchtemplate.go
index 5355660d9..561eaf894 100644
--- a/pkg/cloud/services/ec2/launchtemplate.go
+++ b/pkg/cloud/services/ec2/launchtemplate.go
@@ -456,6 +456,20 @@ func (s *Service) createLaunchTemplateData(scope scope.LaunchTemplateScope, imag
 		UserData:     pointer.String(base64.StdEncoding.EncodeToString(userData)),
 	}
 
+	if lt.InstanceMetadataOptions != nil {
+		data.MetadataOptions = &ec2.LaunchTemplateInstanceMetadataOptionsRequest{
+			HttpEndpoint:         aws.String(string(lt.InstanceMetadataOptions.HTTPEndpoint)),
+			InstanceMetadataTags: aws.String(string(lt.InstanceMetadataOptions.InstanceMetadataTags)),
+		}
+
+		if lt.InstanceMetadataOptions.HTTPTokens != "" {
+			data.MetadataOptions.HttpTokens = aws.String(string(lt.InstanceMetadataOptions.HTTPTokens))
+		}
+		if lt.InstanceMetadataOptions.HTTPPutResponseHopLimit != 0 {
+			data.MetadataOptions.HttpPutResponseHopLimit = aws.Int64(lt.InstanceMetadataOptions.HTTPPutResponseHopLimit)
+		}
+	}
+
 	if len(lt.IamInstanceProfile) > 0 {
 		data.IamInstanceProfile = &ec2.LaunchTemplateIamInstanceProfileSpecificationRequest{
 			Name: aws.String(lt.IamInstanceProfile),
@@ -638,6 +652,21 @@ func (s *Service) SDKToLaunchTemplate(d *ec2.LaunchTemplateVersion) (*expinfrav1
 		VersionNumber: d.VersionNumber,
 	}
 
+	if v.MetadataOptions != nil {
+		i.InstanceMetadataOptions = &infrav1.InstanceMetadataOptions{
+			HTTPPutResponseHopLimit: aws.Int64Value(v.MetadataOptions.HttpPutResponseHopLimit),
+			HTTPTokens:              infrav1.HTTPTokensState(aws.StringValue(v.MetadataOptions.HttpTokens)),
+			HTTPEndpoint:            infrav1.InstanceMetadataEndpointStateEnabled,
+			InstanceMetadataTags:    infrav1.InstanceMetadataEndpointStateDisabled,
+		}
+		if v.MetadataOptions.HttpEndpoint != nil && aws.StringValue(v.MetadataOptions.HttpEndpoint) == "disabled" {
+			i.InstanceMetadataOptions.HTTPEndpoint = infrav1.InstanceMetadataEndpointStateDisabled
+		}
+		if v.MetadataOptions.InstanceMetadataTags != nil && aws.StringValue(v.MetadataOptions.InstanceMetadataTags) == "enabled" {
+			i.InstanceMetadataOptions.InstanceMetadataTags = infrav1.InstanceMetadataEndpointStateEnabled
+		}
+	}
+
 	if v.IamInstanceProfile != nil {
 		i.IamInstanceProfile = aws.StringValue(v.IamInstanceProfile.Name)
 	}
@@ -680,6 +709,9 @@ func (s *Service) LaunchTemplateNeedsUpdate(scope scope.LaunchTemplateScope, inc
 	if incoming.InstanceType != existing.InstanceType {
 		return true, nil
 	}
+	if !cmp.Equal(incoming.InstanceMetadataOptions, existing.InstanceMetadataOptions) {
+		return true, nil
+	}
 
 	incomingIDs, err := s.GetAdditionalSecurityGroupsIDs(incoming.AdditionalSecurityGroups)
 	if err != nil {


Test patch:
diff --git a/pkg/cloud/services/ec2/launchtemplate_test.go b/pkg/cloud/services/ec2/launchtemplate_test.go
index 96395f681..3c10f7140 100644
--- a/pkg/cloud/services/ec2/launchtemplate_test.go
+++ b/pkg/cloud/services/ec2/launchtemplate_test.go
@@ -75,9 +75,7 @@ users:
 `
 )
 
-var (
-	testUserDataHash = userdata.ComputeHash([]byte(testUserData))
-)
+var testUserDataHash = userdata.ComputeHash([]byte(testUserData))
 
 func TestGetLaunchTemplate(t *testing.T) {
 	mockCtrl := gomock.NewController(t)
@@ -462,6 +460,30 @@ func TestServiceLaunchTemplateNeedsUpdate(t *testing.T) {
 			want:    true,
 			wantErr: false,
 		},
+		{
+			name: "new launch template instance metadata options, requiring IMDSv2",
+			incoming: &expinfrav1.AWSLaunchTemplate{
+				InstanceMetadataOptions: &infrav1.InstanceMetadataOptions{
+					HTTPPutResponseHopLimit: 1,
+					HTTPTokens:              infrav1.HTTPTokensStateRequired,
+				},
+			},
+			existing: &expinfrav1.AWSLaunchTemplate{},
+			want:     true,
+			wantErr:  false,
+		},
+		{
+			name:     "new launch template instance metadata options, removing IMDSv2 requirement",
+			incoming: &expinfrav1.AWSLaunchTemplate{},
+			existing: &expinfrav1.AWSLaunchTemplate{
+				InstanceMetadataOptions: &infrav1.InstanceMetadataOptions{
+					HTTPPutResponseHopLimit: 1,
+					HTTPTokens:              infrav1.HTTPTokensStateRequired,
+				},
+			},
+			want:    true,
+			wantErr: false,
+		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
@@ -705,7 +727,7 @@ func TestCreateLaunchTemplate(t *testing.T) {
 	mockCtrl := gomock.NewController(t)
 	defer mockCtrl.Finish()
 
-	var formatTagsInput = func(arg *ec2.CreateLaunchTemplateInput) {
+	formatTagsInput := func(arg *ec2.CreateLaunchTemplateInput) {
 		sortTags(arg.TagSpecifications[0].Tags)
 
 		for index := range arg.LaunchTemplateData.TagSpecifications {
@@ -713,7 +735,7 @@ func TestCreateLaunchTemplate(t *testing.T) {
 		}
 	}
 
-	var userData = []byte{1, 0, 0}
+	userData := []byte{1, 0, 0}
 	testCases := []struct {
 		name                 string
 		awsResourceReference []infrav1.AWSResourceReference
@@ -728,7 +750,7 @@ func TestCreateLaunchTemplate(t *testing.T) {
 				sgMap[infrav1.SecurityGroupNode] = infrav1.SecurityGroup{ID: "1"}
 				sgMap[infrav1.SecurityGroupLB] = infrav1.SecurityGroup{ID: "2"}
 
-				var expectedInput = &ec2.CreateLaunchTemplateInput{
+				expectedInput := &ec2.CreateLaunchTemplateInput{
 					LaunchTemplateData: &ec2.RequestLaunchTemplateData{
 						InstanceType: aws.String("t3.large"),
 						IamInstanceProfile: &ec2.LaunchTemplateIamInstanceProfileSpecificationRequest{
@@ -788,7 +810,7 @@ func TestCreateLaunchTemplate(t *testing.T) {
 				sgMap[infrav1.SecurityGroupNode] = infrav1.SecurityGroup{ID: "1"}
 				sgMap[infrav1.SecurityGroupLB] = infrav1.SecurityGroup{ID: "2"}
 
-				var expectedInput = &ec2.CreateLaunchTemplateInput{
+				expectedInput := &ec2.CreateLaunchTemplateInput{
 					LaunchTemplateData: &ec2.RequestLaunchTemplateData{
 						InstanceType: aws.String("t3.large"),
 						IamInstanceProfile: &ec2.LaunchTemplateIamInstanceProfileSpecificationRequest{
@@ -850,7 +872,7 @@ func TestCreateLaunchTemplate(t *testing.T) {
 				sgMap[infrav1.SecurityGroupNode] = infrav1.SecurityGroup{ID: "1"}
 				sgMap[infrav1.SecurityGroupLB] = infrav1.SecurityGroup{ID: "2"}
 
-				var expectedInput = &ec2.CreateLaunchTemplateInput{
+				expectedInput := &ec2.CreateLaunchTemplateInput{
 					LaunchTemplateData: &ec2.RequestLaunchTemplateData{
 						InstanceType: aws.String("t3.large"),
 						IamInstanceProfile: &ec2.LaunchTemplateIamInstanceProfileSpecificationRequest{
@@ -958,12 +980,12 @@ func TestCreateLaunchTemplateVersion(t *testing.T) {
 	mockCtrl := gomock.NewController(t)
 	defer mockCtrl.Finish()
 
-	var formatTagsInput = func(arg *ec2.CreateLaunchTemplateVersionInput) {
+	formatTagsInput := func(arg *ec2.CreateLaunchTemplateVersionInput) {
 		for index := range arg.LaunchTemplateData.TagSpecifications {
 			sortTags(arg.LaunchTemplateData.TagSpecifications[index].Tags)
 		}
 	}
-	var userData = []byte{1, 0, 0}
+	userData := []byte{1, 0, 0}
 	testCases := []struct {
 		name                 string
 		imageID              *string
@@ -979,7 +1001,7 @@ func TestCreateLaunchTemplateVersion(t *testing.T) {
 				sgMap[infrav1.SecurityGroupNode] = infrav1.SecurityGroup{ID: "1"}
 				sgMap[infrav1.SecurityGroupLB] = infrav1.SecurityGroup{ID: "2"}
 
-				var expectedInput = &ec2.CreateLaunchTemplateVersionInput{
+				expectedInput := &ec2.CreateLaunchTemplateVersionInput{
 					LaunchTemplateData: &ec2.RequestLaunchTemplateData{
 						InstanceType: aws.String("t3.large"),
 						IamInstanceProfile: &ec2.LaunchTemplateIamInstanceProfileSpecificationRequest{
@@ -1030,7 +1052,7 @@ func TestCreateLaunchTemplateVersion(t *testing.T) {
 				sgMap[infrav1.SecurityGroupNode] = infrav1.SecurityGroup{ID: "1"}
 				sgMap[infrav1.SecurityGroupLB] = infrav1.SecurityGroup{ID: "2"}
 
-				var expectedInput = &ec2.CreateLaunchTemplateVersionInput{
+				expectedInput := &ec2.CreateLaunchTemplateVersionInput{
 					LaunchTemplateData: &ec2.RequestLaunchTemplateData{
 						InstanceType: aws.String("t3.large"),
 						IamInstanceProfile: &ec2.LaunchTemplateIamInstanceProfileSpecificationRequest{