From c7417f06cef2677ced1c2e81d54d9fd3a0b524b8 Mon Sep 17 00:00:00 2001 From: Anna-Karin Salander Date: Thu, 1 Feb 2024 22:53:47 +0300 Subject: [PATCH] Add option to disable EC2 metadata (IMDS) calls without token (#4866) * Add option to disable IMDS v1 fallback when token is not returned (#4840) * Add support for disable IMDS v1 fallback for regions --- .../feature-AWSSDKforJavav2-2c52c12.json | 6 + .../InstanceProfileCredentialsProvider.java | 51 ++- .../Ec2MetadataDisableV1Resolver.java | 63 ++++ .../internal/ProfileCredentialsUtils.java | 11 +- ...nstanceProfileCredentialsProviderTest.java | 332 ++++++++++-------- .../Ec2MetadataDisableV1ResolverTest.java | 129 +++++++ .../awssdk/profiles/ProfileProperty.java | 2 + .../internal/util/EC2MetadataUtils.java | 46 ++- .../util/Ec2MetadataDisableV1Resolver.java | 70 ++++ .../InstanceProfileRegionProvider.java | 10 +- .../internal/util/EC2MetadataUtilsTest.java | 52 ++- .../Ec2MetadataDisableV1ResolverTest.java | 90 +++++ .../amazon/awssdk/core/SdkSystemSetting.java | 5 + ...ileCredentialsProviderIntegrationTest.java | 147 ++++++-- 14 files changed, 795 insertions(+), 219 deletions(-) create mode 100644 .changes/next-release/feature-AWSSDKforJavav2-2c52c12.json create mode 100644 core/auth/src/main/java/software/amazon/awssdk/auth/credentials/internal/Ec2MetadataDisableV1Resolver.java create mode 100644 core/auth/src/test/java/software/amazon/awssdk/auth/credentials/internal/Ec2MetadataDisableV1ResolverTest.java create mode 100644 core/regions/src/main/java/software/amazon/awssdk/regions/internal/util/Ec2MetadataDisableV1Resolver.java create mode 100644 core/regions/src/test/java/software/amazon/awssdk/regions/internal/util/Ec2MetadataDisableV1ResolverTest.java 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(); + } + }