Skip to content

Commit f369c57

Browse files
authored
Refresh IMDS credentials more aggressively. (#3147)
This improves the behavior introduced in #2989 if the customer is using a mock IMDS endpoint with aggressively short session durations.
1 parent 3625b67 commit f369c57

File tree

3 files changed

+78
-14
lines changed

3 files changed

+78
-14
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"category": "AWS SDK for Java v2",
3+
"contributor": "",
4+
"type": "bugfix",
5+
"description": "Refresh IMDS credentials more aggressively."
6+
}

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

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@
1515

1616
package software.amazon.awssdk.auth.credentials;
1717

18-
import static java.time.temporal.ChronoUnit.HOURS;
1918
import static java.time.temporal.ChronoUnit.MINUTES;
19+
import static java.time.temporal.ChronoUnit.SECONDS;
20+
import static software.amazon.awssdk.utils.ComparableUtils.minimum;
2021

2122
import java.io.IOException;
2223
import java.net.URI;
@@ -156,24 +157,39 @@ private boolean isLocalCredentialLoadingDisabled() {
156157

157158
private Instant prefetchTime(Instant expiration) {
158159
Instant now = clock.instant();
159-
Instant oneHourFromNow = now.plus(1, HOURS);
160160

161-
// If expiration time is infinite or farther out than an hour, wait an hour before refreshing
162-
if (expiration == null || expiration.isAfter(oneHourFromNow)) {
163-
return oneHourFromNow;
161+
// If expiration time doesn't exist, refresh in 60 minutes
162+
if (expiration == null) {
163+
return now.plus(60, MINUTES);
164164
}
165165

166-
// If expiration time is within 15 minutes (or in the past), wait 15 minutes and warn the customer that they'll be using
167-
// expired credentials.
168-
Instant fifteenMinutesFromNow = now.plus(15, MINUTES);
169-
if (expiration.isBefore(fifteenMinutesFromNow)) {
170-
log.warn(() -> "IMDS credential expiration has been extended due to an IMDS availability outage. A refresh"
171-
+ " of these credentials will be attempted again in 15 minutes.");
172-
return fifteenMinutesFromNow;
166+
// If expiration time is 60+ minutes from now, refresh in 60 minutes or 60 minutes before expiration, whichever is
167+
// sooner. This is the average case, where customers are using IMDS and there is no IMDS outage.
168+
Instant sixtyMinutesBeforeExpiration = expiration.minus(60, MINUTES);
169+
if (now.isBefore(sixtyMinutesBeforeExpiration)) {
170+
return minimum(sixtyMinutesBeforeExpiration, now.plus(60, MINUTES));
173171
}
174172

175-
// Otherwise, just refresh 15 minutes before the credentials expire.
176-
return expiration.minus(15, MINUTES);
173+
// If expiration time is 5-60 minutes from now, refresh in 30 minutes or 5 minutes before expiration, whatever is
174+
// sooner. This is an unusual case: IMDS is either having an outage or the customer is using a mock IMDS with shorter
175+
// default session durations.
176+
Instant fiveMinutesBeforeExpiration = expiration.minus(5, MINUTES);
177+
if (now.isBefore(fiveMinutesBeforeExpiration)) {
178+
return minimum(fiveMinutesBeforeExpiration, now.plus(30, MINUTES));
179+
}
180+
181+
// If expiration time is 0.25-5 minutes from now, refresh 15 seconds before expiration. This is an unusual case: IMDS is
182+
// either having an outage or the customer is using a mock IMDS with very aggressive session durations.
183+
Instant fifteenSecondsBeforeExpiration = expiration.minus(15, SECONDS);
184+
if (now.isBefore(fifteenSecondsBeforeExpiration)) {
185+
return fifteenSecondsBeforeExpiration;
186+
}
187+
188+
// These credentials are expired. Try refreshing again in 5 minutes. We can't be more aggressive than that, because we
189+
// don't want to overload the IMDS endpoint.
190+
log.warn(() -> "IMDS credential expiration has been extended due to an IMDS availability outage. A refresh "
191+
+ "of these credentials will be attempted again in 5 minutes.");
192+
return now.plus(5, MINUTES);
177193
}
178194

179195
@Override

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

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
2525
import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
2626
import static java.time.temporal.ChronoUnit.HOURS;
27+
import static java.time.temporal.ChronoUnit.MINUTES;
28+
import static java.time.temporal.ChronoUnit.SECONDS;
2729
import static org.assertj.core.api.Assertions.assertThat;
2830
import static org.assertj.core.api.Assertions.assertThatThrownBy;
2931

@@ -335,6 +337,46 @@ public void resolveCredentials_usesCacheIfImdsFailsOnSecondCall() {
335337
assertThat(credentialsBefore).isEqualTo(credentialsAfter);
336338
}
337339

340+
@Test
341+
public void resolveCredentials_callsImdsIfCredentialsWithin5MinutesOfExpiration() {
342+
AdjustableClock clock = new AdjustableClock();
343+
AwsCredentialsProvider credentialsProvider = credentialsProviderWithClock(clock);
344+
Instant now = Instant.now();
345+
String successfulCredentialsResponse1 =
346+
"{"
347+
+ "\"AccessKeyId\":\"ACCESS_KEY_ID\","
348+
+ "\"SecretAccessKey\":\"SECRET_ACCESS_KEY\","
349+
+ "\"Expiration\":\"" + DateUtils.formatIso8601Date(now) + '"'
350+
+ "}";
351+
352+
String successfulCredentialsResponse2 =
353+
"{"
354+
+ "\"AccessKeyId\":\"ACCESS_KEY_ID\","
355+
+ "\"SecretAccessKey\":\"SECRET_ACCESS_KEY2\","
356+
+ "\"Expiration\":\"" + DateUtils.formatIso8601Date(now.plus(6, HOURS)) + '"'
357+
+ "}";
358+
359+
// Set the time to the past and call IMDS to prime the cache
360+
clock.time = now.minus(24, HOURS);
361+
stubCredentialsResponse(aResponse().withBody(successfulCredentialsResponse1));
362+
AwsCredentials credentials24HoursAgo = credentialsProvider.resolveCredentials();
363+
364+
// Set the time to 3 minutes before expiration, and fail to call IMDS
365+
clock.time = now.minus(3, MINUTES);
366+
stubCredentialsResponse(aResponse().withStatus(500));
367+
AwsCredentials credentials3MinutesAgo = credentialsProvider.resolveCredentials();
368+
369+
// Set the time to 10 seconds before expiration, and verify that we still call IMDS to try to get credentials in at the
370+
// last moment before expiration
371+
clock.time = now.minus(10, SECONDS);
372+
stubCredentialsResponse(aResponse().withBody(successfulCredentialsResponse2));
373+
AwsCredentials credentials10SecondsAgo = credentialsProvider.resolveCredentials();
374+
375+
assertThat(credentials24HoursAgo).isEqualTo(credentials3MinutesAgo);
376+
assertThat(credentials24HoursAgo.secretAccessKey()).isEqualTo("SECRET_ACCESS_KEY");
377+
assertThat(credentials10SecondsAgo.secretAccessKey()).isEqualTo("SECRET_ACCESS_KEY2");
378+
}
379+
338380
private AwsCredentialsProvider credentialsProviderWithClock(Clock clock) {
339381
InstanceProfileCredentialsProvider.BuilderImpl builder =
340382
(InstanceProfileCredentialsProvider.BuilderImpl) InstanceProfileCredentialsProvider.builder();

0 commit comments

Comments
 (0)