Skip to content

Commit 7564c68

Browse files
authored
Merge pull request #1513 from aws/imds
Add support for IMDS token
2 parents 0f466b4 + 5345141 commit 7564c68

File tree

5 files changed

+321
-12
lines changed

5 files changed

+321
-12
lines changed

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

Lines changed: 106 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,18 @@
1616
package software.amazon.awssdk.auth.credentials;
1717

1818
import java.io.IOException;
19+
import java.net.SocketTimeoutException;
1920
import java.net.URI;
21+
import java.util.HashMap;
22+
import java.util.Map;
2023
import software.amazon.awssdk.annotations.SdkPublicApi;
2124
import software.amazon.awssdk.core.SdkSystemSetting;
2225
import software.amazon.awssdk.core.exception.SdkClientException;
26+
import software.amazon.awssdk.core.exception.SdkServiceException;
27+
import software.amazon.awssdk.core.internal.util.UserAgentUtils;
2328
import software.amazon.awssdk.regions.util.HttpResourcesUtils;
2429
import software.amazon.awssdk.regions.util.ResourcesEndpointProvider;
30+
import software.amazon.awssdk.utils.Logger;
2531
import software.amazon.awssdk.utils.ToString;
2632

2733
/**
@@ -33,9 +39,15 @@
3339
*/
3440
@SdkPublicApi
3541
public final class InstanceProfileCredentialsProvider extends HttpCredentialsProvider {
42+
private static final Logger log = Logger.loggerFor(InstanceProfileCredentialsProvider.class);
43+
44+
private static final String TOKEN_RESOURCE_PATH = "/latest/api/token";
45+
private static final String EC2_METADATA_TOKEN_HEADER = "x-aws-ec2-metadata-token";
46+
private static final String EC2_METADATA_TOKEN_TTL_HEADER = "x-aws-ec2-metadata-token-ttl-seconds";
47+
private static final String DEFAULT_TOKEN_TTL = "21600";
3648

3749
private static final String SECURITY_CREDENTIALS_RESOURCE = "/latest/meta-data/iam/security-credentials/";
38-
private final ResourcesEndpointProvider credentialsEndpointProvider = new InstanceProviderCredentialsEndpointProvider();
50+
private final InstanceProviderTokenEndpointProvider tokenEndpointProvider = new InstanceProviderTokenEndpointProvider();
3951

4052
/**
4153
* @see #builder()
@@ -62,7 +74,7 @@ public static InstanceProfileCredentialsProvider create() {
6274

6375
@Override
6476
protected ResourcesEndpointProvider getCredentialsEndpointProvider() {
65-
return credentialsEndpointProvider;
77+
return new InstanceProviderCredentialsEndpointProvider(getToken());
6678
}
6779

6880
@Override
@@ -75,13 +87,68 @@ public String toString() {
7587
return ToString.create("InstanceProfileCredentialsProvider");
7688
}
7789

90+
private String getToken() {
91+
try {
92+
return HttpResourcesUtils.instance().readResource(getTokenEndpointProvider(), "PUT");
93+
} catch (Exception e) {
94+
log.debug(() -> "Error retrieving credentials metadata token", e);
95+
96+
boolean is400ServiceException = e instanceof SdkServiceException
97+
&& ((SdkServiceException) e).statusCode() == 400;
98+
99+
boolean isSocketTimeout = e instanceof SocketTimeoutException;
100+
101+
// Credentials resolution must not continue to the token-less flow if either of these errors occur
102+
if (is400ServiceException || isSocketTimeout) {
103+
throw SdkClientException.builder()
104+
.message("Unable to load credentials from service endpoint")
105+
.cause(e)
106+
.build();
107+
}
108+
109+
return null;
110+
}
111+
}
112+
113+
private ResourcesEndpointProvider getTokenEndpointProvider() {
114+
return tokenEndpointProvider;
115+
}
116+
117+
private static ResourcesEndpointProvider includeTokenHeader(ResourcesEndpointProvider provider, String token) {
118+
return new ResourcesEndpointProvider() {
119+
@Override
120+
public URI endpoint() throws IOException {
121+
return provider.endpoint();
122+
}
123+
124+
@Override
125+
public Map<String, String> headers() {
126+
Map<String, String> headers = new HashMap<>(provider.headers());
127+
headers.put(EC2_METADATA_TOKEN_HEADER, token);
128+
return headers;
129+
}
130+
};
131+
}
132+
78133
private static final class InstanceProviderCredentialsEndpointProvider implements ResourcesEndpointProvider {
134+
private final String metadataToken;
135+
136+
private InstanceProviderCredentialsEndpointProvider(String metadataToken) {
137+
this.metadataToken = metadataToken;
138+
}
139+
79140
@Override
80141
public URI endpoint() throws IOException {
81142
String host = SdkSystemSetting.AWS_EC2_METADATA_SERVICE_ENDPOINT.getStringValueOrThrow();
82143

83144
URI endpoint = URI.create(host + SECURITY_CREDENTIALS_RESOURCE);
84-
String securityCredentialsList = HttpResourcesUtils.instance().readResource(endpoint);
145+
ResourcesEndpointProvider endpointProvider = () -> endpoint;
146+
147+
if (metadataToken != null) {
148+
endpointProvider = includeTokenHeader(endpointProvider, metadataToken);
149+
}
150+
151+
String securityCredentialsList = HttpResourcesUtils.instance().readResource(endpointProvider);
85152
String[] securityCredentials = securityCredentialsList.trim().split("\n");
86153

87154
if (securityCredentials.length == 0) {
@@ -90,6 +157,42 @@ public URI endpoint() throws IOException {
90157

91158
return URI.create(host + SECURITY_CREDENTIALS_RESOURCE + securityCredentials[0]);
92159
}
160+
161+
@Override
162+
public Map<String, String> headers() {
163+
Map<String, String> requestHeaders = new HashMap<>();
164+
requestHeaders.put("User-Agent", UserAgentUtils.getUserAgent());
165+
requestHeaders.put("Accept", "*/*");
166+
requestHeaders.put("Connection", "keep-alive");
167+
168+
if (metadataToken != null) {
169+
requestHeaders.put(EC2_METADATA_TOKEN_HEADER, metadataToken);
170+
}
171+
172+
return requestHeaders;
173+
}
174+
}
175+
176+
private static final class InstanceProviderTokenEndpointProvider implements ResourcesEndpointProvider {
177+
@Override
178+
public URI endpoint() {
179+
String host = SdkSystemSetting.AWS_EC2_METADATA_SERVICE_ENDPOINT.getStringValueOrThrow();
180+
if (host.endsWith("/")) {
181+
host = host.substring(0, host.length() - 1);
182+
}
183+
return URI.create(host + TOKEN_RESOURCE_PATH);
184+
}
185+
186+
@Override
187+
public Map<String, String> headers() {
188+
Map<String, String> requestHeaders = new HashMap<>();
189+
requestHeaders.put("User-Agent", UserAgentUtils.getUserAgent());
190+
requestHeaders.put("Accept", "*/*");
191+
requestHeaders.put("Connection", "keep-alive");
192+
requestHeaders.put(EC2_METADATA_TOKEN_TTL_HEADER, DEFAULT_TOKEN_TTL);
193+
194+
return requestHeaders;
195+
}
93196
}
94197

