Skip to content

Commit 5151e40

Browse files
authored
Fix issue in IMDS credentials provider that causes expired credentials to be vended for a short period of time after the credentials provider is inactive for a long time. (#3314)
Before this change, if credentials are stale (because they weren't prefetched, likely due to inactivity) only one thread would block to refresh and other calling threads would be given expired credentials. This is good during an IMDS outage if the credential expiration has been extended service-side, but it's bad when the credentials are actually expired. After this change, if credentials are stale and we go to refresh them, we'll hold other calling threads until that refresh completes. This will cause increased latency during credential refreshes during an IMDS outage, but ensure that vending expired credentials is minimized outside of outage scenarios.
1 parent 4da2d30 commit 5151e40

File tree

7 files changed

+452
-95
lines changed

7 files changed

+452
-95
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"type": "bugfix",
3+
"category": "AWS SDK for Java v2",
4+
"contributor": "",
5+
"description": "Fix issue in IMDS credentials provider that causes expired credentials to be vended for a short period of time after the credentials provider is inactive for a long time."
6+
}

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

Lines changed: 33 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,15 @@
1717

1818
import static java.time.temporal.ChronoUnit.MINUTES;
1919
import static software.amazon.awssdk.utils.ComparableUtils.maximum;
20+
import static software.amazon.awssdk.utils.FunctionalUtils.invokeSafely;
21+
import static software.amazon.awssdk.utils.cache.CachedSupplier.StaleValueBehavior.ALLOW;
2022

21-
import java.io.IOException;
2223
import java.net.URI;
2324
import java.time.Clock;
2425
import java.time.Duration;
2526
import java.time.Instant;
2627
import java.util.Collections;
2728
import java.util.Map;
28-
import java.util.function.Supplier;
2929
import software.amazon.awssdk.annotations.SdkPublicApi;
3030
import software.amazon.awssdk.annotations.SdkTestInternalApi;
3131
import software.amazon.awssdk.auth.credentials.internal.Ec2MetadataConfigProvider;
@@ -81,8 +81,6 @@ public final class InstanceProfileCredentialsProvider
8181

8282
private final String profileName;
8383

84-
private volatile LoadedCredentials cachedCredentials;
85-
8684
/**
8785
* @see #builder()
8886
*/
@@ -105,9 +103,14 @@ private InstanceProfileCredentialsProvider(BuilderImpl builder) {
105103
Validate.paramNotBlank(builder.asyncThreadName, "asyncThreadName");
106104
this.credentialsCache = CachedSupplier.builder(this::refreshCredentials)
107105
.prefetchStrategy(new NonBlocking(builder.asyncThreadName))
106+
.staleValueBehavior(ALLOW)
107+
.clock(clock)
108108
.build();
109109
} else {
110-
this.credentialsCache = CachedSupplier.builder(this::refreshCredentials).build();
110+
this.credentialsCache = CachedSupplier.builder(this::refreshCredentials)
111+
.staleValueBehavior(ALLOW)
112+
.clock(clock)
113+
.build();
111114
}
112115
}
113116

@@ -138,45 +141,32 @@ private RefreshResult<AwsCredentials> refreshCredentials() {
138141
throw SdkClientException.create("IMDS credentials have been disabled by environment variable or system property.");
139142
}
140143

141-
LoadedCredentials credentials;
142144
try {
143-
credentials = httpCredentialsLoader.loadCredentials(createEndpointProvider());
144-
logExpirationTime(credentials);
145-
this.cachedCredentials = credentials;
146-
} catch (RuntimeException | IOException e) {
147-
credentials = this.cachedCredentials;
148-
149-
if (credentials != null) {
150-
credentials.getExpiration().ifPresent(expiration -> {
151-
// Choose whether to report this failure at the debug or warn level based on how much time is left on the
152-
// credentials before expiration.
153-
Supplier<String> errorMessage = () -> "Failure encountered when attempting to refresh credentials from IMDS.";
154-
Instant fifteenMinutesFromNow = clock.instant().plus(15, MINUTES);
155-
if (expiration.isBefore(fifteenMinutesFromNow)) {
156-
log.warn(errorMessage, e);
157-
} else {
158-
log.debug(errorMessage, e);
159-
}
160-
});
161-
} else {
162-
throw SdkClientException.create("Failed to load credentials from IMDS.", e);
163-
}
145+
LoadedCredentials credentials = httpCredentialsLoader.loadCredentials(createEndpointProvider());
146+
Instant expiration = credentials.getExpiration().orElse(null);
147+
log.debug(() -> "Loaded credentials from IMDS with expiration time of " + expiration);
148+
149+
return RefreshResult.builder(credentials.getAwsCredentials())
150+
.staleTime(staleTime(expiration))
151+
.prefetchTime(prefetchTime(expiration))
152+
.build();
153+
} catch (RuntimeException e) {
154+
throw SdkClientException.create("Failed to load credentials from IMDS.", e);
164155
}
165-
166-
return RefreshResult.builder(credentials.getAwsCredentials())
167-
.staleTime(Instant.MAX) // Allow use of expired credentials - they may still work
168-
.prefetchTime(prefetchTime(credentials.getExpiration().orElse(null)))
169-
.build();
170-
}
171-
172-
private void logExpirationTime(LoadedCredentials credentials) {
173-
log.debug(() -> "Loaded credentials from IMDS with expiration time of " + credentials.getExpiration());
174156
}
175157

