Skip to content

Commit 00a503b

Browse files
authored
Add support for IMDS over IPv6 (#2585)
This changes implements support for the 'AWS_EC2_METADATA_SERVICE_ENDPOINT_MODE' environment variable and 'ec2_metadata_service_endpoint' profile file property. When no endpoint override is set using 'AWS_EC2_METADATA_SERVICE_ENDPOINT', this configuration controls which of the default IMDS endpoints the client will use. Valid values are 'IPv4' or 'IPv6'.
1 parent af72d85 commit 00a503b

File tree

34 files changed

+1330
-36
lines changed

34 files changed

+1330
-36
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"type": "feature",
3+
"category": "AWS SDK for Java v2",
4+
"contributor": "",
5+
"description": "This changes implements support for the `AWS_EC2_METADATA_SERVICE_ENDPOINT_MODE` environment variable and\n`ec2_metadata_service_endpoint` profile file property.\n\nWhen no endpoint override is set using `AWS_EC2_METADATA_SERVICE_ENDPOINT`, this configuration controls which of the default\nIMDS endpoints the client will use. Valid values are `IPv4` or `IPv6`"
6+
}

buildspecs/release-to-maven.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ phases:
2626
if ! curl -f --head $SONATYPE_URL; then
2727
mkdir -p $CREDENTIALS
2828
aws s3 cp s3://aws-java-sdk-release-credentials/ $CREDENTIALS/ --recursive
29-
mvn clean deploy -B -s $SETTINGS_XML -Dgpg.homedir=$GPG_HOME -Ppublishing -DperformRelease -Dspotbugs.skip -DskipTests -Dcheckstyle.skip -Djapicmp.skip -Ddoclint=none -pl !:protocol-tests,!:protocol-tests-core,!:codegen-generated-classes-test,!:sdk-benchmarks,!:module-path-tests,!:tests-coverage-reporting,!:stability-tests,!:sdk-native-image-test -DautoReleaseAfterClose=true -DstagingProgressTimeoutMinutes=30
29+
mvn clean deploy -B -s $SETTINGS_XML -Dgpg.homedir=$GPG_HOME -Ppublishing -DperformRelease -Dspotbugs.skip -DskipTests -Dcheckstyle.skip -Djapicmp.skip -Ddoclint=none -pl !:protocol-tests,!:protocol-tests-core,!:codegen-generated-classes-test,!:sdk-benchmarks,!:module-path-tests,!:tests-coverage-reporting,!:stability-tests,!:sdk-native-image-test,!:auth-sts-testing -DautoReleaseAfterClose=true -DstagingProgressTimeoutMinutes=30
3030
else
3131
echo "This version was already released."
3232
fi

core/auth/src/main/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProvider.java

Lines changed: 80 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,11 @@
2020
import java.util.HashMap;
2121
import java.util.Map;
2222
import software.amazon.awssdk.annotations.SdkPublicApi;
23+
import software.amazon.awssdk.auth.credentials.internal.Ec2MetadataConfigProvider;
2324
import software.amazon.awssdk.core.SdkSystemSetting;
2425
import software.amazon.awssdk.core.exception.SdkClientException;
26+
import software.amazon.awssdk.core.exception.SdkServiceException;
2527
import software.amazon.awssdk.core.internal.util.UserAgentUtils;
26-
import software.amazon.awssdk.regions.internal.util.EC2MetadataUtils;
2728
import software.amazon.awssdk.regions.util.HttpResourcesUtils;
2829
import software.amazon.awssdk.regions.util.ResourcesEndpointProvider;
2930
import software.amazon.awssdk.utils.ToString;
@@ -41,11 +42,15 @@ public final class InstanceProfileCredentialsProvider extends HttpCredentialsPro
4142

4243
private static final String SECURITY_CREDENTIALS_RESOURCE = "/latest/meta-data/iam/security-credentials/";
4344

45+
private final String endpoint;
46+
private final Ec2MetadataConfigProvider configProvider = Ec2MetadataConfigProvider.builder().build();
47+
4448
/**
4549
* @see #builder()
4650
*/
4751
private InstanceProfileCredentialsProvider(BuilderImpl builder) {
4852
super(builder);
53+
this.endpoint = builder.endpoint;
4954
}
5055

