Skip to content

Commit d65352d

Browse files
committed
Add support for disable IMDS v1 fallback for regions
1 parent 4023c1a commit d65352d

File tree

6 files changed

+233
-19
lines changed

6 files changed

+233
-19
lines changed

.changes/next-release/feature-AWSSDKforJavav2-2c52c12.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@
22
"type": "feature",
33
"category": "AWS SDK for Java v2",
44
"contributor": "",
5-
"description": "Adds setting to disable making EC2 Instance Metadata Service (IMDS) calls for credentials 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."
5+
"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."
66
}

core/regions/src/main/java/software/amazon/awssdk/regions/internal/util/EC2MetadataUtils.java

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import software.amazon.awssdk.core.exception.SdkClientException;
3434
import software.amazon.awssdk.core.exception.SdkServiceException;
3535
import software.amazon.awssdk.core.util.SdkUserAgent;
36+
import software.amazon.awssdk.profiles.ProfileProperty;
3637
import software.amazon.awssdk.protocols.jsoncore.JsonNode;
3738
import software.amazon.awssdk.protocols.jsoncore.JsonNodeParser;
3839
import software.amazon.awssdk.regions.util.HttpResourcesUtils;
@@ -54,11 +55,13 @@
5455
* retrieve their content from the Amazon S3 bucket you specify at launch. To
5556
* add a new customer at any time, simply create a bucket for the customer, add
5657
* their content, and launch your AMI.<br>
57-
*
58-
* <P>
58+
* <p>
5959
* If {@link SdkSystemSetting#AWS_EC2_METADATA_DISABLED} is set to true, EC2 metadata usage
6060
* will be disabled and {@link SdkClientException} will be thrown for any metadata retrieval attempt.
61-
*
61+
* <p>
62+
* If {@link SdkSystemSetting#AWS_EC2_METADATA_V1_DISABLED} or {@link ProfileProperty#EC2_METADATA_V1_DISABLED}
63+
* is set to true, data will only be loaded from EC2 metadata service if a token is successfully retrieved -
64+
* fallback to load data without a token will be disabled.
6265
* <p>
6366
* More information about Amazon EC2 Metadata
6467
*
@@ -434,9 +437,28 @@ public static String getToken() {
434437
.cause(e)
435438
.build();
436439
}
440+
return handleTokenErrorResponse(e);
441+
}
442+
}
437443

438-
return null;
444+
private static String handleTokenErrorResponse(Exception e) {
445+
if (isInsecureFallbackDisabled()) {
446+
String message = String.format("Failed to retrieve IMDS token, and fallback to IMDS v1 is disabled via the "
447+
+ "%s system property, %s environment variable, or %s configuration file profile"
448+
+ " setting.",
449+
SdkSystemSetting.AWS_EC2_METADATA_V1_DISABLED.environmentVariable(),
450+
SdkSystemSetting.AWS_EC2_METADATA_V1_DISABLED.property(),
451+
ProfileProperty.EC2_METADATA_V1_DISABLED);
452+
throw SdkClientException.builder()
453+
.message(message)
454+
.cause(e)
455+
.build();
439456
}
457+
return null;
458+
}
459+
460+
private static boolean isInsecureFallbackDisabled() {
461+
return Ec2MetadataDisableV1Resolver.create().resolve();
440462
}
441463

442464
private static String fetchData(String path) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.awssdk.regions.internal.util;
17+
18+
import java.util.Optional;
19+
import java.util.function.Supplier;
20+
import software.amazon.awssdk.annotations.SdkInternalApi;
21+
import software.amazon.awssdk.core.SdkSystemSetting;
22+
import software.amazon.awssdk.profiles.ProfileFile;
23+
import software.amazon.awssdk.profiles.ProfileFileSystemSetting;
24+
import software.amazon.awssdk.profiles.ProfileProperty;
25+
import software.amazon.awssdk.utils.OptionalUtils;
26+
27+
@SdkInternalApi
28+
public final class Ec2MetadataDisableV1Resolver {
29+
30+
private Ec2MetadataDisableV1Resolver() {
31+
}
32+
33+
public static Ec2MetadataDisableV1Resolver create() {
34+
return new Ec2MetadataDisableV1Resolver();
35+
}
36+
37+
public boolean resolve() {
38+
return OptionalUtils.firstPresent(fromSystemSettings(), Ec2MetadataDisableV1Resolver::fromProfileFile)
39+
.orElse(false);
40+
}
41+
42+
private static Optional<Boolean> fromSystemSettings() {
43+
return SdkSystemSetting.AWS_EC2_METADATA_V1_DISABLED.getBooleanValue();
44+
}
45+
46+
private static Optional<Boolean> fromProfileFile() {
47+
Supplier<ProfileFile> profileFile = ProfileFile::defaultProfileFile;
48+
String profileName = ProfileFileSystemSetting.AWS_PROFILE.getStringValueOrThrow();
49+
if (profileFile.get() == null) {
50+
return Optional.empty();
51+
}
52+
return profileFile.get()
53+
.profile(profileName)
54+
.flatMap(p -> p.property(ProfileProperty.EC2_METADATA_V1_DISABLED))
55+
.map(Ec2MetadataDisableV1Resolver::safeProfileStringToBoolean);
56+
}
57+
58+
private static boolean safeProfileStringToBoolean(String value) {
59+
if (value.equalsIgnoreCase("true")) {
60+
return true;
61+
}
62+
if (value.equalsIgnoreCase("false")) {
63+
return false;
64+
}
65+
66+
throw new IllegalStateException("Profile property '" + ProfileProperty.EC2_METADATA_V1_DISABLED + "', "
67+
+ "was defined as '" + value + "', but should be 'false' or 'true'");
68+
}
69+
70+
}