176158
private boolean isLocalCredentialLoadingDisabled() {
177159
return SdkSystemSetting.AWS_EC2_METADATA_DISABLED.getBooleanValueOrThrow();
178160
}
179161

162+
private Instant staleTime(Instant expiration) {
163+
if (expiration == null) {
164+
return null;
165+
}
166+
167+
return expiration.minusSeconds(1);
168+
}
169+
180170
private Instant prefetchTime(Instant expiration) {
181171
Instant now = clock.instant();
182172

@@ -186,12 +176,11 @@ private Instant prefetchTime(Instant expiration) {
186176

187177
Duration timeUntilExpiration = Duration.between(now, expiration);
188178
if (timeUntilExpiration.isNegative()) {
189-
log.warn(() -> "IMDS credential expiration has been extended due to an IMDS availability outage. A refresh "
190-
+ "of these credentials will be attempted again in ~5 minutes.");
191-
return now.plus(5, MINUTES);
179+
// IMDS gave us a time in the past. We're already stale. Don't prefetch.
180+
return null;
192181
}
193182

194-
return now.plus(maximum(timeUntilExpiration.abs().dividedBy(2), Duration.ofMinutes(5)));
183+
return now.plus(maximum(timeUntilExpiration.dividedBy(2), Duration.ofMinutes(5)));
195184
}
196185

197186
@Override
@@ -204,7 +193,7 @@ public String toString() {
204193
return ToString.create("InstanceProfileCredentialsProvider");
205194
}
206195

207-
private ResourcesEndpointProvider createEndpointProvider() throws IOException {
196+
private ResourcesEndpointProvider createEndpointProvider() {
208197
String imdsHostname = getImdsEndpoint();
209198
String token = getToken(imdsHostname);
210199
String[] securityCredentials = getSecurityCredentials(imdsHostname, token);
@@ -253,12 +242,13 @@ private URI getTokenEndpoint(String imdsHostname) {
253242
return URI.create(finalHost + TOKEN_RESOURCE);
254243
}
255244

256-
private String[] getSecurityCredentials(String imdsHostname, String metadataToken) throws IOException {
245+
private String[] getSecurityCredentials(String imdsHostname, String metadataToken) {
257246
ResourcesEndpointProvider securityCredentialsEndpoint =
258247
new StaticResourcesEndpointProvider(URI.create(imdsHostname + SECURITY_CREDENTIALS_RESOURCE),
259248
getTokenHeaders(metadataToken));
260249

261-
String securityCredentialsList = HttpResourcesUtils.instance().readResource(securityCredentialsEndpoint);
250+
String securityCredentialsList =
251+
invokeSafely(() -> HttpResourcesUtils.instance().readResource(securityCredentialsEndpoint));
262252
String[] securityCredentials = securityCredentialsList.trim().split("\n");
263253

264254
if (securityCredentials.length == 0) {

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -404,12 +404,12 @@ public void imdsCallFrequencyIsLimited() {
404404
stubCredentialsResponse(aResponse().withBody(successfulCredentialsResponse1));
405405
AwsCredentials credentials5MinutesAgo = credentialsProvider.resolveCredentials();
406406

407-
// Set the time to 1 second before expiration, and verify that do not call IMDS because it hasn't been 5 minutes yet
408-
clock.time = now.minus(1, SECONDS);
407+
// Set the time to 2 seconds before expiration, and verify that do not call IMDS because it hasn't been 5 minutes yet
408+
clock.time = now.minus(2, SECONDS);
409409
stubCredentialsResponse(aResponse().withBody(successfulCredentialsResponse2));
410-
AwsCredentials credentials1SecondsAgo = credentialsProvider.resolveCredentials();
410+
AwsCredentials credentials2SecondsAgo = credentialsProvider.resolveCredentials();
411411

412-
assertThat(credentials5MinutesAgo).isEqualTo(credentials1SecondsAgo);
412+
assertThat(credentials2SecondsAgo).isEqualTo(credentials5MinutesAgo);
413413
assertThat(credentials5MinutesAgo.secretAccessKey()).isEqualTo("SECRET_ACCESS_KEY");
414414
}
415415
}

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
import java.io.UncheckedIOException;
2525
import java.time.Duration;
2626
import java.time.Instant;
27-
2827
import org.assertj.core.api.Assertions;
2928
import org.junit.AfterClass;
3029
import org.junit.Assert;

0 commit comments

Comments
 (0)