5156
/**
@@ -66,7 +71,7 @@ public static InstanceProfileCredentialsProvider create() {
6671

6772
@Override
6873
protected ResourcesEndpointProvider getCredentialsEndpointProvider() {
69-
return new InstanceProviderCredentialsEndpointProvider(getToken());
74+
return new InstanceProviderCredentialsEndpointProvider(getImdsEndpoint(), getToken());
7075
}
7176

7277
@Override
@@ -80,7 +85,24 @@ public String toString() {
8085
}
8186

8287
private String getToken() {
83-
return EC2MetadataUtils.getToken();
88+
try {
89+
return HttpResourcesUtils.instance()
90+
.readResource(new TokenEndpointProvider(getImdsEndpoint()), "PUT");
91+
} catch (Exception e) {
92+
93+
boolean is400ServiceException = e instanceof SdkServiceException
94+
&& ((SdkServiceException) e).statusCode() == 400;
95+
96+
// metadata resolution must not continue to the token-less flow for a 400
97+
if (is400ServiceException) {
98+
throw SdkClientException.builder()
99+
.message("Unable to fetch metadata token")
100+
.cause(e)
101+
.build();
102+
}
103+
104+
return null;
105+
}
84106
}
85107

86108
private static ResourcesEndpointProvider includeTokenHeader(ResourcesEndpointProvider provider, String token) {
@@ -99,18 +121,26 @@ public Map<String, String> headers() {
99121
};
100122
}
101123

124+
private String getImdsEndpoint() {
125+
if (endpoint != null) {
126+
return endpoint;
127+
}
128+
129+
return configProvider.getEndpoint();
130+
}
131+
102132
private static final class InstanceProviderCredentialsEndpointProvider implements ResourcesEndpointProvider {
133+
private final String imdsEndpoint;
103134
private final String metadataToken;
104135

105-
private InstanceProviderCredentialsEndpointProvider(String metadataToken) {
136+
private InstanceProviderCredentialsEndpointProvider(String imdsEndpoint, String metadataToken) {
137+
this.imdsEndpoint = imdsEndpoint;
106138
this.metadataToken = metadataToken;
107139
}
108140

109141
@Override
110142
public URI endpoint() throws IOException {
111-
String host = SdkSystemSetting.AWS_EC2_METADATA_SERVICE_ENDPOINT.getStringValueOrThrow();
112-
113-
URI endpoint = URI.create(host + SECURITY_CREDENTIALS_RESOURCE);
143+
URI endpoint = URI.create(imdsEndpoint + SECURITY_CREDENTIALS_RESOURCE);
114144
ResourcesEndpointProvider endpointProvider = () -> endpoint;
115145

116146
if (metadataToken != null) {
@@ -124,7 +154,7 @@ public URI endpoint() throws IOException {
124154
throw SdkClientException.builder().message("Unable to load credentials path").build();
125155
}
126156

127-
return URI.create(host + SECURITY_CREDENTIALS_RESOURCE + securityCredentials[0]);
157+
return URI.create(imdsEndpoint + SECURITY_CREDENTIALS_RESOURCE + securityCredentials[0]);
128158
}
129159

130160
@Override
@@ -142,12 +172,46 @@ public Map<String, String> headers() {
142172
}
143173
}
144174

175+
private static final class TokenEndpointProvider implements ResourcesEndpointProvider {
176+
private static final String TOKEN_RESOURCE_PATH = "/latest/api/token";
177+
private static final String EC2_METADATA_TOKEN_TTL_HEADER = "x-aws-ec2-metadata-token-ttl-seconds";
178+
private static final String DEFAULT_TOKEN_TTL = "21600";
179+
180+
private final String host;
181+
182+
private TokenEndpointProvider(String host) {
183+
this.host = host;
184+
}
185+
186+
@Override
187+
public URI endpoint() {
188+
String finalHost = host;
189+
if (finalHost.endsWith("/")) {
190+
finalHost = finalHost.substring(0, finalHost.length() - 1);
191+
}
192+
return URI.create(finalHost + TOKEN_RESOURCE_PATH);
193+
}
194+
195+
@Override
196+
public Map<String, String> headers() {
197+
Map<String, String> requestHeaders = new HashMap<>();
198+
requestHeaders.put("User-Agent", UserAgentUtils.getUserAgent());
199+
requestHeaders.put("Accept", "*/*");
200+
requestHeaders.put("Connection", "keep-alive");
201+
requestHeaders.put(EC2_METADATA_TOKEN_TTL_HEADER, DEFAULT_TOKEN_TTL);
202+
203+
return requestHeaders;
204+
}
205+
}
206+
145207

