elastic/elasticsearch · error · IllegalStateException
STS returned credentials that are already expired at [${expi
Error message
STS returned credentials that are already expired at [${expiry}] (now [${now}]) What it means
toCached computes now = clock.instant() and checks expiry.isAfter(now). If the returned credentials already expired at or before now, it throws IllegalStateException with both timestamps. This protects against accepting credentials STS should not have issued.
Source
Thrown at libs/workload-identity-aws/src/main/java/org/elasticsearch/workload/identity/aws/AsyncWebIdentityCredentialsProvider.java:206
.roleArn(roleArn)
.roleSessionName(roleSessionName)
.webIdentityToken(token)
.build();
return stsAsyncClient.assumeRoleWithWebIdentity(request);
}
private Cached toCached(AssumeRoleWithWebIdentityResponse response) {
Credentials credentials = response.credentials();
if (credentials == null) {
throw new IllegalStateException("STS AssumeRoleWithWebIdentity response did not include credentials");
}
Instant expiry = credentials.expiration();
if (expiry == null) {
throw new IllegalStateException("STS AssumeRoleWithWebIdentity response did not include a credential expiry");
}
Instant now = clock.instant();
if (expiry.isAfter(now) == false) {
throw new IllegalStateException("STS returned credentials that are already expired at [" + expiry + "] (now [" + now + "])");
}
AwsSessionCredentials sessionCredentials = AwsSessionCredentials.builder()
.accessKeyId(credentials.accessKeyId())
.secretAccessKey(credentials.secretAccessKey())
.sessionToken(credentials.sessionToken())
.expirationTime(expiry)
.build();
return new Cached(sessionCredentials, expiry.minus(prefetchTime), expiry.minus(staleTime));
}
private static Throwable unwrap(Throwable t) {
return t instanceof CompletionException && t.getCause() != null ? t.getCause() : t;
}
/**
* No-op: the {@link StsAsyncClient} is owned and closed by the caller.
*/
@OverrideView on GitHub (pinned to db6a809a66)
Solutions
- Verify NTP / clock sync on the host running the provider
- If in tests, ensure the stubbed expiry is in the future relative to the injected clock
- Increase the requested role session duration if STS is returning very short-lived credentials
- If clock skew is the cause, correct the system clock and restart the provider
Example fix
// before (test)
Instant now = Instant.now();
Credentials c = Credentials.builder().accessKeyId("k").secretAccessKey("s").expiration(now.minusSeconds(1)).build();
// after
Credentials c = Credentials.builder().accessKeyId("k").secretAccessKey("s").expiration(now.plusSeconds(900)).build(); Defensive patterns
Strategy: validation
Validate before calling
Instant now = clock.instant();
if (!expiry.isAfter(now)) {
throw new IllegalStateException("credentials expired: " + expiry);
} Type guard
static boolean isFutureExpiry(Instant expiry, Clock clock) {
return expiry != null && expiry.isAfter(clock.instant());
} Try / catch
try { toCached(resp); }
catch (IllegalStateException e) { /* check clock skew, then retry */ } Prevention
- Keep NTP synchronised on hosts running the provider
- In tests, stub expiry relative to the injected clock
- Treat returned-expired as a clock-skew signal first
When it happens
Trigger: STS returns credentials whose expiration Instant is <= the provider's clock now. The check is strict: expiry must be strictly after now.
Common situations: Clock skew between the ES node and STS (node clock ahead); STS returned short-lived creds that expired in transit; a misconfigured clock source (e.g. wrong NTP); tests with a fixed clock that is already past the stubbed expiry; refresh attempts racing after the cached value already lapsed.
Related errors
- STS AssumeRoleWithWebIdentity response did not include crede
- STS AssumeRoleWithWebIdentity response did not include a cre
- staleTime must be a positive duration but was [${staleTime}]
- prefetchTime must be a positive duration but was [${prefetch
- prefetchTime [${prefetchTime}] must be greater than or equal
AI-assisted analysis of elastic/elasticsearch@db6a809a66 (2026-08-12).
Data as JSON: /api/errors/f57ab8dee1f4e1a2.
Report an issue: GitHub.