core/regions/src/main/java/software/amazon/awssdk/regions/providers/InstanceProfileRegionProvider.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,20 @@
1818
import software.amazon.awssdk.annotations.SdkProtectedApi;
1919
import software.amazon.awssdk.core.SdkSystemSetting;
2020
import software.amazon.awssdk.core.exception.SdkClientException;
21+
import software.amazon.awssdk.profiles.ProfileProperty;
2122
import software.amazon.awssdk.regions.Region;
2223
import software.amazon.awssdk.regions.internal.util.EC2MetadataUtils;
2324

2425
/**
2526
* Attempts to load region information from the EC2 Metadata service. If the application is not
26-
* running on EC2 this provider will thrown an exception.
27-
*
28-
* <P>
27+
* running on EC2 this provider will throw an exception.
28+
* <p>
2929
* If {@link SdkSystemSetting#AWS_EC2_METADATA_DISABLED} is set to true, it will not try to load
3030
* region from EC2 metadata service and will return null.
31+
* <p>
32+
* If {@link SdkSystemSetting#AWS_EC2_METADATA_V1_DISABLED} or {@link ProfileProperty#EC2_METADATA_V1_DISABLED}
33+
* is set to true, the region will only be loaded from EC2 metadata service if a token is successfully retrieved -
34+
* fallback to load region without a token will be disabled.
3135
*/
3236
@SdkProtectedApi
3337
public final class InstanceProfileRegionProvider implements AwsRegionProvider {

core/regions/src/test/java/software/amazon/awssdk/regions/internal/util/EC2MetadataUtilsTest.java

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,10 @@ public class EC2MetadataUtilsTest {
3939
private static final String TOKEN_RESOURCE_PATH = "/latest/api/token";
4040
private static final String TOKEN_HEADER = "x-aws-ec2-metadata-token";
4141
private static final String EC2_METADATA_TOKEN_TTL_HEADER = "x-aws-ec2-metadata-token-ttl-seconds";
42-
4342
private static final String EC2_METADATA_ROOT = "/latest/meta-data";
44-
4543
private static final String AMI_ID_RESOURCE = EC2_METADATA_ROOT + "/ami-id";
44+
private static final String TOKEN_STUB = "some-token";
45+
private static final String EMPTY_BODY = "{}";
4646

4747

4848
@Rule
@@ -59,28 +59,27 @@ public void methodSetup() {
5959

6060
@Test
6161
public void getToken_queriesCorrectPath() {
62-
stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(aResponse().withBody("some-token")));
62+
stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(aResponse().withBody(TOKEN_STUB)));
6363

6464
String token = EC2MetadataUtils.getToken();
65-
assertThat(token).isEqualTo("some-token");
65+
assertThat(token).isEqualTo(TOKEN_STUB);
6666

6767
WireMock.verify(putRequestedFor(urlPathEqualTo(TOKEN_RESOURCE_PATH)).withHeader(EC2_METADATA_TOKEN_TTL_HEADER, equalTo("21600")));
6868
}
6969

7070
@Test
7171
public void getAmiId_queriesAndIncludesToken() {
72-
stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(aResponse().withBody("some-token")));
72+
stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(aResponse().withBody(TOKEN_STUB)));
7373
stubFor(get(urlPathEqualTo(AMI_ID_RESOURCE)).willReturn(aResponse().withBody("{}")));
7474

7575
EC2MetadataUtils.getAmiId();
7676