146208
/**
147209
* A builder for creating a custom a {@link InstanceProfileCredentialsProvider}.
148210
*/
149211
public interface Builder extends HttpCredentialsProvider.Builder<InstanceProfileCredentialsProvider, Builder> {
150212

213+
Builder endpoint(String endpoint);
214+
151215
/**
152216
* Build a {@link InstanceProfileCredentialsProvider} from the provided configuration.
153217
*/
@@ -159,10 +223,18 @@ private static final class BuilderImpl
159223
extends HttpCredentialsProvider.BuilderImpl<InstanceProfileCredentialsProvider, Builder>
160224
implements Builder {
161225

226+
private String endpoint;
227+
162228
private BuilderImpl() {
163229
super.asyncThreadName("instance-profile-credentials-provider");
164230
}
165231

232+
@Override
233+
public Builder endpoint(String endpoint) {
234+
this.endpoint = endpoint;
235+
return this;
236+
}
237+
166238
@Override
167239
public InstanceProfileCredentialsProvider build() {
168240
return new InstanceProfileCredentialsProvider(this);

core/auth/src/main/java/software/amazon/awssdk/auth/credentials/ProfileCredentialsProvider.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,8 @@ private ProfileCredentialsProvider(BuilderImpl builder) {
6969
ProfileFile finalProfileFile = profileFile;
7070
credentialsProvider =
7171
profileFile.profile(profileName)
72-
.flatMap(p -> new ProfileCredentialsUtils(p, finalProfileFile::profile).credentialsProvider())
72+
.flatMap(p -> new ProfileCredentialsUtils(finalProfileFile, p, finalProfileFile::profile)
73+
.credentialsProvider())
7374
.orElseThrow(() -> {
7475
String errorMessage = String.format("Profile file contained no credentials for " +
7576
"profile '%s': %s", finalProfileName, finalProfileFile);
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
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.auth.credentials.internal;
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.core.exception.SdkClientException;
23+
import software.amazon.awssdk.profiles.Profile;
24+
import software.amazon.awssdk.profiles.ProfileFile;
25+
import software.amazon.awssdk.profiles.ProfileFileSystemSetting;
26+
import software.amazon.awssdk.profiles.ProfileProperty;
27+
28+
@SdkInternalApi
29+
// TODO: Remove or consolidate this class with the one from the regions module.
30+
// There's currently no good way for both auth and regions to share the same
31+
// class since there's no suitable common dependency between the two where this
32+
// can live. Ideally, we can do this when the EC2MetadataUtils is replaced with
33+
// the IMDS client.
34+
public final class Ec2MetadataConfigProvider {
35+
/** Default IPv4 endpoint for the Amazon EC2 Instance Metadata Service. */
36+
private static final String EC2_METADATA_SERVICE_URL_IPV4 = "http://169.254.169.254";
37+
38+
/** Default IPv6 endpoint for the Amazon EC2 Instance Metadata Service. */
39+
private static final String EC2_METADATA_SERVICE_URL_IPV6 = "http://[fd00:ec2::254]";
40+
41+
private final Supplier<ProfileFile> profileFile;
42+
private final String profileName;
43+
44+
private Ec2MetadataConfigProvider(Builder builder) {
45+
this.profileFile = builder.profileFile;
46+
this.profileName = builder.profileName;
47+
}
48+
49+
public enum EndpointMode {
50+
IPV4,
51+
IPV6,
52+
;
53+
54+
public static EndpointMode fromValue(String s) {
55+
if (s == null) {
56+
return null;
57+
}
58+
59+
for (EndpointMode value : EndpointMode.values()) {
60+
if (value.name().equalsIgnoreCase(s)) {
61+
return value;
62+
}
63+
}
64+
65+
throw new IllegalArgumentException("Unrecognized value for endpoint mode: " + s);
66+
}
67+
}
68+
69+
public String getEndpoint() {
70+
String endpointOverride = getEndpointOverride();
71+
if (endpointOverride != null) {
72+
return endpointOverride;
73+
}
74+
75+
EndpointMode endpointMode = getEndpointMode();
76+
switch (endpointMode) {
77+
case IPV4:
78+
return EC2_METADATA_SERVICE_URL_IPV4;
79+
case IPV6:
80+
return EC2_METADATA_SERVICE_URL_IPV6;
81+
default:
82+
throw SdkClientException.create("Unknown endpoint mode: " + endpointMode);
83+
}
84+
}
85+
86+
public EndpointMode getEndpointMode() {
87+
Optional<String> endpointMode = SdkSystemSetting.AWS_EC2_METADATA_SERVICE_ENDPOINT_MODE.getNonDefaultStringValue();
88+
if (endpointMode.isPresent()) {
89+
return EndpointMode.fromValue(endpointMode.get());
90+
}
91+
92+
return configFileEndpointMode().orElseGet(() ->
93+
EndpointMode.fromValue(SdkSystemSetting.AWS_EC2_METADATA_SERVICE_ENDPOINT_MODE.defaultValue()));
94+
}
95+
96+
public String getEndpointOverride() {
97+
Optional<String> endpointOverride = SdkSystemSetting.AWS_EC2_METADATA_SERVICE_ENDPOINT.getNonDefaultStringValue();
98+
if (endpointOverride.isPresent()) {
99+
return endpointOverride.get();
100+
}
101+
102+
Optional<String> configFileValue = configFileEndpointOverride();
103+
104+
return configFileValue.orElse(null);
105+
}
106+
107+
public static Builder builder() {
108+
return new Builder();
109+
}
110+
111+
private Optional<EndpointMode> configFileEndpointMode() {
112+
return resolveProfile().flatMap(p -> p.property(ProfileProperty.EC2_METADATA_SERVICE_ENDPOINT_MODE))
113+
.map(EndpointMode::fromValue);
114+
}
115+
116+
private Optional<String> configFileEndpointOverride() {
117+
return resolveProfile().flatMap(p -> p.property(ProfileProperty.EC2_METADATA_SERVICE_ENDPOINT));
118+
}
119+
120+
private Optional<Profile> resolveProfile() {
121+
ProfileFile profileFileToUse = resolveProfileFile();
122+
String profileNameToUse = resolveProfileName();
123+
124+
return profileFileToUse.profile(profileNameToUse);
125+
}
126+
127+
private ProfileFile resolveProfileFile() {
128+
if (profileFile != null) {
129+
return profileFile.get();
130+
}
131+
132+
return ProfileFile.defaultProfileFile();
133+
}
134+
135+
private String resolveProfileName() {
136+
if (profileName != null) {
137+
return profileName;
138+
}
139+
140+
return ProfileFileSystemSetting.AWS_PROFILE.getStringValueOrThrow();
141+
}
142+
143+
public static class Builder {
144+
private Supplier<ProfileFile> profileFile;
145+
private String profileName;
146+
147+
public Builder profileFile(Supplier<ProfileFile> profileFile) {
148+
this.profileFile = profileFile;
149+
return this;
150+
}
151+
152+
public Builder profileName(String profileName) {
153+
this.profileName = profileName;
154+
return this;
155+
}
156+
157+
public Ec2MetadataConfigProvider build() {
158+
return new Ec2MetadataConfigProvider(this);
159+
}
160+
}
161+
}

0 commit comments

Comments
 (0)