From 844abaf31f2b66606c6673fdc625cf3d141169cf Mon Sep 17 00:00:00 2001 From: Ian Botsford <83236726+ianbotsf@users.noreply.github.com> Date: Tue, 24 Jun 2025 22:08:09 +0000 Subject: [PATCH] feat: update to IMDS credentials provider SEP v2.1.1 --- .../credentials/ImdsCredentialsProvider.kt | 13 +- .../kotlin/runtime/config/AwsSdkSetting.kt | 5 +- .../runtime/config/profile/AwsProfile.kt | 2 +- .../ImdsCredentialsProviderTestResources.kt | 143 ++++++++++++++++++ 4 files changed, 157 insertions(+), 6 deletions(-) diff --git a/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/auth/credentials/ImdsCredentialsProvider.kt b/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/auth/credentials/ImdsCredentialsProvider.kt index f4794ba130a..481ac66f22f 100644 --- a/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/auth/credentials/ImdsCredentialsProvider.kt +++ b/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/auth/credentials/ImdsCredentialsProvider.kt @@ -23,6 +23,7 @@ import aws.smithy.kotlin.runtime.util.asyncLazy import kotlin.coroutines.coroutineContext private const val CODE_ASSUME_ROLE_UNAUTHORIZED_ACCESS: String = "AssumeRoleUnauthorizedAccess" +private const val MAX_ATTEMPTS = 10 private const val PROVIDER_NAME = "IMDSv2" /** @@ -105,11 +106,15 @@ public class ImdsCredentialsProvider( override suspend fun resolve(attributes: Attributes): Credentials = sfg.singleFlight(::resolveSingleFlight) - private suspend fun resolveSingleFlight(): Credentials { + private suspend fun resolveSingleFlight(attempts: Int = 0): Credentials { if (providerDisabled.get()) { throw CredentialsNotLoadedException("AWS EC2 metadata is explicitly disabled; credentials not loaded") } + if (attempts >= MAX_ATTEMPTS) { + throw CredentialsNotLoadedException("Failed to retrieve profile credentials after $attempts attempts") + } + val profileName = instanceProfileName.get() ?: resolvedProfileName ?: try { actualClient.get(urlBase).also { if (apiVersion == null) { @@ -122,7 +127,7 @@ public class ImdsCredentialsProvider( apiVersion == null && ex.status == HttpStatusCode.NotFound -> { // Tried EXTENDED and that didn't work; fallback to LEGACY apiVersion = ApiVersion.LEGACY - return resolveSingleFlight() + return resolveSingleFlight(attempts) // one-time condition, do not increase `attempts` } ex.status == HttpStatusCode.NotFound -> { @@ -148,13 +153,13 @@ public class ImdsCredentialsProvider( apiVersion == null && ex.status == HttpStatusCode.NotFound -> { // Tried EXTENDED and that didn't work; fallback to LEGACY apiVersion = ApiVersion.LEGACY - return resolveSingleFlight() + return resolveSingleFlight() // one-time condition, do not increase `attempts` } instanceProfileName.get() == null && ex.status == HttpStatusCode.NotFound -> { // A previously-resolved profile is now invalid; forget the resolved name and re-resolve resolvedProfileName = null - return resolveSingleFlight() + return resolveSingleFlight(attempts + 1) // potentially infinite recursion, increase `attempts` } else -> return usePreviousCredentials() diff --git a/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/config/AwsSdkSetting.kt b/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/config/AwsSdkSetting.kt index 2f86a889006..8f9740af691 100644 --- a/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/config/AwsSdkSetting.kt +++ b/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/config/AwsSdkSetting.kt @@ -6,6 +6,9 @@ package aws.sdk.kotlin.runtime.config import aws.sdk.kotlin.runtime.InternalSdkApi +import aws.sdk.kotlin.runtime.config.AwsSdkSetting.AwsAccessKeyId +import aws.sdk.kotlin.runtime.config.AwsSdkSetting.AwsContainerCredentialsRelativeUri +import aws.sdk.kotlin.runtime.config.AwsSdkSetting.AwsSecretAccessKey import aws.sdk.kotlin.runtime.config.endpoints.AccountIdEndpointMode import aws.sdk.kotlin.runtime.http.AWS_APP_ID_ENV import aws.sdk.kotlin.runtime.http.AWS_APP_ID_PROP @@ -88,7 +91,7 @@ public object AwsSdkSetting { * Whether to load information such as credentials, regions from EC2 Metadata instance service. */ public val AwsEc2MetadataDisabled: EnvironmentSetting = - boolEnvSetting("aws.disableEc2Metadata", "AWS_EC2_METADATA_DISABLED").orElse(false) + boolEnvSetting("aws.ec2MetadataDisabled", "AWS_EC2_METADATA_DISABLED").orElse(false) /** * The EC2 instance metadata service endpoint. diff --git a/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/config/profile/AwsProfile.kt b/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/config/profile/AwsProfile.kt index cda2bc11deb..da0d7783bfa 100644 --- a/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/config/profile/AwsProfile.kt +++ b/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/config/profile/AwsProfile.kt @@ -189,7 +189,7 @@ public val AwsProfile.ec2InstanceProfileName: String? */ @InternalSdkApi public val AwsProfile.ec2MetadataDisabled: Boolean? - get() = getBooleanOrNull("disable_ec2_metadata") + get() = getBooleanOrNull("ec2_metadata_disabled") /** * Parse a config value as a boolean, ignoring case. diff --git a/aws-runtime/aws-config/common/test/aws/sdk/kotlin/runtime/auth/credentials/ImdsCredentialsProviderTestResources.kt b/aws-runtime/aws-config/common/test/aws/sdk/kotlin/runtime/auth/credentials/ImdsCredentialsProviderTestResources.kt index e5ddb3b191d..43d5773a0ff 100644 --- a/aws-runtime/aws-config/common/test/aws/sdk/kotlin/runtime/auth/credentials/ImdsCredentialsProviderTestResources.kt +++ b/aws-runtime/aws-config/common/test/aws/sdk/kotlin/runtime/auth/credentials/ImdsCredentialsProviderTestResources.kt @@ -653,6 +653,149 @@ val imdsCredentialsTestSpec = """ "result": "invalid profile" } ] + }, + { + "summary": "Test IMDS credentials provider when profile never stabilizes returns no credentials", + "config": { + "ec2InstanceProfileName": null + }, + "expectations": [ + { + "get": "/latest/meta-data/iam/security-credentials-extended", + "response": { + "status": 200, + "body": "my-profile-0013" + } + }, + { + "get": "/latest/meta-data/iam/security-credentials-extended/my-profile-0013", + "response": { + "status": 404 + } + }, + { + "get": "/latest/meta-data/iam/security-credentials-extended", + "response": { + "status": 200, + "body": "my-profile-0013" + } + }, + { + "get": "/latest/meta-data/iam/security-credentials-extended/my-profile-0013", + "response": { + "status": 404 + } + }, + { + "get": "/latest/meta-data/iam/security-credentials-extended", + "response": { + "status": 200, + "body": "my-profile-0013" + } + }, + { + "get": "/latest/meta-data/iam/security-credentials-extended/my-profile-0013", + "response": { + "status": 404 + } + }, + { + "get": "/latest/meta-data/iam/security-credentials-extended", + "response": { + "status": 200, + "body": "my-profile-0013" + } + }, + { + "get": "/latest/meta-data/iam/security-credentials-extended/my-profile-0013", + "response": { + "status": 404 + } + }, + { + "get": "/latest/meta-data/iam/security-credentials-extended", + "response": { + "status": 200, + "body": "my-profile-0013" + } + }, + { + "get": "/latest/meta-data/iam/security-credentials-extended/my-profile-0013", + "response": { + "status": 404 + } + }, + { + "get": "/latest/meta-data/iam/security-credentials-extended", + "response": { + "status": 200, + "body": "my-profile-0013" + } + }, + { + "get": "/latest/meta-data/iam/security-credentials-extended/my-profile-0013", + "response": { + "status": 404 + } + }, + { + "get": "/latest/meta-data/iam/security-credentials-extended", + "response": { + "status": 200, + "body": "my-profile-0013" + } + }, + { + "get": "/latest/meta-data/iam/security-credentials-extended/my-profile-0013", + "response": { + "status": 404 + } + }, + { + "get": "/latest/meta-data/iam/security-credentials-extended", + "response": { + "status": 200, + "body": "my-profile-0013" + } + }, + { + "get": "/latest/meta-data/iam/security-credentials-extended/my-profile-0013", + "response": { + "status": 404 + } + }, + { + "get": "/latest/meta-data/iam/security-credentials-extended", + "response": { + "status": 200, + "body": "my-profile-0013" + } + }, + { + "get": "/latest/meta-data/iam/security-credentials-extended/my-profile-0013", + "response": { + "status": 404 + } + }, + { + "get": "/latest/meta-data/iam/security-credentials-extended", + "response": { + "status": 200, + "body": "my-profile-0013" + } + }, + { + "get": "/latest/meta-data/iam/security-credentials-extended/my-profile-0013", + "response": { + "status": 404 + } + } + ], + "outcomes": [ + { + "result": "no credentials" + } + ] } ] """.trimIndent()