95198
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
/*
2+
* Copyright 2010-2019 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;
17+
18+
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
19+
import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
20+
import static com.github.tomakehurst.wiremock.client.WireMock.get;
21+
import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor;
22+
import static com.github.tomakehurst.wiremock.client.WireMock.put;
23+
import static com.github.tomakehurst.wiremock.client.WireMock.putRequestedFor;
24+
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
25+
import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
26+
import static org.hamcrest.Matchers.instanceOf;
27+
import com.github.tomakehurst.wiremock.client.WireMock;
28+
import com.github.tomakehurst.wiremock.junit.WireMockRule;
29+
import java.net.SocketTimeoutException;
30+
import java.time.Duration;
31+
import java.time.Instant;
32+
import org.junit.AfterClass;
33+
import org.junit.Before;
34+
import org.junit.Rule;
35+
import org.junit.Test;
36+
import org.junit.rules.ExpectedException;
37+
import software.amazon.awssdk.core.SdkSystemSetting;
38+
import software.amazon.awssdk.core.exception.SdkClientException;
39+
import software.amazon.awssdk.core.internal.util.UserAgentUtils;
40+
import software.amazon.awssdk.utils.DateUtils;
41+
42+
public class InstanceProfileCredentialsProviderTest {
43+
private static final String TOKEN_RESOURCE_PATH = "/latest/api/token";
44+
private static final String CREDENTIALS_RESOURCE_PATH = "/latest/meta-data/iam/security-credentials/";
45+
private static final String STUB_CREDENTIALS = "{\"AccessKeyId\":\"ACCESS_KEY_ID\",\"SecretAccessKey\":\"SECRET_ACCESS_KEY\","
46+
+ "\"Expiration\":\"" + DateUtils.formatIso8601Date(Instant.now().plus(Duration.ofDays(1)))
47+
+ "\"}";
48+
private static final String TOKEN_HEADER = "x-aws-ec2-metadata-token";
49+
private static final String EC2_METADATA_TOKEN_TTL_HEADER = "x-aws-ec2-metadata-token-ttl-seconds";
50+
51+
52+
@Rule
53+
public ExpectedException thrown = ExpectedException.none();
54+
55+
@Rule
56+
public WireMockRule mockMetadataEndpoint = new WireMockRule();
57+
58+
@Before
59+
public void methodSetup() {
60+
System.setProperty(SdkSystemSetting.AWS_EC2_METADATA_SERVICE_ENDPOINT.property(), "http://localhost:" + mockMetadataEndpoint.port());
61+
}
62+
63+
@AfterClass
64+
public static void teardown() {
65+
System.clearProperty(SdkSystemSetting.AWS_EC2_METADATA_SERVICE_ENDPOINT.property());
66+
}
67+
68+
@Test
69+
public void resolveCredentials_metadataLookupDisabled_throws() {
70+
System.setProperty(SdkSystemSetting.AWS_EC2_METADATA_DISABLED.property(), "true");
71+
thrown.expect(SdkClientException.class);
72+
thrown.expectMessage("Loading credentials from local endpoint is disabled");
73+
try {
74+
InstanceProfileCredentialsProvider.builder().build().resolveCredentials();
75+
} finally {
76+
System.clearProperty(SdkSystemSetting.AWS_EC2_METADATA_DISABLED.property());
77+
}
78+
}
79+
80+
@Test
81+
public void resolveCredentials_requestsIncludeUserAgent() {
82+
String stubToken = "some-token";
83+
stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(aResponse().withBody(stubToken)));
84+
stubFor(get(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH)).willReturn(aResponse().withBody("some-profile")));
85+
stubFor(get(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH + "some-profile")).willReturn(aResponse().withBody(STUB_CREDENTIALS)));
86+
87+
InstanceProfileCredentialsProvider provider = InstanceProfileCredentialsProvider.builder().build();
88+
89+
provider.resolveCredentials();
90+
91+
String userAgentHeader = "User-Agent";
92+
String userAgent = UserAgentUtils.getUserAgent();
93+
WireMock.verify(putRequestedFor(urlPathEqualTo(TOKEN_RESOURCE_PATH)).withHeader(userAgentHeader, equalTo(userAgent)));
94+
WireMock.verify(getRequestedFor(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH)).withHeader(userAgentHeader, equalTo(userAgent)));
95+
WireMock.verify(getRequestedFor(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH + "some-profile")).withHeader(userAgentHeader, equalTo(userAgent)));
96+
}
97+
98+
@Test
99+
public void resolveCredentials_queriesTokenResource() {
100+
stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(aResponse().withBody("some-token")));
101+
stubFor(get(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH)).willReturn(aResponse().withBody("some-profile")));
102+
stubFor(get(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH + "some-profile")).willReturn(aResponse().withBody(STUB_CREDENTIALS)));
103+
104+
InstanceProfileCredentialsProvider provider = InstanceProfileCredentialsProvider.builder().build();
105+
106+
provider.resolveCredentials();
107+
108+
WireMock.verify(putRequestedFor(urlPathEqualTo(TOKEN_RESOURCE_PATH)).withHeader(EC2_METADATA_TOKEN_TTL_HEADER, equalTo("21600")));
109+
}
110+
111+
@Test
112+
public void resolveCredentials_queriesTokenResource_includedInCredentialsRequests() {
113+
String stubToken = "some-token";
114+
stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(aResponse().withBody(stubToken)));
115+
stubFor(get(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH)).willReturn(aResponse().withBody("some-profile")));
116+
stubFor(get(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH + "some-profile")).willReturn(aResponse().withBody(STUB_CREDENTIALS)));
117+
118+
InstanceProfileCredentialsProvider provider = InstanceProfileCredentialsProvider.builder().build();
119+
120+
provider.resolveCredentials();
121+
122+
WireMock.verify(getRequestedFor(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH)).withHeader(TOKEN_HEADER, equalTo(stubToken)));
123+
WireMock.verify(getRequestedFor(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH + "some-profile")).withHeader(TOKEN_HEADER, equalTo(stubToken)));
124+
}
125+
126+
@Test
127+
public void resolveCredentials_queriesTokenResource_404Error_fallbackToInsecure() {
128+
stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(aResponse().withStatus(404).withBody("oops")));
129+
stubFor(get(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH)).willReturn(aResponse().withBody("some-profile")));
130+
stubFor(get(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH + "some-profile")).willReturn(aResponse().withBody(STUB_CREDENTIALS)));
131+
132+
InstanceProfileCredentialsProvider provider = InstanceProfileCredentialsProvider.builder().build();
133+
134+
provider.resolveCredentials();
135+
136+
WireMock.verify(getRequestedFor(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH)));
137+
WireMock.verify(getRequestedFor(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH + "some-profile")));
138+
}
139+
140+
@Test
141+
public void resolveCredentials_queriesTokenResource_400Error_throws() {
142+
thrown.expect(SdkClientException.class);
143+
thrown.expectMessage("Unable to load credentials");
144+
145+
stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(aResponse().withStatus(400).withBody("oops")));
146+
147+
InstanceProfileCredentialsProvider provider = InstanceProfileCredentialsProvider.builder().build();
148+
149+
provider.resolveCredentials();
150+
}
151+
152+
@Test
153+
public void resolveCredentials_queriesTokenResource_socketTimeout_throws() {
154+
thrown.expect(SdkClientException.class);
155+
thrown.expectCause(instanceOf(SocketTimeoutException.class));
156+
thrown.expectMessage("Unable to load credentials");
157+
158+
stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(aResponse().withBody("some-token").withFixedDelay(Integer.MAX_VALUE)));
159+
160+
InstanceProfileCredentialsProvider provider = InstanceProfileCredentialsProvider.builder().build();
161+
162+
provider.resolveCredentials();
163+
}
164+
165+
@Test
166+
public void resolveCredentials_endpointSettingEmpty_throws() {
167+
thrown.expect(SdkClientException.class);
168+
169+
System.setProperty(SdkSystemSetting.AWS_EC2_METADATA_SERVICE_ENDPOINT.property(), "");
170+
InstanceProfileCredentialsProvider provider = InstanceProfileCredentialsProvider.builder().build();
171+
172+
provider.resolveCredentials();
173+
}
174+
175+
@Test
176+
public void resolveCredentials_endpointSettingHostNotExists_throws() {
177+
thrown.expect(SdkClientException.class);
178+
179+
System.setProperty(SdkSystemSetting.AWS_EC2_METADATA_SERVICE_ENDPOINT.property(), "some-host-that-does-not-exist");
180+
InstanceProfileCredentialsProvider provider = InstanceProfileCredentialsProvider.builder().build();
181+
182+
provider.resolveCredentials();
183+
}
184+
}

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,14 @@ public static ConnectionUtils create() {
3131
}
3232

3333
public HttpURLConnection connectToEndpoint(URI endpoint, Map<String, String> headers) throws IOException {
34+
return connectToEndpoint(endpoint, headers, "GET");
35+
}
36+
37+
public HttpURLConnection connectToEndpoint(URI endpoint, Map<String, String> headers, String method) throws IOException {
3438
HttpURLConnection connection = (HttpURLConnection) endpoint.toURL().openConnection(Proxy.NO_PROXY);
3539
connection.setConnectTimeout(1000 * 2);
3640
connection.setReadTimeout(1000 * 5);
37-
connection.setRequestMethod("GET");
41+
connection.setRequestMethod(method);
3842
connection.setDoOutput(true);
3943
headers.forEach(connection::addRequestProperty);
4044
connection.setInstanceFollowRedirects(false);

0 commit comments

Comments
 (0)