|
37 | 37 | import software.amazon.awssdk.core.SdkSystemSetting; |
38 | 38 | import software.amazon.awssdk.core.exception.SdkClientException; |
39 | 39 | import software.amazon.awssdk.core.exception.SdkServiceException; |
| 40 | +import software.amazon.awssdk.imds.Ec2MetadataClientException; |
40 | 41 | import software.amazon.awssdk.profiles.ProfileFile; |
41 | 42 | import software.amazon.awssdk.profiles.ProfileFileSupplier; |
42 | 43 | import software.amazon.awssdk.profiles.ProfileFileSystemSetting; |
@@ -70,9 +71,24 @@ public final class InstanceProfileCredentialsProvider |
70 | 71 | private static final String PROVIDER_NAME = "InstanceProfileCredentialsProvider"; |
71 | 72 | private static final String EC2_METADATA_TOKEN_HEADER = "x-aws-ec2-metadata-token"; |
72 | 73 | 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/"; |
73 | 75 | 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 | + |
74 | 84 | private static final String EC2_METADATA_TOKEN_TTL_HEADER = "x-aws-ec2-metadata-token-ttl-seconds"; |
75 | 85 | 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; |
76 | 92 |
|
77 | 93 | private final Clock clock; |
78 | 94 | private final String endpoint; |
@@ -160,12 +176,33 @@ private RefreshResult<AwsCredentials> refreshCredentials() { |
160 | 176 | Instant expiration = credentials.getExpiration().orElse(null); |
161 | 177 | log.debug(() -> "Loaded credentials from IMDS with expiration time of " + expiration); |
162 | 178 |
|
| 179 | + // Reset profile retry count after successful credential fetch |
| 180 | + profileRetryCount = 0; |
| 181 | + |
163 | 182 | return RefreshResult.builder(credentials.getAwsCredentials()) |
164 | 183 | .staleTime(staleTime(expiration)) |
165 | 184 | .prefetchTime(prefetchTime(expiration)) |
166 | 185 | .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); |
167 | 204 | } catch (RuntimeException e) { |
168 | | - throw SdkClientException.create("Failed to load credentials from IMDS.", e); |
| 205 | + throw SdkClientException.create(FAILED_TO_LOAD_CREDENTIALS_ERROR, e); |
169 | 206 | } |
170 | 207 | } |
171 | 208 |
|
@@ -207,14 +244,20 @@ public String toString() { |
207 | 244 | return ToString.create(PROVIDER_NAME); |
208 | 245 | } |
209 | 246 |
|
| 247 | + private String getSecurityCredentialsResource() { |
| 248 | + return apiVersion == ApiVersion.LEGACY ? |
| 249 | + SECURITY_CREDENTIALS_RESOURCE : |
| 250 | + SECURITY_CREDENTIALS_EXTENDED_RESOURCE; |
| 251 | + } |
| 252 | + |
210 | 253 | private ResourcesEndpointProvider createEndpointProvider() { |
211 | 254 | String imdsHostname = getImdsEndpoint(); |
212 | 255 | String token = getToken(imdsHostname); |
213 | 256 | String[] securityCredentials = getSecurityCredentials(imdsHostname, token); |
214 | | - |
| 257 | + String urlBase = getSecurityCredentialsResource(); |
| 258 | + |
215 | 259 | return StaticResourcesEndpointProvider.builder() |
216 | | - .endpoint(URI.create(imdsHostname + SECURITY_CREDENTIALS_RESOURCE |
217 | | - + securityCredentials[0])) |
| 260 | + .endpoint(URI.create(imdsHostname + urlBase + securityCredentials[0])) |
218 | 261 | .headers(getTokenHeaders(token)) |
219 | 262 | .connectionTimeout(Duration.ofMillis( |
220 | 263 | this.configProvider.serviceTimeout())) |
@@ -285,21 +328,41 @@ private boolean isInsecureFallbackDisabled() { |
285 | 328 | } |
286 | 329 |
|
287 | 330 | private String[] getSecurityCredentials(String imdsHostname, String metadataToken) { |
| 331 | + if (resolvedProfile != null) { |
| 332 | + return new String[]{resolvedProfile}; |
| 333 | + } |
| 334 | + |
| 335 | + String urlBase = getSecurityCredentialsResource(); |
288 | 336 | ResourcesEndpointProvider securityCredentialsEndpoint = |
289 | 337 | StaticResourcesEndpointProvider.builder() |
290 | | - .endpoint(URI.create(imdsHostname + SECURITY_CREDENTIALS_RESOURCE)) |
| 338 | + .endpoint(URI.create(imdsHostname + urlBase)) |
291 | 339 | .headers(getTokenHeaders(metadataToken)) |
292 | | - .connectionTimeout(Duration.ofMillis(this.configProvider.serviceTimeout())) |
| 340 | + .connectionTimeout(Duration.ofMillis(this.configProvider.serviceTimeout())) |
293 | 341 | .build(); |
294 | 342 |
|
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"); |
298 | 347 |
|
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); |
301 | 365 | } |
302 | | - return securityCredentials; |
303 | 366 | } |
304 | 367 |
|
305 | 368 | private Map<String, String> getTokenHeaders(String metadataToken) { |
|
0 commit comments