Skip to content

Commit edfe8d5

Browse files
authored
EC2 IMDS Changes to Support Account ID (#6176)
* EC2 IMDS Changes to Support Account ID * Fixing dependency issue * Addition additional tests and replacing initialization * Updating the test file name * Additional Changes -created a new integration test file for IMDS extended url separating it from legacy -Included the status code to the fallback logic * Additional Changes - Removed the duplicate test files - Make ApiVersion Volatile * Additional Changes: -Adding additional tests -Updating to use AtomicReference * Address PR feedback: Updating the debug logging message Modified the fallback logic in refresh credentials * Replacing AtomicReference fields and updating test file * Adding retry counter to the fallback logic * Updating the fallback logic * Adding comment
1 parent 81cf9cc commit edfe8d5

File tree

8 files changed

+440
-16
lines changed

8 files changed

+440
-16
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": "Include the account ID associated with the credentials retrieved from IMDS when available."
6+
}

core/auth/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@
5858
<artifactId>regions</artifactId>
5959
<version>${awsjavasdk.version}</version>
6060
</dependency>
61+
<dependency>
62+
<groupId>software.amazon.awssdk</groupId>
63+
<artifactId>imds</artifactId>
64+
<version>${awsjavasdk.version}</version>
65+
</dependency>
6166
<dependency>
6267
<groupId>software.amazon.awssdk</groupId>
6368
<artifactId>profiles</artifactId>

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

Lines changed: 75 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import software.amazon.awssdk.core.SdkSystemSetting;
3838
import software.amazon.awssdk.core.exception.SdkClientException;
3939
import software.amazon.awssdk.core.exception.SdkServiceException;
40+
import software.amazon.awssdk.imds.Ec2MetadataClientException;
4041
import software.amazon.awssdk.profiles.ProfileFile;
4142
import software.amazon.awssdk.profiles.ProfileFileSupplier;
4243
import software.amazon.awssdk.profiles.ProfileFileSystemSetting;
@@ -70,9 +71,24 @@ public final class InstanceProfileCredentialsProvider
7071
private static final String PROVIDER_NAME = "InstanceProfileCredentialsProvider";
7172
private static final String EC2_METADATA_TOKEN_HEADER = "x-aws-ec2-metadata-token";
7273
private static final String SECURITY_CREDENTIALS_RESOURCE = "/latest/meta-data/iam/security-credentials/";
74+
private static final String SECURITY_CREDENTIALS_EXTENDED_RESOURCE = "/latest/meta-data/iam/security-credentials-extended/";
7375
private static final String TOKEN_RESOURCE = "/latest/api/token";
76+
private static final String FAILED_TO_LOAD_CREDENTIALS_ERROR = "Failed to load credentials from IMDS.";
77+
78+
private enum ApiVersion {
79+
UNKNOWN,
80+
LEGACY,
81+
EXTENDED
82+
}
83+
7484
private static final String EC2_METADATA_TOKEN_TTL_HEADER = "x-aws-ec2-metadata-token-ttl-seconds";
7585
private static final String DEFAULT_TOKEN_TTL = "21600";
86+
private static final int MAX_PROFILE_RETRIES = 1;
87+
88+
// These fields are accessed from methods called by CachedSupplier which provides thread safety through its ReentrantLock
89+
private ApiVersion apiVersion = ApiVersion.UNKNOWN;
90+
private String resolvedProfile = null;
91+
private int profileRetryCount = 0;
7692

