diff --git a/.changes/next-release/feature-AWSSDKforJavav2-2c52c12.json b/.changes/next-release/feature-AWSSDKforJavav2-2c52c12.json
new file mode 100644
index 000000000000..52fafcbf8a48
--- /dev/null
+++ b/.changes/next-release/feature-AWSSDKforJavav2-2c52c12.json
@@ -0,0 +1,6 @@
+{
+ "type": "feature",
+ "category": "AWS SDK for Java v2",
+ "contributor": "",
+ "description": "Adds setting to disable making EC2 Instance Metadata Service (IMDS) calls without a token header when prefetching a token does not work. This feature can be configured through environment variables (AWS_EC2_METADATA_V1_DISABLED), system property (aws.disableEc2MetadataV1) or AWS config file (ec2_metadata_v1_disabled). When you configure this setting to true, no calls without token headers will be made to IMDS."
+}
diff --git a/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProvider.java b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProvider.java
index fa19467e99d0..db84a1b13ecf 100644
--- a/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProvider.java
+++ b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProvider.java
@@ -31,6 +31,7 @@
import software.amazon.awssdk.annotations.SdkPublicApi;
import software.amazon.awssdk.annotations.SdkTestInternalApi;
import software.amazon.awssdk.auth.credentials.internal.Ec2MetadataConfigProvider;
+import software.amazon.awssdk.auth.credentials.internal.Ec2MetadataDisableV1Resolver;
import software.amazon.awssdk.auth.credentials.internal.HttpCredentialsLoader;
import software.amazon.awssdk.auth.credentials.internal.HttpCredentialsLoader.LoadedCredentials;
import software.amazon.awssdk.auth.credentials.internal.StaticResourcesEndpointProvider;
@@ -40,6 +41,7 @@
import software.amazon.awssdk.profiles.ProfileFile;
import software.amazon.awssdk.profiles.ProfileFileSupplier;
import software.amazon.awssdk.profiles.ProfileFileSystemSetting;
+import software.amazon.awssdk.profiles.ProfileProperty;
import software.amazon.awssdk.regions.util.HttpResourcesUtils;
import software.amazon.awssdk.regions.util.ResourcesEndpointProvider;
import software.amazon.awssdk.utils.Logger;
@@ -53,10 +55,13 @@
/**
* Credentials provider implementation that loads credentials from the Amazon EC2 Instance Metadata Service.
- *
- *
+ *
* If {@link SdkSystemSetting#AWS_EC2_METADATA_DISABLED} is set to true, it will not try to load
* credentials from EC2 metadata service and will return null.
+ *
+ * If {@link SdkSystemSetting#AWS_EC2_METADATA_V1_DISABLED} or {@link ProfileProperty#EC2_METADATA_V1_DISABLED}
+ * is set to true, credentials will only be loaded from EC2 metadata service if a token is successfully retrieved -
+ * fallback to load credentials without a token will be disabled.
*/
@SdkPublicApi
public final class InstanceProfileCredentialsProvider
@@ -73,6 +78,7 @@ public final class InstanceProfileCredentialsProvider
private final Clock clock;
private final String endpoint;
private final Ec2MetadataConfigProvider configProvider;
+ private final Ec2MetadataDisableV1Resolver ec2MetadataDisableV1Resolver;
private final HttpCredentialsLoader httpCredentialsLoader;
private final CachedSupplier credentialsCache;
@@ -92,15 +98,18 @@ private InstanceProfileCredentialsProvider(BuilderImpl builder) {
this.endpoint = builder.endpoint;
this.asyncCredentialUpdateEnabled = builder.asyncCredentialUpdateEnabled;
this.asyncThreadName = builder.asyncThreadName;
- this.profileFile = builder.profileFile;
- this.profileName = builder.profileName;
+ this.profileFile = Optional.ofNullable(builder.profileFile)
+ .orElseGet(() -> ProfileFileSupplier.fixedProfileFile(ProfileFile.defaultProfileFile()));
+ this.profileName = Optional.ofNullable(builder.profileName)
+ .orElseGet(ProfileFileSystemSetting.AWS_PROFILE::getStringValueOrThrow);
this.httpCredentialsLoader = HttpCredentialsLoader.create();
this.configProvider =
Ec2MetadataConfigProvider.builder()
- .profileFile(builder.profileFile)
- .profileName(builder.profileName)
+ .profileFile(profileFile)
+ .profileName(profileName)
.build();
+ this.ec2MetadataDisableV1Resolver = Ec2MetadataDisableV1Resolver.create(profileFile, profileName);
if (Boolean.TRUE.equals(builder.asyncCredentialUpdateEnabled)) {
Validate.paramNotBlank(builder.asyncThreadName, "asyncThreadName");
@@ -135,7 +144,6 @@ public static InstanceProfileCredentialsProvider create() {
return builder().build();
}
-
@Override
public AwsCredentials resolveCredentials() {
return credentialsCache.get();
@@ -225,17 +233,15 @@ private String getToken(String imdsHostname) {
return HttpResourcesUtils.instance().readResource(tokenEndpoint, "PUT");
} catch (SdkServiceException e) {
if (e.statusCode() == 400) {
+
throw SdkClientException.builder()
.message("Unable to fetch metadata token.")
.cause(e)
.build();
}
-
- log.debug(() -> "Ignoring non-fatal exception while attempting to load metadata token from instance profile.", e);
- return null;
+ return handleTokenErrorResponse(e);
} catch (Exception e) {
- log.debug(() -> "Ignoring non-fatal exception while attempting to load metadata token from instance profile.", e);
- return null;
+ return handleTokenErrorResponse(e);
}
}
@@ -247,6 +253,27 @@ private URI getTokenEndpoint(String imdsHostname) {
return URI.create(finalHost + TOKEN_RESOURCE);
}
+ private String handleTokenErrorResponse(Exception e) {
+ if (isInsecureFallbackDisabled()) {
+ String message = String.format("Failed to retrieve IMDS token, and fallback to IMDS v1 is disabled via the "
+ + "%s system property, %s environment variable, or %s configuration file profile"
+ + " setting.",
+ SdkSystemSetting.AWS_EC2_METADATA_V1_DISABLED.environmentVariable(),
+ SdkSystemSetting.AWS_EC2_METADATA_V1_DISABLED.property(),
+ ProfileProperty.EC2_METADATA_V1_DISABLED);
+ throw SdkClientException.builder()
+ .message(message)
+ .cause(e)
+ .build();
+ }
+ log.debug(() -> "Ignoring non-fatal exception while attempting to load metadata token from instance profile.", e);
+ return null;
+ }
+
+ private boolean isInsecureFallbackDisabled() {
+ return ec2MetadataDisableV1Resolver.resolve();
+ }
+
private String[] getSecurityCredentials(String imdsHostname, String metadataToken) {
ResourcesEndpointProvider securityCredentialsEndpoint =
new StaticResourcesEndpointProvider(URI.create(imdsHostname + SECURITY_CREDENTIALS_RESOURCE),
diff --git a/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/internal/Ec2MetadataDisableV1Resolver.java b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/internal/Ec2MetadataDisableV1Resolver.java
new file mode 100644
index 000000000000..b5d4173a374c
--- /dev/null
+++ b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/internal/Ec2MetadataDisableV1Resolver.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package software.amazon.awssdk.auth.credentials.internal;
+
+import java.util.Optional;
+import java.util.function.Supplier;
+import software.amazon.awssdk.annotations.SdkInternalApi;
+import software.amazon.awssdk.core.SdkSystemSetting;
+import software.amazon.awssdk.profiles.ProfileFile;
+import software.amazon.awssdk.profiles.ProfileProperty;
+import software.amazon.awssdk.utils.Lazy;
+import software.amazon.awssdk.utils.OptionalUtils;
+
+@SdkInternalApi
+public final class Ec2MetadataDisableV1Resolver {
+ private final Supplier profileFile;
+ private final String profileName;
+ private final Lazy resolvedValue;
+
+ private Ec2MetadataDisableV1Resolver(Supplier profileFile, String profileName) {
+ this.profileFile = profileFile;
+ this.profileName = profileName;
+ this.resolvedValue = new Lazy<>(this::doResolve);
+ }
+
+ public static Ec2MetadataDisableV1Resolver create(Supplier profileFile, String profileName) {
+ return new Ec2MetadataDisableV1Resolver(profileFile, profileName);
+ }
+
+ public boolean resolve() {
+ return resolvedValue.getValue();
+ }
+
+ public boolean doResolve() {
+ return OptionalUtils.firstPresent(fromSystemSettings(),
+ () -> fromProfileFile(profileFile, profileName))
+ .orElse(false);
+ }
+
+ private static Optional fromSystemSettings() {
+ return SdkSystemSetting.AWS_EC2_METADATA_V1_DISABLED.getBooleanValue();
+ }
+
+ private static Optional fromProfileFile(Supplier profileFile, String profileName) {
+ return profileFile.get()
+ .profile(profileName)
+ .flatMap(p -> p.booleanProperty(ProfileProperty.EC2_METADATA_V1_DISABLED));
+ }
+
+}
diff --git a/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/internal/ProfileCredentialsUtils.java b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/internal/ProfileCredentialsUtils.java
index 37e310e6e0d5..ef15454a0421 100644
--- a/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/internal/ProfileCredentialsUtils.java
+++ b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/internal/ProfileCredentialsUtils.java
@@ -262,15 +262,10 @@ private AwsCredentialsProvider credentialSourceCredentialProvider(CredentialSour
case ECS_CONTAINER:
return ContainerCredentialsProvider.builder().build();
case EC2_INSTANCE_METADATA:
- // The IMDS credentials provider should source the endpoint config properties from the currently active profile
- Ec2MetadataConfigProvider configProvider = Ec2MetadataConfigProvider.builder()
- .profileFile(() -> profileFile)
- .profileName(name)
- .build();
-
return InstanceProfileCredentialsProvider.builder()
- .endpoint(configProvider.getEndpoint())
- .build();
+ .profileFile(profileFile)
+ .profileName(name)
+ .build();
case ENVIRONMENT:
return AwsCredentialsProviderChain.builder()
.addCredentialsProvider(SystemPropertyCredentialsProvider.create())
diff --git a/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProviderTest.java b/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProviderTest.java
index 0f32ed3c17bd..05db14c7a2fd 100644
--- a/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProviderTest.java
+++ b/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProviderTest.java
@@ -23,6 +23,7 @@
import static com.github.tomakehurst.wiremock.client.WireMock.putRequestedFor;
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
+import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
import static java.time.temporal.ChronoUnit.HOURS;
import static java.time.temporal.ChronoUnit.MINUTES;
import static java.time.temporal.ChronoUnit.SECONDS;
@@ -33,7 +34,8 @@
import com.github.tomakehurst.wiremock.client.ResponseDefinitionBuilder;
import com.github.tomakehurst.wiremock.client.WireMock;
import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
-import com.github.tomakehurst.wiremock.junit.WireMockRule;
+import com.github.tomakehurst.wiremock.junit5.WireMockExtension;
+import com.github.tomakehurst.wiremock.junit5.WireMockTest;
import com.github.tomakehurst.wiremock.matching.RequestPattern;
import com.github.tomakehurst.wiremock.matching.RequestPatternBuilder;
import java.time.Clock;
@@ -45,11 +47,12 @@
import java.util.List;
import java.util.function.Supplier;
import java.util.stream.Collectors;
-import org.junit.AfterClass;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.ExpectedException;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
import software.amazon.awssdk.core.SdkSystemSetting;
import software.amazon.awssdk.core.exception.SdkClientException;
import software.amazon.awssdk.core.util.SdkUserAgent;
@@ -60,6 +63,7 @@
import software.amazon.awssdk.utils.Pair;
import software.amazon.awssdk.utils.StringInputStream;
+@WireMockTest
public class InstanceProfileCredentialsProviderTest {
private static final String TOKEN_RESOURCE_PATH = "/latest/api/token";
private static final String CREDENTIALS_RESOURCE_PATH = "/latest/meta-data/iam/security-credentials/";
@@ -67,169 +71,197 @@ public class InstanceProfileCredentialsProviderTest {
+ "\"Expiration\":\"" + DateUtils.formatIso8601Date(Instant.now().plus(Duration.ofDays(1)))
+ "\"}";
private static final String TOKEN_HEADER = "x-aws-ec2-metadata-token";
+ private static final String USER_AGENT_HEADER = "User-Agent";
+ private static final String TOKEN_STUB = "some-token";
+ private static final String PROFILE_NAME = "some-profile";
+ private static final String USER_AGENT = SdkUserAgent.create().userAgent();
private static final String EC2_METADATA_TOKEN_TTL_HEADER = "x-aws-ec2-metadata-token-ttl-seconds";
+ @RegisterExtension
+ static WireMockExtension wireMockServer = WireMockExtension.newInstance()
+ .options(wireMockConfig().dynamicPort().dynamicPort())
+ .configureStaticDsl(true)
+ .build();
- @Rule
- public ExpectedException thrown = ExpectedException.none();
-
- @Rule
- public WireMockRule mockMetadataEndpoint = new WireMockRule();
-
- @Before
+ @BeforeEach
public void methodSetup() {
- System.setProperty(SdkSystemSetting.AWS_EC2_METADATA_SERVICE_ENDPOINT.property(), "http://localhost:" + mockMetadataEndpoint.port());
+ System.setProperty(SdkSystemSetting.AWS_EC2_METADATA_SERVICE_ENDPOINT.property(), "http://localhost:" + wireMockServer.getPort());
}
- @AfterClass
+ @AfterAll
public static void teardown() {
System.clearProperty(SdkSystemSetting.AWS_EC2_METADATA_SERVICE_ENDPOINT.property());
}
- @Test
- public void resolveCredentials_metadataLookupDisabled_throws() {
- System.setProperty(SdkSystemSetting.AWS_EC2_METADATA_DISABLED.property(), "true");
- thrown.expect(SdkClientException.class);
- thrown.expectMessage("IMDS credentials have been disabled");
- try {
- InstanceProfileCredentialsProvider.builder().build().resolveCredentials();
- } finally {
- System.clearProperty(SdkSystemSetting.AWS_EC2_METADATA_DISABLED.property());
- }
+ private void stubSecureCredentialsResponse(ResponseDefinitionBuilder responseDefinitionBuilder) {
+ wireMockServer.stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(aResponse().withBody(TOKEN_STUB)));
+ stubCredentialsResponse(responseDefinitionBuilder);
}
- @Test
- public void resolveCredentials_requestsIncludeUserAgent() {
- String stubToken = "some-token";
- stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(aResponse().withBody(stubToken)));
- stubFor(get(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH)).willReturn(aResponse().withBody("some-profile")));
- stubFor(get(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH + "some-profile")).willReturn(aResponse().withBody(STUB_CREDENTIALS)));
+ private void stubTokenFetchErrorResponse(ResponseDefinitionBuilder responseDefinitionBuilder, int statusCode) {
+ wireMockServer.stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(aResponse().withStatus(statusCode)
+ .withBody("oops")));
+ stubCredentialsResponse(responseDefinitionBuilder);
+ }
- InstanceProfileCredentialsProvider provider = InstanceProfileCredentialsProvider.builder().build();
+ private void stubCredentialsResponse(ResponseDefinitionBuilder responseDefinitionBuilder) {
+ wireMockServer.stubFor(get(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH)).willReturn(aResponse().withBody(PROFILE_NAME)));
+ wireMockServer.stubFor(get(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH + PROFILE_NAME)).willReturn(responseDefinitionBuilder));
+ }
- provider.resolveCredentials();
+ private void verifyImdsCallWithToken() {
+ WireMock.verify(putRequestedFor(urlPathEqualTo(TOKEN_RESOURCE_PATH))
+ .withHeader(USER_AGENT_HEADER, equalTo(USER_AGENT))
+ .withHeader(EC2_METADATA_TOKEN_TTL_HEADER, equalTo("21600")));
+ WireMock.verify(getRequestedFor(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH))
+ .withHeader(TOKEN_HEADER, equalTo(TOKEN_STUB))
+ .withHeader(USER_AGENT_HEADER, equalTo(USER_AGENT)));
+ WireMock.verify(getRequestedFor(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH + "some-profile"))
+ .withHeader(TOKEN_HEADER, equalTo(TOKEN_STUB))
+ .withHeader(USER_AGENT_HEADER, equalTo(USER_AGENT)));
+ }
- String userAgentHeader = "User-Agent";
- String userAgent = SdkUserAgent.create().userAgent();
- WireMock.verify(putRequestedFor(urlPathEqualTo(TOKEN_RESOURCE_PATH)).withHeader(userAgentHeader, equalTo(userAgent)));
- WireMock.verify(getRequestedFor(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH)).withHeader(userAgentHeader, equalTo(userAgent)));
- WireMock.verify(getRequestedFor(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH + "some-profile")).withHeader(userAgentHeader, equalTo(userAgent)));
+ private void verifyImdsCallInsecure() {
+ WireMock.verify(getRequestedFor(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH))
+ .withoutHeader(TOKEN_HEADER)
+ .withHeader(USER_AGENT_HEADER, equalTo(USER_AGENT)));
+ WireMock.verify(getRequestedFor(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH + "some-profile"))
+ .withoutHeader(TOKEN_HEADER)
+ .withHeader(USER_AGENT_HEADER, equalTo(USER_AGENT)));
}
@Test
- public void resolveCredentials_queriesTokenResource() {
- stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(aResponse().withBody("some-token")));
- stubFor(get(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH)).willReturn(aResponse().withBody("some-profile")));
- stubFor(get(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH + "some-profile")).willReturn(aResponse().withBody(STUB_CREDENTIALS)));
-
+ public void resolveCredentials_queriesTokenResource_includesTokenInCredentialsRequests() {
+ stubSecureCredentialsResponse(aResponse().withBody(STUB_CREDENTIALS));
InstanceProfileCredentialsProvider provider = InstanceProfileCredentialsProvider.builder().build();
-
provider.resolveCredentials();
-
- WireMock.verify(putRequestedFor(urlPathEqualTo(TOKEN_RESOURCE_PATH)).withHeader(EC2_METADATA_TOKEN_TTL_HEADER, equalTo("21600")));
+ verifyImdsCallWithToken();
}
- @Test
- public void resolveCredentials_queriesTokenResource_includedInCredentialsRequests() {
- String stubToken = "some-token";
- stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(aResponse().withBody(stubToken)));
- stubFor(get(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH)).willReturn(aResponse().withBody("some-profile")));
- stubFor(get(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH + "some-profile")).willReturn(aResponse().withBody(STUB_CREDENTIALS)));
-
+ @ParameterizedTest
+ @ValueSource(ints = {403, 404, 405})
+ public void resolveCredentials_queriesTokenResource_40xError_fallbackToInsecure(int statusCode) {
+ stubTokenFetchErrorResponse(aResponse().withBody(STUB_CREDENTIALS), statusCode);
InstanceProfileCredentialsProvider provider = InstanceProfileCredentialsProvider.builder().build();
-
provider.resolveCredentials();
-
- WireMock.verify(getRequestedFor(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH)).withHeader(TOKEN_HEADER, equalTo(stubToken)));
- WireMock.verify(getRequestedFor(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH + "some-profile")).withHeader(TOKEN_HEADER, equalTo(stubToken)));
+ verifyImdsCallInsecure();
}
@Test
- public void resolveCredentials_queriesTokenResource_403Error_fallbackToInsecure() {
- stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(aResponse().withStatus(403).withBody("oops")));
- stubFor(get(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH)).willReturn(aResponse().withBody("some-profile")));
- stubFor(get(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH + "some-profile")).willReturn(aResponse().withBody(STUB_CREDENTIALS)));
+ public void resolveCredentials_queriesTokenResource_socketTimeout_fallbackToInsecure() {
+ stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(aResponse().withBody("some-token").withFixedDelay(Integer.MAX_VALUE)));
+ stubFor(get(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH)).willReturn(aResponse().withBody(PROFILE_NAME)));
+ stubFor(get(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH + PROFILE_NAME)).willReturn(aResponse().withBody(STUB_CREDENTIALS)));
InstanceProfileCredentialsProvider provider = InstanceProfileCredentialsProvider.builder().build();
-
provider.resolveCredentials();
+ verifyImdsCallInsecure();
+ }
- WireMock.verify(getRequestedFor(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH)));
- WireMock.verify(getRequestedFor(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH + "some-profile")));
+ @ParameterizedTest
+ @ValueSource(ints = {403, 404, 405})
+ public void resolveCredentials_fallbackToInsecureDisabledThroughProperty_throwsWhenTokenFails(int statusCode) {
+ System.setProperty(SdkSystemSetting.AWS_EC2_METADATA_V1_DISABLED.property(), "true");
+ stubTokenFetchErrorResponse(aResponse().withBody(STUB_CREDENTIALS), statusCode);
+ try {
+ InstanceProfileCredentialsProvider.builder().build().resolveCredentials();
+ } catch (Exception e) {
+ assertThat(e).isInstanceOf(SdkClientException.class);
+ Throwable cause = e.getCause();
+ assertThat(cause).isInstanceOf(SdkClientException.class);
+ assertThat(cause).hasMessageContaining("fallback to IMDS v1 is disabled");
+ }
+ finally {
+ System.clearProperty(SdkSystemSetting.AWS_EC2_METADATA_V1_DISABLED.property());
+ }
}
@Test
- public void resolveCredentials_queriesTokenResource_404Error_fallbackToInsecure() {
- stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(aResponse().withStatus(404).withBody("oops")));
- stubFor(get(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH)).willReturn(aResponse().withBody("some-profile")));
- stubFor(get(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH + "some-profile")).willReturn(aResponse().withBody(STUB_CREDENTIALS)));
-
- InstanceProfileCredentialsProvider provider = InstanceProfileCredentialsProvider.builder().build();
-
- provider.resolveCredentials();
+ public void resolveCredentials_fallbackToInsecureDisabledThroughProperty_returnsCredentialsWhenTokenReturned() {
+ System.setProperty(SdkSystemSetting.AWS_EC2_METADATA_V1_DISABLED.property(), "true");
+ stubSecureCredentialsResponse(aResponse().withBody(STUB_CREDENTIALS));
+ try {
+ InstanceProfileCredentialsProvider provider = InstanceProfileCredentialsProvider.builder().build();
+ provider.resolveCredentials();
+ verifyImdsCallWithToken();
+ } finally {
+ System.clearProperty(SdkSystemSetting.AWS_EC2_METADATA_V1_DISABLED.property());
+ }
+ }
- WireMock.verify(getRequestedFor(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH)));
- WireMock.verify(getRequestedFor(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH + "some-profile")));
+ @ParameterizedTest
+ @ValueSource(ints = {403, 404, 405})
+ public void resolveCredentials_fallbackToInsecureDisabledThroughConfig_throwsWhenTokenFails(int statusCode) {
+ stubTokenFetchErrorResponse(aResponse().withBody(STUB_CREDENTIALS), statusCode);
+ try {
+ InstanceProfileCredentialsProvider.builder()
+ .profileFile(configFile("profile test", Pair.of(ProfileProperty.EC2_METADATA_V1_DISABLED, "true")))
+ .profileName("test")
+ .build()
+ .resolveCredentials();
+ } catch (Exception e) {
+ assertThat(e).isInstanceOf(SdkClientException.class);
+ Throwable cause = e.getCause();
+ assertThat(cause).isInstanceOf(SdkClientException.class);
+ assertThat(cause).hasMessageContaining("fallback to IMDS v1 is disabled");
+ }
}
@Test
- public void resolveCredentials_queriesTokenResource_405Error_fallbackToInsecure() {
- stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(aResponse().withStatus(405).withBody("oops")));
- stubFor(get(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH)).willReturn(aResponse().withBody("some-profile")));
- stubFor(get(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH + "some-profile")).willReturn(aResponse().withBody(STUB_CREDENTIALS)));
-
- InstanceProfileCredentialsProvider provider = InstanceProfileCredentialsProvider.builder().build();
-
- provider.resolveCredentials();
-
- WireMock.verify(getRequestedFor(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH)));
- WireMock.verify(getRequestedFor(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH + "some-profile")));
+ public void resolveCredentials_fallbackToInsecureDisabledThroughConfig_returnsCredentialsWhenTokenReturned() {
+ stubSecureCredentialsResponse(aResponse().withBody(STUB_CREDENTIALS));
+ InstanceProfileCredentialsProvider.builder()
+ .profileFile(configFile("profile test", Pair.of(ProfileProperty.EC2_METADATA_V1_DISABLED, "true")))
+ .profileName("test")
+ .build()
+ .resolveCredentials();
+ verifyImdsCallWithToken();
}
@Test
- public void resolveCredentials_queriesTokenResource_400Error_throws() {
- thrown.expect(SdkClientException.class);
- thrown.expectMessage("Failed to load credentials from IMDS");
-
- stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(aResponse().withStatus(400).withBody("oops")));
-
- InstanceProfileCredentialsProvider provider = InstanceProfileCredentialsProvider.builder().build();
-
- provider.resolveCredentials();
+ public void resolveCredentials_fallbackToInsecureEnabledThroughConfig_returnsCredentialsWhenTokenReturned() {
+ stubSecureCredentialsResponse(aResponse().withBody(STUB_CREDENTIALS));
+ InstanceProfileCredentialsProvider.builder()
+ .profileFile(configFile("profile test",
+ Pair.of(ProfileProperty.EC2_METADATA_V1_DISABLED, "false")))
+ .profileName("test")
+ .build()
+ .resolveCredentials();
+ verifyImdsCallWithToken();
}
@Test
- public void resolveCredentials_queriesTokenResource_socketTimeout_fallbackToInsecure() {
- stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(aResponse().withBody("some-token").withFixedDelay(Integer.MAX_VALUE)));
- stubFor(get(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH)).willReturn(aResponse().withBody("some-profile")));
- stubFor(get(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH + "some-profile")).willReturn(aResponse().withBody(STUB_CREDENTIALS)));
-
- InstanceProfileCredentialsProvider provider = InstanceProfileCredentialsProvider.builder().build();
-
- provider.resolveCredentials();
+ public void resolveCredentials_queriesTokenResource_400Error_throws() {
+ stubTokenFetchErrorResponse(aResponse().withBody(STUB_CREDENTIALS), 400);
- WireMock.verify(getRequestedFor(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH)));
- WireMock.verify(getRequestedFor(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH + "some-profile")));
+ assertThatThrownBy(() -> InstanceProfileCredentialsProvider.builder().build().resolveCredentials())
+ .isInstanceOf(SdkClientException.class).hasMessage("Failed to load credentials from IMDS.");
}
@Test
public void resolveCredentials_endpointSettingEmpty_throws() {
- thrown.expect(SdkClientException.class);
-
System.setProperty(SdkSystemSetting.AWS_EC2_METADATA_SERVICE_ENDPOINT.property(), "");
- InstanceProfileCredentialsProvider provider = InstanceProfileCredentialsProvider.builder().build();
-
- provider.resolveCredentials();
+ assertThatThrownBy(() -> InstanceProfileCredentialsProvider.builder().build().resolveCredentials())
+ .isInstanceOf(SdkClientException.class).hasMessage("Failed to load credentials from IMDS.");
}
@Test
public void resolveCredentials_endpointSettingHostNotExists_throws() {
- thrown.expect(SdkClientException.class);
-
System.setProperty(SdkSystemSetting.AWS_EC2_METADATA_SERVICE_ENDPOINT.property(), "some-host-that-does-not-exist");
- InstanceProfileCredentialsProvider provider = InstanceProfileCredentialsProvider.builder().build();
+ assertThatThrownBy(() -> InstanceProfileCredentialsProvider.builder().build().resolveCredentials())
+ .isInstanceOf(SdkClientException.class).hasMessage("Failed to load credentials from IMDS.");
+ }
- provider.resolveCredentials();
+ @Test
+ public void resolveCredentials_metadataLookupDisabled_throws() {
+ System.setProperty(SdkSystemSetting.AWS_EC2_METADATA_DISABLED.property(), "true");
+ try {
+ assertThatThrownBy(() -> InstanceProfileCredentialsProvider.builder().build().resolveCredentials())
+ .isInstanceOf(SdkClientException.class)
+ .hasMessage("IMDS credentials have been disabled by environment variable or system property.");
+ } finally {
+ System.clearProperty(SdkSystemSetting.AWS_EC2_METADATA_DISABLED.property());
+ }
}
@Test
@@ -250,14 +282,15 @@ public void resolveCredentials_customProfileFileAndName_usesCorrectEndpoint() {
provider.resolveCredentials();
- String userAgentHeader = "User-Agent";
- String userAgent = SdkUserAgent.create().userAgent();
- mockMetadataEndpoint_2.verify(putRequestedFor(urlPathEqualTo(TOKEN_RESOURCE_PATH)).withHeader(userAgentHeader, equalTo(userAgent)));
- mockMetadataEndpoint_2.verify(getRequestedFor(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH)).withHeader(userAgentHeader, equalTo(userAgent)));
- mockMetadataEndpoint_2.verify(getRequestedFor(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH + "some-profile")).withHeader(userAgentHeader, equalTo(userAgent)));
+ mockMetadataEndpoint_2.verify(putRequestedFor(urlPathEqualTo(TOKEN_RESOURCE_PATH))
+ .withHeader(USER_AGENT_HEADER, equalTo(USER_AGENT)));
+ mockMetadataEndpoint_2.verify(getRequestedFor(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH))
+ .withHeader(USER_AGENT_HEADER, equalTo(USER_AGENT)));
+ mockMetadataEndpoint_2.verify(getRequestedFor(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH + "some-profile"))
+ .withHeader(USER_AGENT_HEADER, equalTo(USER_AGENT)));
// all requests should have gone to the second server, and none to the other one
- mockMetadataEndpoint.verify(0, RequestPatternBuilder.allRequests());
+ wireMockServer.verify(0, RequestPatternBuilder.allRequests());
} finally {
mockMetadataEndpoint_2.stop();
}
@@ -293,14 +326,15 @@ public void resolveCredentials_customProfileFileSupplierAndNameSettingEndpointOv
assertThat(awsCredentials1).isNotNull();
- String userAgentHeader = "User-Agent";
- String userAgent = SdkUserAgent.create().userAgent();
- mockMetadataEndpoint_2.verify(putRequestedFor(urlPathEqualTo(TOKEN_RESOURCE_PATH)).withHeader(userAgentHeader, equalTo(userAgent)));
- mockMetadataEndpoint_2.verify(getRequestedFor(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH)).withHeader(userAgentHeader, equalTo(userAgent)));
- mockMetadataEndpoint_2.verify(getRequestedFor(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH + "some-profile")).withHeader(userAgentHeader, equalTo(userAgent)));
+ mockMetadataEndpoint_2.verify(putRequestedFor(urlPathEqualTo(TOKEN_RESOURCE_PATH))
+ .withHeader(USER_AGENT_HEADER, equalTo(USER_AGENT)));
+ mockMetadataEndpoint_2.verify(getRequestedFor(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH))
+ .withHeader(USER_AGENT_HEADER, equalTo(USER_AGENT)));
+ mockMetadataEndpoint_2.verify(getRequestedFor(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH + "some-profile"))
+ .withHeader(USER_AGENT_HEADER, equalTo(USER_AGENT)));
// all requests should have gone to the second server, and none to the other one
- mockMetadataEndpoint.verify(0, RequestPatternBuilder.allRequests());
+ wireMockServer.verify(0, RequestPatternBuilder.allRequests());
} finally {
mockMetadataEndpoint_2.stop();
}
@@ -334,14 +368,15 @@ public void resolveCredentials_customSupplierProfileFileAndNameSettingEndpointOv
assertThat(awsCredentials1).isNotNull();
- String userAgentHeader = "User-Agent";
- String userAgent = SdkUserAgent.create().userAgent();
- mockMetadataEndpoint_2.verify(putRequestedFor(urlPathEqualTo(TOKEN_RESOURCE_PATH)).withHeader(userAgentHeader, equalTo(userAgent)));
- mockMetadataEndpoint_2.verify(getRequestedFor(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH)).withHeader(userAgentHeader, equalTo(userAgent)));
- mockMetadataEndpoint_2.verify(getRequestedFor(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH + "some-profile")).withHeader(userAgentHeader, equalTo(userAgent)));
+ mockMetadataEndpoint_2.verify(putRequestedFor(urlPathEqualTo(TOKEN_RESOURCE_PATH))
+ .withHeader(USER_AGENT_HEADER, equalTo(USER_AGENT)));
+ mockMetadataEndpoint_2.verify(getRequestedFor(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH))
+ .withHeader(USER_AGENT_HEADER, equalTo(USER_AGENT)));
+ mockMetadataEndpoint_2.verify(getRequestedFor(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH + "some-profile"))
+ .withHeader(USER_AGENT_HEADER, equalTo(USER_AGENT)));
// all requests should have gone to the second server, and none to the other one
- mockMetadataEndpoint.verify(0, RequestPatternBuilder.allRequests());
+ wireMockServer.verify(0, RequestPatternBuilder.allRequests());
} finally {
mockMetadataEndpoint_2.stop();
}
@@ -356,7 +391,7 @@ public void resolveCredentials_doesNotFailIfImdsReturnsExpiredCredentials() {
+ "\"Expiration\":\"" + DateUtils.formatIso8601Date(Instant.now().minus(Duration.ofHours(1))) + '"'
+ "}";
- stubCredentialsResponse(aResponse().withBody(credentialsResponse));
+ stubSecureCredentialsResponse(aResponse().withBody(credentialsResponse));
AwsCredentials credentials = InstanceProfileCredentialsProvider.builder().build().resolveCredentials();
@@ -373,18 +408,18 @@ public void resolveCredentials_onlyCallsImdsOnceEvenWithExpiredCredentials() {
+ "\"Expiration\":\"" + DateUtils.formatIso8601Date(Instant.now().minus(Duration.ofHours(1))) + '"'
+ "}";
- stubCredentialsResponse(aResponse().withBody(credentialsResponse));
+ stubSecureCredentialsResponse(aResponse().withBody(credentialsResponse));
AwsCredentialsProvider credentialsProvider = InstanceProfileCredentialsProvider.builder().build();
credentialsProvider.resolveCredentials();
- int requestCountAfterOneRefresh = mockMetadataEndpoint.countRequestsMatching(RequestPattern.everything()).getCount();
+ int requestCountAfterOneRefresh = wireMockServer.countRequestsMatching(RequestPattern.everything()).getCount();
credentialsProvider.resolveCredentials();
credentialsProvider.resolveCredentials();
- int requestCountAfterThreeRefreshes = mockMetadataEndpoint.countRequestsMatching(RequestPattern.everything()).getCount();
+ int requestCountAfterThreeRefreshes = wireMockServer.countRequestsMatching(RequestPattern.everything()).getCount();
assertThat(requestCountAfterThreeRefreshes).isEqualTo(requestCountAfterOneRefresh);
}
@@ -398,8 +433,8 @@ public void resolveCredentials_failsIfImdsReturns500OnFirstCall() {
+ "\"message\": \"" + errorMessage + "\""
+ "}";
- stubCredentialsResponse(aResponse().withStatus(500)
- .withBody(credentialsResponse));
+ stubSecureCredentialsResponse(aResponse().withStatus(500)
+ .withBody(credentialsResponse));
assertThatThrownBy(InstanceProfileCredentialsProvider.builder().build()::resolveCredentials)
.isInstanceOf(SdkClientException.class)
@@ -419,12 +454,12 @@ public void resolveCredentials_usesCacheIfImdsFailsOnSecondCall() {
// Set the time to the past, so that the cache expiration time is still is in the past, and then prime the cache
clock.time = Instant.now().minus(24, HOURS);
- stubCredentialsResponse(aResponse().withBody(successfulCredentialsResponse));
+ stubSecureCredentialsResponse(aResponse().withBody(successfulCredentialsResponse));
AwsCredentials credentialsBefore = credentialsProvider.resolveCredentials();
// Travel to the present time take down IMDS, so we can see if we use the cached credentials
clock.time = Instant.now();
- stubCredentialsResponse(aResponse().withStatus(500));
+ stubSecureCredentialsResponse(aResponse().withStatus(500));
AwsCredentials credentialsAfter = credentialsProvider.resolveCredentials();
assertThat(credentialsBefore).isEqualTo(credentialsAfter);
@@ -451,18 +486,18 @@ public void resolveCredentials_callsImdsIfCredentialsWithin5MinutesOfExpiration(
// Set the time to the past and call IMDS to prime the cache
clock.time = now.minus(24, HOURS);
- stubCredentialsResponse(aResponse().withBody(successfulCredentialsResponse1));
+ stubSecureCredentialsResponse(aResponse().withBody(successfulCredentialsResponse1));
AwsCredentials credentials24HoursAgo = credentialsProvider.resolveCredentials();
// Set the time to 10 minutes before expiration, and fail to call IMDS
clock.time = now.minus(10, MINUTES);
- stubCredentialsResponse(aResponse().withStatus(500));
+ stubSecureCredentialsResponse(aResponse().withStatus(500));
AwsCredentials credentials10MinutesAgo = credentialsProvider.resolveCredentials();
// Set the time to 10 seconds before expiration, and verify that we still call IMDS to try to get credentials in at the
// last moment before expiration
clock.time = now.minus(10, SECONDS);
- stubCredentialsResponse(aResponse().withBody(successfulCredentialsResponse2));
+ stubSecureCredentialsResponse(aResponse().withBody(successfulCredentialsResponse2));
AwsCredentials credentials10SecondsAgo = credentialsProvider.resolveCredentials();
assertThat(credentials24HoursAgo).isEqualTo(credentials10MinutesAgo);
@@ -493,12 +528,12 @@ public void imdsCallFrequencyIsLimited() {
// Set the time to 5 minutes before expiration and call IMDS
clock.time = now.minus(5, MINUTES);
- stubCredentialsResponse(aResponse().withBody(successfulCredentialsResponse1));
+ stubSecureCredentialsResponse(aResponse().withBody(successfulCredentialsResponse1));
AwsCredentials credentials5MinutesAgo = credentialsProvider.resolveCredentials();
// Set the time to 2 seconds before expiration, and verify that do not call IMDS because it hasn't been 5 minutes yet
clock.time = now.minus(2, SECONDS);
- stubCredentialsResponse(aResponse().withBody(successfulCredentialsResponse2));
+ stubSecureCredentialsResponse(aResponse().withBody(successfulCredentialsResponse2));
AwsCredentials credentials2SecondsAgo = credentialsProvider.resolveCredentials();
assertThat(credentials2SecondsAgo).isEqualTo(credentials5MinutesAgo);
@@ -513,15 +548,6 @@ private AwsCredentialsProvider credentialsProviderWithClock(Clock clock) {
return builder.build();
}
- private void stubCredentialsResponse(ResponseDefinitionBuilder responseDefinitionBuilder) {
- mockMetadataEndpoint.stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH))
- .willReturn(aResponse().withBody("some-token")));
- mockMetadataEndpoint.stubFor(get(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH))
- .willReturn(aResponse().withBody("some-profile")));
- mockMetadataEndpoint.stubFor(get(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH + "some-profile"))
- .willReturn(responseDefinitionBuilder));
- }
-
private static class AdjustableClock extends Clock {
private Instant time;
diff --git a/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/internal/Ec2MetadataDisableV1ResolverTest.java b/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/internal/Ec2MetadataDisableV1ResolverTest.java
new file mode 100644
index 000000000000..fe7bb90eca6d
--- /dev/null
+++ b/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/internal/Ec2MetadataDisableV1ResolverTest.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package software.amazon.awssdk.auth.credentials.internal;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import java.util.Arrays;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import software.amazon.awssdk.core.SdkSystemSetting;
+import software.amazon.awssdk.profiles.ProfileFile;
+import software.amazon.awssdk.profiles.ProfileProperty;
+import software.amazon.awssdk.testutils.EnvironmentVariableHelper;
+import software.amazon.awssdk.utils.Pair;
+import software.amazon.awssdk.utils.StringInputStream;
+
+public class Ec2MetadataDisableV1ResolverTest {
+
+ private static final EnvironmentVariableHelper ENVIRONMENT_VARIABLE_HELPER = new EnvironmentVariableHelper();
+
+ @BeforeEach
+ public void methodSetup() {
+ ENVIRONMENT_VARIABLE_HELPER.reset();
+ System.clearProperty(SdkSystemSetting.AWS_EC2_METADATA_V1_DISABLED.property());
+ }
+
+ @ParameterizedTest(name = "{index} - EXPECTED:{3} (sys:{0}, env:{1}, cfg:{2})")
+ @MethodSource("booleanConfigValues")
+ public void resolveDisableValue_whenBoolean_resolvesCorrectly(
+ String systemProperty, String envVar, ProfileFile profileFile, boolean expected) {
+
+ setUpSystemSettings(systemProperty, envVar);
+
+ Ec2MetadataDisableV1Resolver resolver = Ec2MetadataDisableV1Resolver.create(() -> profileFile, "test");
+ assertThat(resolver.resolve()).isEqualTo(expected);
+ }
+
+ private static Stream booleanConfigValues() {
+ ProfileFile emptyProfile = configFile("profile test", Pair.of("foo", "bar"));
+
+ Function profileDisableValues =
+ s -> configFile("profile test", Pair.of(ProfileProperty.EC2_METADATA_V1_DISABLED, s));
+
+ return Stream.of(
+ Arguments.of(null, null, emptyProfile, false),
+ Arguments.of("false", null, null, false),
+ Arguments.of("true", null, null, true),
+ Arguments.of(null, "false", null, false),
+ Arguments.of(null, "true", null, true),
+ Arguments.of(null, null, profileDisableValues.apply("false"), false),
+ Arguments.of(null, null, profileDisableValues.apply("true"), true),
+ Arguments.of(null, null, configFile("profile test", Pair.of("bar", "baz")), false),
+ Arguments.of(null, null, configFile("profile foo", Pair.of(ProfileProperty.EC2_METADATA_V1_DISABLED, "true")),
+ false),
+ Arguments.of("false", "true", null, false),
+ Arguments.of("true", "false", null, true),
+ Arguments.of("false", null, profileDisableValues.apply("true"), false),
+ Arguments.of("true", null, profileDisableValues.apply("false"), true)
+ );
+ }
+
+ @ParameterizedTest(name = "{index} - sys:{0}, env:{1}, cfg:{2}")
+ @MethodSource("nonBooleanConfigValues")
+ public void resolveDisableValue_whenNonBoolean_throws(
+ String systemProperty, String envVar, ProfileFile profileFile) {
+
+ setUpSystemSettings(systemProperty, envVar);
+
+ Ec2MetadataDisableV1Resolver resolver = Ec2MetadataDisableV1Resolver.create(() -> profileFile, "test");
+ assertThatThrownBy(resolver::resolve).isInstanceOf(IllegalStateException.class);
+ }
+
+ private static Stream nonBooleanConfigValues() {
+ Function profileDisableValues =
+ s -> configFile("profile test", Pair.of(ProfileProperty.EC2_METADATA_V1_DISABLED, s));
+
+ return Stream.of(
+ Arguments.of("foo", null, null),
+ Arguments.of(null, "foo", null),
+ Arguments.of(null, null, profileDisableValues.apply("foo"))
+ );
+ }
+
+ private static void setUpSystemSettings(String systemProperty, String envVar) {
+ if (systemProperty != null) {
+ System.setProperty(SdkSystemSetting.AWS_EC2_METADATA_V1_DISABLED.property(), systemProperty);
+
+ }
+ if (envVar != null) {
+ ENVIRONMENT_VARIABLE_HELPER.set(SdkSystemSetting.AWS_EC2_METADATA_V1_DISABLED.environmentVariable(),
+ envVar);
+ }
+ }
+
+ private static ProfileFile configFile(String name, Pair, ?>... pairs) {
+ String values = Arrays.stream(pairs)
+ .map(pair -> String.format("%s=%s", pair.left(), pair.right()))
+ .collect(Collectors.joining(System.lineSeparator()));
+ String contents = String.format("[%s]\n%s", name, values);
+
+ return configFile(contents);
+ }
+
+ private static ProfileFile configFile(String credentialFile) {
+ return ProfileFile.builder()
+ .content(new StringInputStream(credentialFile))
+ .type(ProfileFile.Type.CONFIGURATION)
+ .build();
+ }
+}
diff --git a/core/profiles/src/main/java/software/amazon/awssdk/profiles/ProfileProperty.java b/core/profiles/src/main/java/software/amazon/awssdk/profiles/ProfileProperty.java
index fe3395b22b50..4f880510ca5a 100644
--- a/core/profiles/src/main/java/software/amazon/awssdk/profiles/ProfileProperty.java
+++ b/core/profiles/src/main/java/software/amazon/awssdk/profiles/ProfileProperty.java
@@ -143,6 +143,8 @@ public final class ProfileProperty {
public static final String EC2_METADATA_SERVICE_ENDPOINT = "ec2_metadata_service_endpoint";
+ public static final String EC2_METADATA_V1_DISABLED = "ec2_metadata_v1_disabled";
+
/**
* Whether request compression is disabled for operations marked with the RequestCompression trait. The default value is
* false, i.e., request compression is enabled.
diff --git a/core/regions/src/main/java/software/amazon/awssdk/regions/internal/util/EC2MetadataUtils.java b/core/regions/src/main/java/software/amazon/awssdk/regions/internal/util/EC2MetadataUtils.java
index a7aedf74810e..83c41052da92 100644
--- a/core/regions/src/main/java/software/amazon/awssdk/regions/internal/util/EC2MetadataUtils.java
+++ b/core/regions/src/main/java/software/amazon/awssdk/regions/internal/util/EC2MetadataUtils.java
@@ -33,6 +33,7 @@
import software.amazon.awssdk.core.exception.SdkClientException;
import software.amazon.awssdk.core.exception.SdkServiceException;
import software.amazon.awssdk.core.util.SdkUserAgent;
+import software.amazon.awssdk.profiles.ProfileProperty;
import software.amazon.awssdk.protocols.jsoncore.JsonNode;
import software.amazon.awssdk.protocols.jsoncore.JsonNodeParser;
import software.amazon.awssdk.regions.util.HttpResourcesUtils;
@@ -54,11 +55,13 @@
* retrieve their content from the Amazon S3 bucket you specify at launch. To
* add a new customer at any time, simply create a bucket for the customer, add
* their content, and launch your AMI.
- *
- *
+ *
* If {@link SdkSystemSetting#AWS_EC2_METADATA_DISABLED} is set to true, EC2 metadata usage
* will be disabled and {@link SdkClientException} will be thrown for any metadata retrieval attempt.
- *
+ *
+ * If {@link SdkSystemSetting#AWS_EC2_METADATA_V1_DISABLED} or {@link ProfileProperty#EC2_METADATA_V1_DISABLED}
+ * is set to true, data will only be loaded from EC2 metadata service if a token is successfully retrieved -
+ * fallback to load data without a token will be disabled.
*
* More information about Amazon EC2 Metadata
*
@@ -85,6 +88,10 @@ public final class EC2MetadataUtils {
private static final Logger log = LoggerFactory.getLogger(EC2MetadataUtils.class);
private static final Map CACHE = new ConcurrentHashMap<>();
+ private static final Ec2MetadataDisableV1Resolver EC2_METADATA_DISABLE_V1_RESOLVER = Ec2MetadataDisableV1Resolver.create();
+ private static final Object FALLBACK_LOCK = new Object();
+ private static volatile Boolean IS_INSECURE_FALLBACK_DISABLED;
+
private static final InstanceProviderTokenEndpointProvider TOKEN_ENDPOINT_PROVIDER =
new InstanceProviderTokenEndpointProvider();
@@ -372,6 +379,11 @@ public static void clearCache() {
CACHE.clear();
}
+ @SdkTestInternalApi
+ public static void resetIsFallbackDisableResolved() {
+ IS_INSECURE_FALLBACK_DISABLED = null;
+ }
+
private static List getItems(String path, int tries, boolean slurp) {
if (tries == 0) {
throw SdkClientException.builder().message("Unable to contact EC2 metadata service.").build();
@@ -434,9 +446,35 @@ public static String getToken() {
.cause(e)
.build();
}
+ return handleTokenErrorResponse(e);
+ }
+ }
- return null;
+ private static String handleTokenErrorResponse(Exception e) {
+ if (isInsecureFallbackDisabled()) {
+ String message = String.format("Failed to retrieve IMDS token, and fallback to IMDS v1 is disabled via the "
+ + "%s system property, %s environment variable, or %s configuration file profile"
+ + " setting.",
+ SdkSystemSetting.AWS_EC2_METADATA_V1_DISABLED.environmentVariable(),
+ SdkSystemSetting.AWS_EC2_METADATA_V1_DISABLED.property(),
+ ProfileProperty.EC2_METADATA_V1_DISABLED);
+ throw SdkClientException.builder()
+ .message(message)
+ .cause(e)
+ .build();
+ }
+ return null;
+ }
+
+ private static boolean isInsecureFallbackDisabled() {
+ if (IS_INSECURE_FALLBACK_DISABLED == null) {
+ synchronized (FALLBACK_LOCK) {
+ if (IS_INSECURE_FALLBACK_DISABLED == null) {
+ IS_INSECURE_FALLBACK_DISABLED = EC2_METADATA_DISABLE_V1_RESOLVER.resolve();
+ }
+ }
}
+ return IS_INSECURE_FALLBACK_DISABLED;
}
private static String fetchData(String path) {
diff --git a/core/regions/src/main/java/software/amazon/awssdk/regions/internal/util/Ec2MetadataDisableV1Resolver.java b/core/regions/src/main/java/software/amazon/awssdk/regions/internal/util/Ec2MetadataDisableV1Resolver.java
new file mode 100644
index 000000000000..414a47e2501d
--- /dev/null
+++ b/core/regions/src/main/java/software/amazon/awssdk/regions/internal/util/Ec2MetadataDisableV1Resolver.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package software.amazon.awssdk.regions.internal.util;
+
+import java.util.Optional;
+import java.util.function.Supplier;
+import software.amazon.awssdk.annotations.SdkInternalApi;
+import software.amazon.awssdk.core.SdkSystemSetting;
+import software.amazon.awssdk.profiles.ProfileFile;
+import software.amazon.awssdk.profiles.ProfileFileSystemSetting;
+import software.amazon.awssdk.profiles.ProfileProperty;
+import software.amazon.awssdk.utils.OptionalUtils;
+
+@SdkInternalApi
+public final class Ec2MetadataDisableV1Resolver {
+
+ private Ec2MetadataDisableV1Resolver() {
+ }
+
+ public static Ec2MetadataDisableV1Resolver create() {
+ return new Ec2MetadataDisableV1Resolver();
+ }
+
+ public boolean resolve() {
+ return OptionalUtils.firstPresent(fromSystemSettings(), Ec2MetadataDisableV1Resolver::fromProfileFile)
+ .orElse(false);
+ }
+
+ private static Optional fromSystemSettings() {
+ return SdkSystemSetting.AWS_EC2_METADATA_V1_DISABLED.getBooleanValue();
+ }
+
+ private static Optional fromProfileFile() {
+ Supplier profileFile = ProfileFile::defaultProfileFile;
+ String profileName = ProfileFileSystemSetting.AWS_PROFILE.getStringValueOrThrow();
+ if (profileFile.get() == null) {
+ return Optional.empty();
+ }
+ return profileFile.get()
+ .profile(profileName)
+ .flatMap(p -> p.property(ProfileProperty.EC2_METADATA_V1_DISABLED))
+ .map(Ec2MetadataDisableV1Resolver::safeProfileStringToBoolean);
+ }
+
+ private static boolean safeProfileStringToBoolean(String value) {
+ if (value.equalsIgnoreCase("true")) {
+ return true;
+ }
+ if (value.equalsIgnoreCase("false")) {
+ return false;
+ }
+
+ throw new IllegalStateException("Profile property '" + ProfileProperty.EC2_METADATA_V1_DISABLED + "', "
+ + "was defined as '" + value + "', but should be 'false' or 'true'");
+ }
+
+}
diff --git a/core/regions/src/main/java/software/amazon/awssdk/regions/providers/InstanceProfileRegionProvider.java b/core/regions/src/main/java/software/amazon/awssdk/regions/providers/InstanceProfileRegionProvider.java
index 0d12df38f882..83011c06e885 100644
--- a/core/regions/src/main/java/software/amazon/awssdk/regions/providers/InstanceProfileRegionProvider.java
+++ b/core/regions/src/main/java/software/amazon/awssdk/regions/providers/InstanceProfileRegionProvider.java
@@ -18,16 +18,20 @@
import software.amazon.awssdk.annotations.SdkProtectedApi;
import software.amazon.awssdk.core.SdkSystemSetting;
import software.amazon.awssdk.core.exception.SdkClientException;
+import software.amazon.awssdk.profiles.ProfileProperty;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.regions.internal.util.EC2MetadataUtils;
/**
* Attempts to load region information from the EC2 Metadata service. If the application is not
- * running on EC2 this provider will thrown an exception.
- *
- *
+ * running on EC2 this provider will throw an exception.
+ *
* If {@link SdkSystemSetting#AWS_EC2_METADATA_DISABLED} is set to true, it will not try to load
* region from EC2 metadata service and will return null.
+ *
+ * If {@link SdkSystemSetting#AWS_EC2_METADATA_V1_DISABLED} or {@link ProfileProperty#EC2_METADATA_V1_DISABLED}
+ * is set to true, the region will only be loaded from EC2 metadata service if a token is successfully retrieved -
+ * fallback to load region without a token will be disabled.
*/
@SdkProtectedApi
public final class InstanceProfileRegionProvider implements AwsRegionProvider {
diff --git a/core/regions/src/test/java/software/amazon/awssdk/regions/internal/util/EC2MetadataUtilsTest.java b/core/regions/src/test/java/software/amazon/awssdk/regions/internal/util/EC2MetadataUtilsTest.java
index 4172db957e78..506dba82522e 100644
--- a/core/regions/src/test/java/software/amazon/awssdk/regions/internal/util/EC2MetadataUtilsTest.java
+++ b/core/regions/src/test/java/software/amazon/awssdk/regions/internal/util/EC2MetadataUtilsTest.java
@@ -39,10 +39,10 @@ public class EC2MetadataUtilsTest {
private static final String TOKEN_RESOURCE_PATH = "/latest/api/token";
private static final String TOKEN_HEADER = "x-aws-ec2-metadata-token";
private static final String EC2_METADATA_TOKEN_TTL_HEADER = "x-aws-ec2-metadata-token-ttl-seconds";
-
private static final String EC2_METADATA_ROOT = "/latest/meta-data";
-
private static final String AMI_ID_RESOURCE = EC2_METADATA_ROOT + "/ami-id";
+ private static final String TOKEN_STUB = "some-token";
+ private static final String EMPTY_BODY = "{}";
@Rule
@@ -55,32 +55,33 @@ public class EC2MetadataUtilsTest {
public void methodSetup() {
System.setProperty(SdkSystemSetting.AWS_EC2_METADATA_SERVICE_ENDPOINT.property(), "http://localhost:" + mockMetadataEndpoint.port());
EC2MetadataUtils.clearCache();
+ EC2MetadataUtils.resetIsFallbackDisableResolved();
+ System.clearProperty(SdkSystemSetting.AWS_EC2_METADATA_V1_DISABLED.property());
}
@Test
public void getToken_queriesCorrectPath() {
- stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(aResponse().withBody("some-token")));
+ stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(aResponse().withBody(TOKEN_STUB)));
String token = EC2MetadataUtils.getToken();
- assertThat(token).isEqualTo("some-token");
+ assertThat(token).isEqualTo(TOKEN_STUB);
WireMock.verify(putRequestedFor(urlPathEqualTo(TOKEN_RESOURCE_PATH)).withHeader(EC2_METADATA_TOKEN_TTL_HEADER, equalTo("21600")));
}
@Test
public void getAmiId_queriesAndIncludesToken() {
- stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(aResponse().withBody("some-token")));
+ stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(aResponse().withBody(TOKEN_STUB)));
stubFor(get(urlPathEqualTo(AMI_ID_RESOURCE)).willReturn(aResponse().withBody("{}")));
EC2MetadataUtils.getAmiId();
WireMock.verify(putRequestedFor(urlPathEqualTo(TOKEN_RESOURCE_PATH)).withHeader(EC2_METADATA_TOKEN_TTL_HEADER, equalTo("21600")));
- WireMock.verify(getRequestedFor(urlPathEqualTo(AMI_ID_RESOURCE)).withHeader(TOKEN_HEADER, equalTo("some-token")));
+ WireMock.verify(getRequestedFor(urlPathEqualTo(AMI_ID_RESOURCE)).withHeader(TOKEN_HEADER, equalTo(TOKEN_STUB)));
}
@Test
public void getAmiId_tokenQueryTimeout_fallsBackToInsecure() {
-
stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(aResponse().withFixedDelay(Integer.MAX_VALUE)));
stubFor(get(urlPathEqualTo(AMI_ID_RESOURCE)).willReturn(aResponse().withBody("{}")));
@@ -93,7 +94,7 @@ public void getAmiId_tokenQueryTimeout_fallsBackToInsecure() {
@Test
public void getAmiId_queriesTokenResource_403Error_fallbackToInsecure() {
stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(aResponse().withStatus(403).withBody("oops")));
- stubFor(get(urlPathEqualTo(AMI_ID_RESOURCE)).willReturn(aResponse().withBody("{}")));
+ stubFor(get(urlPathEqualTo(AMI_ID_RESOURCE)).willReturn(aResponse().withBody(EMPTY_BODY)));
EC2MetadataUtils.getAmiId();
@@ -104,7 +105,7 @@ public void getAmiId_queriesTokenResource_403Error_fallbackToInsecure() {
@Test
public void getAmiId_queriesTokenResource_404Error_fallbackToInsecure() {
stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(aResponse().withStatus(404).withBody("oops")));
- stubFor(get(urlPathEqualTo(AMI_ID_RESOURCE)).willReturn(aResponse().withBody("{}")));
+ stubFor(get(urlPathEqualTo(AMI_ID_RESOURCE)).willReturn(aResponse().withBody(EMPTY_BODY)));
EC2MetadataUtils.getAmiId();
@@ -115,7 +116,7 @@ public void getAmiId_queriesTokenResource_404Error_fallbackToInsecure() {
@Test
public void getAmiId_queriesTokenResource_405Error_fallbackToInsecure() {
stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(aResponse().withStatus(405).withBody("oops")));
- stubFor(get(urlPathEqualTo(AMI_ID_RESOURCE)).willReturn(aResponse().withBody("{}")));
+ stubFor(get(urlPathEqualTo(AMI_ID_RESOURCE)).willReturn(aResponse().withBody(EMPTY_BODY)));
EC2MetadataUtils.getAmiId();
@@ -123,6 +124,35 @@ public void getAmiId_queriesTokenResource_405Error_fallbackToInsecure() {
WireMock.verify(getRequestedFor(urlPathEqualTo(AMI_ID_RESOURCE)).withoutHeader(TOKEN_HEADER));
}
+ @Test
+ public void getAmiId_fallbackToInsecureDisabledThroughProperty_throwsWhenTokenFails() {
+ System.setProperty(SdkSystemSetting.AWS_EC2_METADATA_V1_DISABLED.property(), "true");
+ stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(aResponse().withStatus(403).withBody("oops")));
+ try {
+ EC2MetadataUtils.getAmiId();
+ } catch (Exception e) {
+ assertThat(e).isInstanceOf(SdkClientException.class);
+ assertThat(e).hasMessageContaining("fallback to IMDS v1 is disabled");
+ }
+ finally {
+ System.clearProperty(SdkSystemSetting.AWS_EC2_METADATA_V1_DISABLED.property());
+ }
+ }
+
+ @Test
+ public void getAmiId_fallbackToInsecureDisabledThroughProperty_returnsDataWhenTokenReturned() {
+ System.setProperty(SdkSystemSetting.AWS_EC2_METADATA_V1_DISABLED.property(), "true");
+ stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(aResponse().withBody(TOKEN_STUB)));
+ stubFor(get(urlPathEqualTo(AMI_ID_RESOURCE)).willReturn(aResponse().withBody("{}")));
+ try {
+ EC2MetadataUtils.getAmiId();
+ WireMock.verify(putRequestedFor(urlPathEqualTo(TOKEN_RESOURCE_PATH)).withHeader(EC2_METADATA_TOKEN_TTL_HEADER, equalTo("21600")));
+ WireMock.verify(getRequestedFor(urlPathEqualTo(AMI_ID_RESOURCE)).withHeader(TOKEN_HEADER, equalTo(TOKEN_STUB)));
+ } finally {
+ System.clearProperty(SdkSystemSetting.AWS_EC2_METADATA_V1_DISABLED.property());
+ }
+ }
+
@Test
public void getAmiId_queriesTokenResource_400Error_throws() {
thrown.expect(SdkClientException.class);
@@ -140,7 +170,7 @@ public void fetchDataWithAttemptNumber_ioError_shouldHonor() {
thrown.expect(SdkClientException.class);
thrown.expectMessage("Unable to contact EC2 metadata service");
- stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(aResponse().withBody("some-token")));;
+ stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(aResponse().withBody(TOKEN_STUB)));
stubFor(get(urlPathEqualTo(AMI_ID_RESOURCE)).willReturn(aResponse().withFault(Fault.CONNECTION_RESET_BY_PEER)));
EC2MetadataUtils.fetchData(AMI_ID_RESOURCE, false, attempts);
diff --git a/core/regions/src/test/java/software/amazon/awssdk/regions/internal/util/Ec2MetadataDisableV1ResolverTest.java b/core/regions/src/test/java/software/amazon/awssdk/regions/internal/util/Ec2MetadataDisableV1ResolverTest.java
new file mode 100644
index 000000000000..46d0a2c7e389
--- /dev/null
+++ b/core/regions/src/test/java/software/amazon/awssdk/regions/internal/util/Ec2MetadataDisableV1ResolverTest.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package software.amazon.awssdk.regions.internal.util;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import java.util.stream.Stream;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import software.amazon.awssdk.core.SdkSystemSetting;
+import software.amazon.awssdk.testutils.EnvironmentVariableHelper;
+
+public class Ec2MetadataDisableV1ResolverTest {
+
+ private static final EnvironmentVariableHelper ENVIRONMENT_VARIABLE_HELPER = new EnvironmentVariableHelper();
+
+ @BeforeEach
+ public void methodSetup() {
+ ENVIRONMENT_VARIABLE_HELPER.reset();
+ System.clearProperty(SdkSystemSetting.AWS_EC2_METADATA_V1_DISABLED.property());
+ }
+
+ @ParameterizedTest(name = "{index} - EXPECTED:{3} (sys:{0}, env:{1}, cfg:{2})")
+ @MethodSource("booleanConfigValues")
+ public void resolveDisableValue_whenBoolean_resolvesCorrectly(
+ String systemProperty, String envVar, boolean expected) {
+
+ setUpSystemSettings(systemProperty, envVar);
+
+ Ec2MetadataDisableV1Resolver resolver = Ec2MetadataDisableV1Resolver.create();
+ assertThat(resolver.resolve()).isEqualTo(expected);
+ }
+
+ private static Stream booleanConfigValues() {
+ return Stream.of(
+ Arguments.of(null, null, false),
+ Arguments.of("false", null, false),
+ Arguments.of("true", null, true),
+ Arguments.of(null, "false", false),
+ Arguments.of(null, "true", true),
+ Arguments.of(null, null, false),
+ Arguments.of("false", "true", false),
+ Arguments.of("true", "false", true)
+ );
+ }
+
+ @ParameterizedTest(name = "{index} - sys:{0}, env:{1}")
+ @MethodSource("nonBooleanConfigValues")
+ public void resolveDisableValue_whenNonBoolean_throws(String systemProperty, String envVar) {
+ setUpSystemSettings(systemProperty, envVar);
+
+ Ec2MetadataDisableV1Resolver resolver = Ec2MetadataDisableV1Resolver.create();
+ assertThatThrownBy(resolver::resolve).isInstanceOf(IllegalStateException.class)
+ .hasMessageContaining("but should be 'false' or 'true'");
+ }
+
+ private static Stream nonBooleanConfigValues() {
+ return Stream.of(
+ Arguments.of("foo", null, null),
+ Arguments.of(null, "foo", null)
+ );
+ }
+
+ private static void setUpSystemSettings(String systemProperty, String envVar) {
+ if (systemProperty != null) {
+ System.setProperty(SdkSystemSetting.AWS_EC2_METADATA_V1_DISABLED.property(), systemProperty);
+
+ }
+ if (envVar != null) {
+ ENVIRONMENT_VARIABLE_HELPER.set(SdkSystemSetting.AWS_EC2_METADATA_V1_DISABLED.environmentVariable(),
+ envVar);
+ }
+ }
+}
diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/SdkSystemSetting.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/SdkSystemSetting.java
index 7a4f16e15c96..1f08db5a9057 100644
--- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/SdkSystemSetting.java
+++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/SdkSystemSetting.java
@@ -70,6 +70,11 @@ public enum SdkSystemSetting implements SystemSetting {
*/
AWS_EC2_METADATA_DISABLED("aws.disableEc2Metadata", "false"),
+ /**
+ * Whether to disable fallback to insecure EC2 Metadata instance service v1 on errors or timeouts.
+ */
+ AWS_EC2_METADATA_V1_DISABLED("aws.disableEc2MetadataV1", null),
+
/**
* The EC2 instance metadata service endpoint.
*
diff --git a/test/auth-tests/src/it/java/software/amazon/awssdk/auth/sts/ProfileCredentialsProviderIntegrationTest.java b/test/auth-tests/src/it/java/software/amazon/awssdk/auth/sts/ProfileCredentialsProviderIntegrationTest.java
index 6247a50da682..a8683e414af0 100644
--- a/test/auth-tests/src/it/java/software/amazon/awssdk/auth/sts/ProfileCredentialsProviderIntegrationTest.java
+++ b/test/auth-tests/src/it/java/software/amazon/awssdk/auth/sts/ProfileCredentialsProviderIntegrationTest.java
@@ -22,18 +22,23 @@
import static com.github.tomakehurst.wiremock.client.WireMock.put;
import static com.github.tomakehurst.wiremock.client.WireMock.putRequestedFor;
import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
-import com.github.tomakehurst.wiremock.WireMockServer;
-import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
-import java.io.ByteArrayInputStream;
-import java.nio.charset.StandardCharsets;
+import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import com.github.tomakehurst.wiremock.client.ResponseDefinitionBuilder;
+import com.github.tomakehurst.wiremock.client.WireMock;
+import com.github.tomakehurst.wiremock.junit5.WireMockExtension;
import java.time.Duration;
import java.time.Instant;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
import software.amazon.awssdk.auth.credentials.ProfileCredentialsProvider;
+import software.amazon.awssdk.core.exception.SdkClientException;
import software.amazon.awssdk.core.util.SdkUserAgent;
import software.amazon.awssdk.profiles.ProfileFile;
import software.amazon.awssdk.services.sts.model.StsException;
import software.amazon.awssdk.utils.DateUtils;
+import software.amazon.awssdk.utils.StringInputStream;
public class ProfileCredentialsProviderIntegrationTest {
private static final String TOKEN_RESOURCE_PATH = "/latest/api/token";
@@ -41,48 +46,134 @@ public class ProfileCredentialsProviderIntegrationTest {
private static final String STUB_CREDENTIALS = "{\"AccessKeyId\":\"ACCESS_KEY_ID\",\"SecretAccessKey\":\"SECRET_ACCESS_KEY\","
+ "\"Expiration\":\"" + DateUtils.formatIso8601Date(Instant.now().plus(Duration.ofDays(1)))
+ "\"}";
+ private static final String USER_AGENT_HEADER = "User-Agent";
+ private static final String USER_AGENT = SdkUserAgent.create().userAgent();
+ private static final String PROFILE_NAME = "some-profile";
+ private static final String TOKEN_HEADER = "x-aws-ec2-metadata-token";
+ private static final String TOKEN_STUB = "some-token";
+
+ @RegisterExtension
+ static WireMockExtension wireMockServer = WireMockExtension.newInstance()
+ .options(wireMockConfig().dynamicPort().dynamicPort())
+ .configureStaticDsl(true)
+ .build();
+
+ private void stubSecureCredentialsResponse(ResponseDefinitionBuilder responseDefinitionBuilder) {
+ wireMockServer.stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(aResponse().withBody(TOKEN_STUB)));
+ stubCredentialsResponse(responseDefinitionBuilder);
+ }
+
+ private void stubTokenFetchErrorResponse(ResponseDefinitionBuilder responseDefinitionBuilder, int statusCode) {
+ wireMockServer.stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(aResponse().withStatus(statusCode)
+ .withBody("oops")));
+ stubCredentialsResponse(responseDefinitionBuilder);
+ }
+
+ private void stubCredentialsResponse(ResponseDefinitionBuilder responseDefinitionBuilder) {
+ wireMockServer.stubFor(get(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH)).willReturn(aResponse().withBody(PROFILE_NAME)));
+ wireMockServer.stubFor(get(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH + PROFILE_NAME)).willReturn(responseDefinitionBuilder));
+ }
@Test
- public void profileWithCredentialSourceUsingEc2InstanceMetadataAndCustomEndpoint_usesEndpointInSourceProfile() {
+ public void resolveCredentials_instanceMetadataSourceAndCustomEndpoint_usesSourceEndpointAndMakesSecureCall() {
String testFileContentsTemplate = "" +
- "[profile a]\n" +
+ "[profile ec2Test]\n" +
"role_arn=arn:aws:iam::123456789012:role/testRole3\n" +
"credential_source = ec2instancemetadata\n" +
"ec2_metadata_service_endpoint = http://localhost:%d\n";
+ String profileFileContents = String.format(testFileContentsTemplate, wireMockServer.getPort());
+
+ ProfileCredentialsProvider profileCredentialsProvider = ProfileCredentialsProvider.builder()
+ .profileFile(configFile(profileFileContents))
+ .profileName("ec2Test")
+ .build();
- WireMockServer mockMetadataEndpoint = new WireMockServer(WireMockConfiguration.options().dynamicPort());
- mockMetadataEndpoint.start();
+ stubSecureCredentialsResponse(aResponse().withBody(STUB_CREDENTIALS));
- String profileFileContents = String.format(testFileContentsTemplate, mockMetadataEndpoint.port());
+ try {
+ profileCredentialsProvider.resolveCredentials();
+ } catch (StsException e) {
+ // ignored
+ }
+ verifyImdsCallWithToken();
+ }
- ProfileFile profileFile = ProfileFile.builder()
- .type(ProfileFile.Type.CONFIGURATION)
- .content(new ByteArrayInputStream(profileFileContents.getBytes(StandardCharsets.UTF_8)))
- .build();
+ @Test
+ public void resolveCredentials_instanceMetadataSource_fallbackToInsecureWhenTokenFails() {
+ String testFileContentsTemplate = "" +
+ "[profile ec2Test]\n" +
+ "role_arn=arn:aws:iam::123456789012:role/testRole3\n" +
+ "credential_source = ec2instancemetadata\n" +
+ "ec2_metadata_service_endpoint = http://localhost:%d\n";
+ String profileFileContents = String.format(testFileContentsTemplate, wireMockServer.getPort());
ProfileCredentialsProvider profileCredentialsProvider = ProfileCredentialsProvider.builder()
- .profileFile(profileFile)
- .profileName("a")
- .build();
+ .profileFile(configFile(profileFileContents))
+ .profileName("ec2Test")
+ .build();
- String stubToken = "some-token";
- mockMetadataEndpoint.stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(aResponse().withBody(stubToken)));
- mockMetadataEndpoint.stubFor(get(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH)).willReturn(aResponse().withBody("some-profile")));
- mockMetadataEndpoint.stubFor(get(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH + "some-profile")).willReturn(aResponse().withBody(STUB_CREDENTIALS)));
+ stubTokenFetchErrorResponse(aResponse().withBody(STUB_CREDENTIALS), 403);
try {
profileCredentialsProvider.resolveCredentials();
-
} catch (StsException e) {
// ignored
- } finally {
- mockMetadataEndpoint.stop();
}
+ verifyImdsCallInsecure();
+ }
+
+ @Test
+ public void resolveCredentials_instanceMetadataSourceAndFallbackToInsecureDisabled_throwsWhenTokenFails() {
+ String testFileContentsTemplate = "" +
+ "[profile ec2Test]\n" +
+ "role_arn=arn:aws:iam::123456789012:role/testRole3\n" +
+ "credential_source = ec2instancemetadata\n" +
+ "ec2_metadata_v1_disabled = true\n" +
+ "ec2_metadata_service_endpoint = http://localhost:%d\n";
+ String profileFileContents = String.format(testFileContentsTemplate, wireMockServer.getPort());
+
+ ProfileCredentialsProvider profileCredentialsProvider = ProfileCredentialsProvider.builder()
+ .profileFile(configFile(profileFileContents))
+ .profileName("ec2Test")
+ .build();
- String userAgentHeader = "User-Agent";
- String userAgent = SdkUserAgent.create().userAgent();
- mockMetadataEndpoint.verify(putRequestedFor(urlPathEqualTo(TOKEN_RESOURCE_PATH)).withHeader(userAgentHeader, equalTo(userAgent)));
- mockMetadataEndpoint.verify(getRequestedFor(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH)).withHeader(userAgentHeader, equalTo(userAgent)));
- mockMetadataEndpoint.verify(getRequestedFor(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH + "some-profile")).withHeader(userAgentHeader, equalTo(userAgent)));
+ stubTokenFetchErrorResponse(aResponse().withBody(STUB_CREDENTIALS), 403);
+
+ try {
+ profileCredentialsProvider.resolveCredentials();
+ } catch (Exception e) {
+ assertThat(e).isInstanceOf(SdkClientException.class);
+ Throwable cause = e.getCause();
+ assertThat(cause).isInstanceOf(SdkClientException.class);
+ assertThat(cause).hasMessageContaining("fallback to IMDS v1 is disabled");
+ }
}
+
+ private void verifyImdsCallWithToken() {
+ WireMock.verify(putRequestedFor(urlPathEqualTo(TOKEN_RESOURCE_PATH))
+ .withHeader(USER_AGENT_HEADER, equalTo(USER_AGENT)));
+ WireMock.verify(getRequestedFor(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH))
+ .withHeader(TOKEN_HEADER, equalTo(TOKEN_STUB))
+ .withHeader(USER_AGENT_HEADER, equalTo(USER_AGENT)));
+ WireMock.verify(getRequestedFor(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH + "some-profile"))
+ .withHeader(TOKEN_HEADER, equalTo(TOKEN_STUB))
+ .withHeader(USER_AGENT_HEADER, equalTo(USER_AGENT)));
+ }
+
+ private void verifyImdsCallInsecure() {
+ WireMock.verify(getRequestedFor(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH))
+ .withoutHeader(TOKEN_HEADER)
+ .withHeader(USER_AGENT_HEADER, equalTo(USER_AGENT)));
+ WireMock.verify(getRequestedFor(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH + "some-profile"))
+ .withoutHeader(TOKEN_HEADER)
+ .withHeader(USER_AGENT_HEADER, equalTo(USER_AGENT)));
+ }
+
+ private ProfileFile configFile(String credentialFile) {
+ return ProfileFile.builder()
+ .content(new StringInputStream(credentialFile))
+ .type(ProfileFile.Type.CONFIGURATION)
+ .build();
+ }
+
}