7777
WireMock.verify(putRequestedFor(urlPathEqualTo(TOKEN_RESOURCE_PATH)).withHeader(EC2_METADATA_TOKEN_TTL_HEADER, equalTo("21600")));
78-
WireMock.verify(getRequestedFor(urlPathEqualTo(AMI_ID_RESOURCE)).withHeader(TOKEN_HEADER, equalTo("some-token")));
78+
WireMock.verify(getRequestedFor(urlPathEqualTo(AMI_ID_RESOURCE)).withHeader(TOKEN_HEADER, equalTo(TOKEN_STUB)));
7979
}
8080

8181
@Test
8282
public void getAmiId_tokenQueryTimeout_fallsBackToInsecure() {
83-
8483
stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(aResponse().withFixedDelay(Integer.MAX_VALUE)));
8584
stubFor(get(urlPathEqualTo(AMI_ID_RESOURCE)).willReturn(aResponse().withBody("{}")));
8685

@@ -93,7 +92,7 @@ public void getAmiId_tokenQueryTimeout_fallsBackToInsecure() {
9392
@Test
9493
public void getAmiId_queriesTokenResource_403Error_fallbackToInsecure() {
9594
stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(aResponse().withStatus(403).withBody("oops")));
96-
stubFor(get(urlPathEqualTo(AMI_ID_RESOURCE)).willReturn(aResponse().withBody("{}")));
95+
stubFor(get(urlPathEqualTo(AMI_ID_RESOURCE)).willReturn(aResponse().withBody(EMPTY_BODY)));
9796

9897
EC2MetadataUtils.getAmiId();
9998

@@ -104,7 +103,7 @@ public void getAmiId_queriesTokenResource_403Error_fallbackToInsecure() {
104103
@Test
105104
public void getAmiId_queriesTokenResource_404Error_fallbackToInsecure() {
106105
stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(aResponse().withStatus(404).withBody("oops")));
107-
stubFor(get(urlPathEqualTo(AMI_ID_RESOURCE)).willReturn(aResponse().withBody("{}")));
106+
stubFor(get(urlPathEqualTo(AMI_ID_RESOURCE)).willReturn(aResponse().withBody(EMPTY_BODY)));
108107

109108
EC2MetadataUtils.getAmiId();
110109

@@ -115,14 +114,43 @@ public void getAmiId_queriesTokenResource_404Error_fallbackToInsecure() {
115114
@Test
116115
public void getAmiId_queriesTokenResource_405Error_fallbackToInsecure() {
117116
stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(aResponse().withStatus(405).withBody("oops")));
118-
stubFor(get(urlPathEqualTo(AMI_ID_RESOURCE)).willReturn(aResponse().withBody("{}")));
117+
stubFor(get(urlPathEqualTo(AMI_ID_RESOURCE)).willReturn(aResponse().withBody(EMPTY_BODY)));
119118

120119
EC2MetadataUtils.getAmiId();
121120

122121
WireMock.verify(putRequestedFor(urlPathEqualTo(TOKEN_RESOURCE_PATH)).withHeader(EC2_METADATA_TOKEN_TTL_HEADER, equalTo("21600")));
123122
WireMock.verify(getRequestedFor(urlPathEqualTo(AMI_ID_RESOURCE)).withoutHeader(TOKEN_HEADER));
124123
}
125124

125+
@Test
126+
public void getAmiId_fallbackToInsecureDisabledThroughProperty_throwsWhenTokenFails() {
127+
System.setProperty(SdkSystemSetting.AWS_EC2_METADATA_V1_DISABLED.property(), "true");
128+
stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(aResponse().withStatus(403).withBody("oops")));
129+
try {
130+
EC2MetadataUtils.getAmiId();
131+
} catch (Exception e) {
132+
assertThat(e).isInstanceOf(SdkClientException.class);
133+
assertThat(e).hasMessageContaining("fallback to IMDS v1 is disabled");
134+
}
135+
finally {
136+
System.clearProperty(SdkSystemSetting.AWS_EC2_METADATA_V1_DISABLED.property());
137+
}
138+
}
139+
140+
@Test
141+
public void getAmiId_fallbackToInsecureDisabledThroughProperty_returnsDataWhenTokenReturned() {
142+
System.setProperty(SdkSystemSetting.AWS_EC2_METADATA_V1_DISABLED.property(), "true");
143+
stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(aResponse().withBody(TOKEN_STUB)));
144+
stubFor(get(urlPathEqualTo(AMI_ID_RESOURCE)).willReturn(aResponse().withBody("{}")));
145+
try {
146+
EC2MetadataUtils.getAmiId();
147+
WireMock.verify(putRequestedFor(urlPathEqualTo(TOKEN_RESOURCE_PATH)).withHeader(EC2_METADATA_TOKEN_TTL_HEADER, equalTo("21600")));
148+
WireMock.verify(getRequestedFor(urlPathEqualTo(AMI_ID_RESOURCE)).withHeader(TOKEN_HEADER, equalTo(TOKEN_STUB)));
149+
} finally {
150+
System.clearProperty(SdkSystemSetting.AWS_EC2_METADATA_V1_DISABLED.property());
151+
}
152+
}
153+
126154
@Test
127155
public void getAmiId_queriesTokenResource_400Error_throws() {
128156
thrown.expect(SdkClientException.class);
@@ -140,7 +168,7 @@ public void fetchDataWithAttemptNumber_ioError_shouldHonor() {
140168
thrown.expect(SdkClientException.class);
141169
thrown.expectMessage("Unable to contact EC2 metadata service");
142170

143-
stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(aResponse().withBody("some-token")));;
171+
stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(aResponse().withBody(TOKEN_STUB)));
144172
stubFor(get(urlPathEqualTo(AMI_ID_RESOURCE)).willReturn(aResponse().withFault(Fault.CONNECTION_RESET_BY_PEER)));
145173