7793
private final Clock clock;
7894
private final String endpoint;
@@ -160,12 +176,33 @@ private RefreshResult<AwsCredentials> refreshCredentials() {
160176
Instant expiration = credentials.getExpiration().orElse(null);
161177
log.debug(() -> "Loaded credentials from IMDS with expiration time of " + expiration);
162178

179+
// Reset profile retry count after successful credential fetch
180+
profileRetryCount = 0;
181+
163182
return RefreshResult.builder(credentials.getAwsCredentials())
164183
.staleTime(staleTime(expiration))
165184
.prefetchTime(prefetchTime(expiration))
166185
.build();
186+
} catch (Ec2MetadataClientException e) {
187+
if (e.statusCode() == 404) {
188+
log.debug(() -> "Resolved profile is no longer available. Resetting it and trying again.");
189+
resolvedProfile = null;
190+
191+
if (apiVersion == ApiVersion.UNKNOWN) {
192+
apiVersion = ApiVersion.LEGACY;
193+
return refreshCredentials();
194+
}
195+
196+
profileRetryCount++;
197+
if (profileRetryCount <= MAX_PROFILE_RETRIES) {
198+
log.debug(() -> "Profile name not found, retrying fetching the profile name again.");
199+
return refreshCredentials();
200+
}
201+
throw SdkClientException.create(FAILED_TO_LOAD_CREDENTIALS_ERROR, e);
202+
}
203+
throw SdkClientException.create(FAILED_TO_LOAD_CREDENTIALS_ERROR, e);
167204
} catch (RuntimeException e) {
168-
throw SdkClientException.create("Failed to load credentials from IMDS.", e);
205+
throw SdkClientException.create(FAILED_TO_LOAD_CREDENTIALS_ERROR, e);
169206
}
170207
}
171208

@@ -207,14 +244,20 @@ public String toString() {
207244
return ToString.create(PROVIDER_NAME);
208245
}
209246

