|
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