146174
EC2MetadataUtils.fetchData(AMI_ID_RESOURCE, false, attempts);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.awssdk.regions.internal.util;
17+
18+
import static org.assertj.core.api.Assertions.assertThat;
19+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
20+
21+
import java.util.stream.Stream;
22+
import org.junit.jupiter.api.BeforeEach;
23+
import org.junit.jupiter.params.ParameterizedTest;
24+
import org.junit.jupiter.params.provider.Arguments;
25+
import org.junit.jupiter.params.provider.MethodSource;
26+
import software.amazon.awssdk.core.SdkSystemSetting;
27+
import software.amazon.awssdk.testutils.EnvironmentVariableHelper;
28+
29+
public class Ec2MetadataDisableV1ResolverTest {
30+
31+
private static final EnvironmentVariableHelper ENVIRONMENT_VARIABLE_HELPER = new EnvironmentVariableHelper();
32+
33+
@BeforeEach
34+
public void methodSetup() {
35+
ENVIRONMENT_VARIABLE_HELPER.reset();
36+
System.clearProperty(SdkSystemSetting.AWS_EC2_METADATA_V1_DISABLED.property());
37+
}
38+
39+
@ParameterizedTest(name = "{index} - EXPECTED:{3} (sys:{0}, env:{1}, cfg:{2})")
40+
@MethodSource("booleanConfigValues")
41+
public void resolveDisableValue_whenBoolean_resolvesCorrectly(
42+
String systemProperty, String envVar, boolean expected) {
43+
44+
setUpSystemSettings(systemProperty, envVar);
45+
46+
Ec2MetadataDisableV1Resolver resolver = Ec2MetadataDisableV1Resolver.create();
47+
assertThat(resolver.resolve()).isEqualTo(expected);
48+
}
49+
50+
private static Stream<Arguments> booleanConfigValues() {
51+
return Stream.of(
52+
Arguments.of(null, null, false),
53+
Arguments.of("false", null, false),
54+
Arguments.of("true", null, true),
55+
Arguments.of(null, "false", false),
56+
Arguments.of(null, "true", true),
57+
Arguments.of(null, null, false),
58+
Arguments.of("false", "true", false),
59+
Arguments.of("true", "false", true)
60+
);
61+
}
62+
63+
@ParameterizedTest(name = "{index} - sys:{0}, env:{1}")
64+
@MethodSource("nonBooleanConfigValues")
65+
public void resolveDisableValue_whenNonBoolean_throws(String systemProperty, String envVar) {
66+
setUpSystemSettings(systemProperty, envVar);
67+
68+
Ec2MetadataDisableV1Resolver resolver = Ec2MetadataDisableV1Resolver.create();
69+
assertThatThrownBy(resolver::resolve).isInstanceOf(IllegalStateException.class)
70+
.hasMessageContaining("but should be 'false' or 'true'");
71+
}
72+
73+
private static Stream<Arguments> nonBooleanConfigValues() {
74+
return Stream.of(
75+
Arguments.of("foo", null, null),
76+
Arguments.of(null, "foo", null)
77+
);
78+
}
79+
80+
private static void setUpSystemSettings(String systemProperty, String envVar) {
81+
if (systemProperty != null) {
82+
System.setProperty(SdkSystemSetting.AWS_EC2_METADATA_V1_DISABLED.property(), systemProperty);
83+
84+
}
85+
if (envVar != null) {
86+
ENVIRONMENT_VARIABLE_HELPER.set(SdkSystemSetting.AWS_EC2_METADATA_V1_DISABLED.environmentVariable(),
87+
envVar);
88+
}
89+
}
90+
}

0 commit comments

Comments
 (0)