247+
private String getSecurityCredentialsResource() {
248+
return apiVersion == ApiVersion.LEGACY ?
249+
SECURITY_CREDENTIALS_RESOURCE :
250+
SECURITY_CREDENTIALS_EXTENDED_RESOURCE;
251+
}
252+
210253
private ResourcesEndpointProvider createEndpointProvider() {
211254
String imdsHostname = getImdsEndpoint();
212255
String token = getToken(imdsHostname);
213256
String[] securityCredentials = getSecurityCredentials(imdsHostname, token);
214-
257+
String urlBase = getSecurityCredentialsResource();
258+
215259
return StaticResourcesEndpointProvider.builder()
216-
.endpoint(URI.create(imdsHostname + SECURITY_CREDENTIALS_RESOURCE
217-
+ securityCredentials[0]))
260+
.endpoint(URI.create(imdsHostname + urlBase + securityCredentials[0]))
218261
.headers(getTokenHeaders(token))
219262
.connectionTimeout(Duration.ofMillis(
220263
this.configProvider.serviceTimeout()))
@@ -285,21 +328,41 @@ private boolean isInsecureFallbackDisabled() {
285328
}
286329

287330
private String[] getSecurityCredentials(String imdsHostname, String metadataToken) {
331+
if (resolvedProfile != null) {
332+
return new String[]{resolvedProfile};
333+
}
334+
335+
String urlBase = getSecurityCredentialsResource();
288336
ResourcesEndpointProvider securityCredentialsEndpoint =
289337
StaticResourcesEndpointProvider.builder()
290-
.endpoint(URI.create(imdsHostname + SECURITY_CREDENTIALS_RESOURCE))
338+
.endpoint(URI.create(imdsHostname + urlBase))
291339
.headers(getTokenHeaders(metadataToken))
292-
.connectionTimeout(Duration.ofMillis(this.configProvider.serviceTimeout()))
340+
.connectionTimeout(Duration.ofMillis(this.configProvider.serviceTimeout()))
293341
.build();
294342

295-
String securityCredentialsList =
296-
invokeSafely(() -> HttpResourcesUtils.instance().readResource(securityCredentialsEndpoint));
297-
String[] securityCredentials = securityCredentialsList.trim().split("\n");
343+
try {
344+
String securityCredentialsList =
345+
invokeSafely(() -> HttpResourcesUtils.instance().readResource(securityCredentialsEndpoint));
346+
String[] securityCredentials = securityCredentialsList.trim().split("\n");
298347

299-
if (securityCredentials.length == 0) {
300-
throw SdkClientException.builder().message("Unable to load credentials path").build();
348+
if (securityCredentials.length == 0) {
349+
throw SdkClientException.builder().message("Unable to load credentials path").build();
350+
}
351+
352+
if (apiVersion == ApiVersion.UNKNOWN) {
353+
apiVersion = ApiVersion.EXTENDED;
354+
}
355+
resolvedProfile = securityCredentials[0];
356+
return securityCredentials;
357+
358+
} catch (Ec2MetadataClientException e) {
359+
if (e.statusCode() == 404 && apiVersion == ApiVersion.UNKNOWN) {
360+
apiVersion = ApiVersion.LEGACY;
361+
log.debug(() -> "Instance does not support IMDS extended API. Falling back to legacy API.");
362+
return getSecurityCredentials(imdsHostname, metadataToken);
363+
}
364+
throw SdkClientException.create(FAILED_TO_LOAD_CREDENTIALS_ERROR, e);
301365
}
302-
return securityCredentials;
303366
}
304367

305368
private Map<String, String> getTokenHeaders(String metadataToken) {

core/auth/src/main/java/software/amazon/awssdk/auth/credentials/internal/HttpCredentialsLoader.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ public LoadedCredentials loadCredentials(ResourcesEndpointProvider endpoint) {
6464
JsonNode secretKey = node.get("SecretAccessKey");
6565
JsonNode token = node.get("Token");
6666
JsonNode expiration = node.get("Expiration");
67+
JsonNode accountId = node.get("AccountId");
6768

6869
Validate.notNull(accessKey, "Failed to load access key from metadata service.");
6970
Validate.notNull(secretKey, "Failed to load secret key from metadata service.");
@@ -72,6 +73,7 @@ public LoadedCredentials loadCredentials(ResourcesEndpointProvider endpoint) {
7273
secretKey.text(),
7374
token != null ? token.text() : null,
7475
expiration != null ? expiration.text() : null,
76+
accountId != null ? accountId.text() : null,
7577
providerName);
7678
} catch (SdkClientException e) {
7779
throw e;
@@ -89,12 +91,15 @@ public static final class LoadedCredentials {
8991
private final String token;
9092
private final Instant expiration;
9193
private final String providerName;
94+
private final String accountId;
9295

93-
private LoadedCredentials(String accessKeyId, String secretKey, String token, String expiration, String providerName) {
96+
private LoadedCredentials(String accessKeyId, String secretKey, String token,
97+
String expiration, String accountId, String providerName) {
9498
this.accessKeyId = Validate.paramNotBlank(accessKeyId, "accessKeyId");
9599
this.secretKey = Validate.paramNotBlank(secretKey, "secretKey");
96100
this.token = token;
97101
this.expiration = expiration == null ? null : parseExpiration(expiration);
102+
this.accountId = accountId;
98103
this.providerName = providerName;
99104
}
100105

@@ -105,11 +110,13 @@ public AwsCredentials getAwsCredentials() {
105110
.secretAccessKey(secretKey)
106111
.sessionToken(token)
107112
.providerName(providerName)
113+
.accountId(accountId)
108114
.build() :
109115
AwsBasicCredentials.builder()
110116
.accessKeyId(accessKeyId)
111117
.secretAccessKey(secretKey)
112118
.providerName(providerName)
119+
.accountId(accountId)
113120
.build();
114121
}
115122

core/auth/src/test/java/software/amazon/awssdk/auth/credentials/EC2MetadataServiceMock.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ public class EC2MetadataServiceMock {
4242
"Content-Type: text/html\r\n" +
4343
"Content-Length: ";
4444
private static final String OUTPUT_END_OF_HEADERS = "\r\n\r\n";
45+
private static final String EXTENDED_PATH = "/latest/meta-data/iam/security-credentials-extended/";
4546
private final String securityCredentialsResource;
4647
private EC2MockMetadataServiceListenerThread hosmMockServerThread;
4748

@@ -140,6 +141,15 @@ public void run() {
140141
String[] strings = requestLine.split(" ");
141142
String resourcePath = strings[1];
142143

144+
// Return 404 for extended path when in legacy mode
145+
if (!credentialsResource.equals(EXTENDED_PATH) &&
146+
(resourcePath.equals(EXTENDED_PATH) || resourcePath.startsWith(EXTENDED_PATH))) {
147+
String notFound = "HTTP/1.1 404 Not Found\r\n" +
148+
"Content-Length: 0\r\n" +
149+
"\r\n";
150+
outputStream.write(notFound.getBytes());
151+
continue;
152+
}
143153

144154
String httpResponse = null;
145155

0 commit comments

Comments
 (0)