1 /* 2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"). 5 * You may not use this file except in compliance with the License. 6 * A copy of the License is located at 7 * 8 * http://aws.amazon.com/apache2.0 9 * 10 * or in the "license" file accompanying this file. This file is distributed 11 * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 * express or implied. See the License for the specific language governing 13 * permissions and limitations under the License. 14 */ 15 16 package software.amazon.awssdk.auth.credentials; 17 18 import static java.time.temporal.ChronoUnit.MINUTES; 19 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; 22 23 import java.net.URI; 24 import java.time.Clock; 25 import java.time.Duration; 26 import java.time.Instant; 27 import java.util.Collections; 28 import java.util.Map; 29 import java.util.Optional; 30 import java.util.function.Supplier; 31 import software.amazon.awssdk.annotations.SdkPublicApi; 32 import software.amazon.awssdk.annotations.SdkTestInternalApi; 33 import software.amazon.awssdk.auth.credentials.internal.Ec2MetadataConfigProvider; 34 import software.amazon.awssdk.auth.credentials.internal.Ec2MetadataDisableV1Resolver; 35 import software.amazon.awssdk.auth.credentials.internal.HttpCredentialsLoader; 36 import software.amazon.awssdk.auth.credentials.internal.HttpCredentialsLoader.LoadedCredentials; 37 import software.amazon.awssdk.auth.credentials.internal.StaticResourcesEndpointProvider; 38 import software.amazon.awssdk.core.SdkSystemSetting; 39 import software.amazon.awssdk.core.exception.SdkClientException; 40 import software.amazon.awssdk.core.exception.SdkServiceException; 41 import software.amazon.awssdk.profiles.ProfileFile; 42 import software.amazon.awssdk.profiles.ProfileFileSupplier; 43 import software.amazon.awssdk.profiles.ProfileFileSystemSetting; 44 import software.amazon.awssdk.profiles.ProfileProperty; 45 import software.amazon.awssdk.regions.util.HttpResourcesUtils; 46 import software.amazon.awssdk.regions.util.ResourcesEndpointProvider; 47 import software.amazon.awssdk.utils.Logger; 48 import software.amazon.awssdk.utils.ToString; 49 import software.amazon.awssdk.utils.Validate; 50 import software.amazon.awssdk.utils.builder.CopyableBuilder; 51 import software.amazon.awssdk.utils.builder.ToCopyableBuilder; 52 import software.amazon.awssdk.utils.cache.CachedSupplier; 53 import software.amazon.awssdk.utils.cache.NonBlocking; 54 import software.amazon.awssdk.utils.cache.RefreshResult; 55 56 /** 57 * Credentials provider implementation that loads credentials from the Amazon EC2 Instance Metadata Service. 58 * <p> 59 * If {@link SdkSystemSetting#AWS_EC2_METADATA_DISABLED} is set to true, it will not try to load 60 * credentials from EC2 metadata service and will return null. 61 * <p> 62 * If {@link SdkSystemSetting#AWS_EC2_METADATA_V1_DISABLED} or {@link ProfileProperty#EC2_METADATA_V1_DISABLED} 63 * is set to true, credentials will only be loaded from EC2 metadata service if a token is successfully retrieved - 64 * fallback to load credentials without a token will be disabled. 65 */ 66 @SdkPublicApi 67 public final class InstanceProfileCredentialsProvider 68 implements HttpCredentialsProvider, 69 ToCopyableBuilder<InstanceProfileCredentialsProvider.Builder, InstanceProfileCredentialsProvider> { 70 private static final Logger log = Logger.loggerFor(InstanceProfileCredentialsProvider.class); 71 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 TOKEN_RESOURCE = "/latest/api/token"; 75 private static final String EC2_METADATA_TOKEN_TTL_HEADER = "x-aws-ec2-metadata-token-ttl-seconds"; 76 private static final String DEFAULT_TOKEN_TTL = "21600"; 77 78 private final Clock clock; 79 private final String endpoint; 80 private final Ec2MetadataConfigProvider configProvider; 81 private final Ec2MetadataDisableV1Resolver ec2MetadataDisableV1Resolver; 82 private final HttpCredentialsLoader httpCredentialsLoader; 83 private final CachedSupplier<AwsCredentials> credentialsCache; 84 85 private final Boolean asyncCredentialUpdateEnabled; 86 87 private final String asyncThreadName; 88 89 private final Supplier<ProfileFile> profileFile; 90 91 private final String profileName; 92 93 /** 94 * @see #builder() 95 */ InstanceProfileCredentialsProvider(BuilderImpl builder)96 private InstanceProfileCredentialsProvider(BuilderImpl builder) { 97 this.clock = builder.clock; 98 this.endpoint = builder.endpoint; 99 this.asyncCredentialUpdateEnabled = builder.asyncCredentialUpdateEnabled; 100 this.asyncThreadName = builder.asyncThreadName; 101 this.profileFile = Optional.ofNullable(builder.profileFile) 102 .orElseGet(() -> ProfileFileSupplier.fixedProfileFile(ProfileFile.defaultProfileFile())); 103 this.profileName = Optional.ofNullable(builder.profileName) 104 .orElseGet(ProfileFileSystemSetting.AWS_PROFILE::getStringValueOrThrow); 105 106 this.httpCredentialsLoader = HttpCredentialsLoader.create(); 107 this.configProvider = 108 Ec2MetadataConfigProvider.builder() 109 .profileFile(profileFile) 110 .profileName(profileName) 111 .build(); 112 this.ec2MetadataDisableV1Resolver = Ec2MetadataDisableV1Resolver.create(profileFile, profileName); 113 114 if (Boolean.TRUE.equals(builder.asyncCredentialUpdateEnabled)) { 115 Validate.paramNotBlank(builder.asyncThreadName, "asyncThreadName"); 116 this.credentialsCache = CachedSupplier.builder(this::refreshCredentials) 117 .cachedValueName(toString()) 118 .prefetchStrategy(new NonBlocking(builder.asyncThreadName)) 119 .staleValueBehavior(ALLOW) 120 .clock(clock) 121 .build(); 122 } else { 123 this.credentialsCache = CachedSupplier.builder(this::refreshCredentials) 124 .cachedValueName(toString()) 125 .staleValueBehavior(ALLOW) 126 .clock(clock) 127 .build(); 128 } 129 } 130 131 /** 132 * Create a builder for creating a {@link InstanceProfileCredentialsProvider}. 133 */ builder()134 public static Builder builder() { 135 return new BuilderImpl(); 136 } 137 138 /** 139 * Create a {@link InstanceProfileCredentialsProvider} with default values. 140 * 141 * @return a {@link InstanceProfileCredentialsProvider} 142 */ create()143 public static InstanceProfileCredentialsProvider create() { 144 return builder().build(); 145 } 146 147 @Override resolveCredentials()148 public AwsCredentials resolveCredentials() { 149 return credentialsCache.get(); 150 } 151 refreshCredentials()152 private RefreshResult<AwsCredentials> refreshCredentials() { 153 if (isLocalCredentialLoadingDisabled()) { 154 throw SdkClientException.create("IMDS credentials have been disabled by environment variable or system property."); 155 } 156 157 try { 158 LoadedCredentials credentials = httpCredentialsLoader.loadCredentials(createEndpointProvider()); 159 Instant expiration = credentials.getExpiration().orElse(null); 160 log.debug(() -> "Loaded credentials from IMDS with expiration time of " + expiration); 161 162 return RefreshResult.builder(credentials.getAwsCredentials()) 163 .staleTime(staleTime(expiration)) 164 .prefetchTime(prefetchTime(expiration)) 165 .build(); 166 } catch (RuntimeException e) { 167 throw SdkClientException.create("Failed to load credentials from IMDS.", e); 168 } 169 } 170 isLocalCredentialLoadingDisabled()171 private boolean isLocalCredentialLoadingDisabled() { 172 return SdkSystemSetting.AWS_EC2_METADATA_DISABLED.getBooleanValueOrThrow(); 173 } 174 staleTime(Instant expiration)175 private Instant staleTime(Instant expiration) { 176 if (expiration == null) { 177 return null; 178 } 179 180 return expiration.minusSeconds(1); 181 } 182 prefetchTime(Instant expiration)183 private Instant prefetchTime(Instant expiration) { 184 Instant now = clock.instant(); 185 186 if (expiration == null) { 187 return now.plus(60, MINUTES); 188 } 189 190 Duration timeUntilExpiration = Duration.between(now, expiration); 191 if (timeUntilExpiration.isNegative()) { 192 // IMDS gave us a time in the past. We're already stale. Don't prefetch. 193 return null; 194 } 195 196 return now.plus(maximum(timeUntilExpiration.dividedBy(2), Duration.ofMinutes(5))); 197 } 198 199 @Override close()200 public void close() { 201 credentialsCache.close(); 202 } 203 204 @Override toString()205 public String toString() { 206 return ToString.create("InstanceProfileCredentialsProvider"); 207 } 208 createEndpointProvider()209 private ResourcesEndpointProvider createEndpointProvider() { 210 String imdsHostname = getImdsEndpoint(); 211 String token = getToken(imdsHostname); 212 String[] securityCredentials = getSecurityCredentials(imdsHostname, token); 213 214 return new StaticResourcesEndpointProvider(URI.create(imdsHostname + SECURITY_CREDENTIALS_RESOURCE + 215 securityCredentials[0]), 216 getTokenHeaders(token)); 217 } 218 getImdsEndpoint()219 private String getImdsEndpoint() { 220 if (endpoint != null) { 221 return endpoint; 222 } 223 224 return configProvider.getEndpoint(); 225 } 226 getToken(String imdsHostname)227 private String getToken(String imdsHostname) { 228 Map<String, String> tokenTtlHeaders = Collections.singletonMap(EC2_METADATA_TOKEN_TTL_HEADER, DEFAULT_TOKEN_TTL); 229 ResourcesEndpointProvider tokenEndpoint = new StaticResourcesEndpointProvider(getTokenEndpoint(imdsHostname), 230 tokenTtlHeaders); 231 232 try { 233 return HttpResourcesUtils.instance().readResource(tokenEndpoint, "PUT"); 234 } catch (SdkServiceException e) { 235 if (e.statusCode() == 400) { 236 237 throw SdkClientException.builder() 238 .message("Unable to fetch metadata token.") 239 .cause(e) 240 .build(); 241 } 242 return handleTokenErrorResponse(e); 243 } catch (Exception e) { 244 return handleTokenErrorResponse(e); 245 } 246 } 247 getTokenEndpoint(String imdsHostname)248 private URI getTokenEndpoint(String imdsHostname) { 249 String finalHost = imdsHostname; 250 if (finalHost.endsWith("/")) { 251 finalHost = finalHost.substring(0, finalHost.length() - 1); 252 } 253 return URI.create(finalHost + TOKEN_RESOURCE); 254 } 255 handleTokenErrorResponse(Exception e)256 private String handleTokenErrorResponse(Exception e) { 257 if (isInsecureFallbackDisabled()) { 258 String message = String.format("Failed to retrieve IMDS token, and fallback to IMDS v1 is disabled via the " 259 + "%s system property, %s environment variable, or %s configuration file profile" 260 + " setting.", 261 SdkSystemSetting.AWS_EC2_METADATA_V1_DISABLED.environmentVariable(), 262 SdkSystemSetting.AWS_EC2_METADATA_V1_DISABLED.property(), 263 ProfileProperty.EC2_METADATA_V1_DISABLED); 264 throw SdkClientException.builder() 265 .message(message) 266 .cause(e) 267 .build(); 268 } 269 log.debug(() -> "Ignoring non-fatal exception while attempting to load metadata token from instance profile.", e); 270 return null; 271 } 272 isInsecureFallbackDisabled()273 private boolean isInsecureFallbackDisabled() { 274 return ec2MetadataDisableV1Resolver.resolve(); 275 } 276 getSecurityCredentials(String imdsHostname, String metadataToken)277 private String[] getSecurityCredentials(String imdsHostname, String metadataToken) { 278 ResourcesEndpointProvider securityCredentialsEndpoint = 279 new StaticResourcesEndpointProvider(URI.create(imdsHostname + SECURITY_CREDENTIALS_RESOURCE), 280 getTokenHeaders(metadataToken)); 281 282 String securityCredentialsList = 283 invokeSafely(() -> HttpResourcesUtils.instance().readResource(securityCredentialsEndpoint)); 284 String[] securityCredentials = securityCredentialsList.trim().split("\n"); 285 286 if (securityCredentials.length == 0) { 287 throw SdkClientException.builder().message("Unable to load credentials path").build(); 288 } 289 return securityCredentials; 290 } 291 getTokenHeaders(String metadataToken)292 private Map<String, String> getTokenHeaders(String metadataToken) { 293 if (metadataToken == null) { 294 return Collections.emptyMap(); 295 } 296 297 return Collections.singletonMap(EC2_METADATA_TOKEN_HEADER, metadataToken); 298 } 299 300 @Override toBuilder()301 public Builder toBuilder() { 302 return new BuilderImpl(this); 303 } 304 305 /** 306 * A builder for creating a custom a {@link InstanceProfileCredentialsProvider}. 307 */ 308 public interface Builder extends HttpCredentialsProvider.Builder<InstanceProfileCredentialsProvider, Builder>, 309 CopyableBuilder<Builder, InstanceProfileCredentialsProvider> { 310 /** 311 * Configure the profile file used for loading IMDS-related configuration, like the endpoint mode (IPv4 vs IPv6). 312 * 313 * <p>By default, {@link ProfileFile#defaultProfileFile()} is used. 314 * 315 * @see #profileFile(Supplier) 316 */ profileFile(ProfileFile profileFile)317 Builder profileFile(ProfileFile profileFile); 318 319 /** 320 * Define the mechanism for loading profile files. 321 * 322 * @param profileFileSupplier Supplier interface for generating a ProfileFile instance. 323 * @see #profileFile(ProfileFile) 324 */ profileFile(Supplier<ProfileFile> profileFileSupplier)325 Builder profileFile(Supplier<ProfileFile> profileFileSupplier); 326 327 /** 328 * Configure the profile name used for loading IMDS-related configuration, like the endpoint mode (IPv4 vs IPv6). 329 * 330 * <p>By default, {@link ProfileFileSystemSetting#AWS_PROFILE} is used. 331 */ profileName(String profileName)332 Builder profileName(String profileName); 333 334 /** 335 * Build a {@link InstanceProfileCredentialsProvider} from the provided configuration. 336 */ 337 @Override build()338 InstanceProfileCredentialsProvider build(); 339 } 340 341 @SdkTestInternalApi 342 static final class BuilderImpl implements Builder { 343 private Clock clock = Clock.systemUTC(); 344 private String endpoint; 345 private Boolean asyncCredentialUpdateEnabled; 346 private String asyncThreadName; 347 private Supplier<ProfileFile> profileFile; 348 private String profileName; 349 BuilderImpl()350 private BuilderImpl() { 351 asyncThreadName("instance-profile-credentials-provider"); 352 } 353 BuilderImpl(InstanceProfileCredentialsProvider provider)354 private BuilderImpl(InstanceProfileCredentialsProvider provider) { 355 this.clock = provider.clock; 356 this.endpoint = provider.endpoint; 357 this.asyncCredentialUpdateEnabled = provider.asyncCredentialUpdateEnabled; 358 this.asyncThreadName = provider.asyncThreadName; 359 this.profileFile = provider.profileFile; 360 this.profileName = provider.profileName; 361 } 362 clock(Clock clock)363 Builder clock(Clock clock) { 364 this.clock = clock; 365 return this; 366 } 367 368 @Override endpoint(String endpoint)369 public Builder endpoint(String endpoint) { 370 this.endpoint = endpoint; 371 return this; 372 } 373 setEndpoint(String endpoint)374 public void setEndpoint(String endpoint) { 375 endpoint(endpoint); 376 } 377 378 @Override asyncCredentialUpdateEnabled(Boolean asyncCredentialUpdateEnabled)379 public Builder asyncCredentialUpdateEnabled(Boolean asyncCredentialUpdateEnabled) { 380 this.asyncCredentialUpdateEnabled = asyncCredentialUpdateEnabled; 381 return this; 382 } 383 setAsyncCredentialUpdateEnabled(boolean asyncCredentialUpdateEnabled)384 public void setAsyncCredentialUpdateEnabled(boolean asyncCredentialUpdateEnabled) { 385 asyncCredentialUpdateEnabled(asyncCredentialUpdateEnabled); 386 } 387 388 @Override asyncThreadName(String asyncThreadName)389 public Builder asyncThreadName(String asyncThreadName) { 390 this.asyncThreadName = asyncThreadName; 391 return this; 392 } 393 setAsyncThreadName(String asyncThreadName)394 public void setAsyncThreadName(String asyncThreadName) { 395 asyncThreadName(asyncThreadName); 396 } 397 398 @Override profileFile(ProfileFile profileFile)399 public Builder profileFile(ProfileFile profileFile) { 400 return profileFile(Optional.ofNullable(profileFile) 401 .map(ProfileFileSupplier::fixedProfileFile) 402 .orElse(null)); 403 } 404 setProfileFile(ProfileFile profileFile)405 public void setProfileFile(ProfileFile profileFile) { 406 profileFile(profileFile); 407 } 408 409 @Override profileFile(Supplier<ProfileFile> profileFileSupplier)410 public Builder profileFile(Supplier<ProfileFile> profileFileSupplier) { 411 this.profileFile = profileFileSupplier; 412 return this; 413 } 414 setProfileFile(Supplier<ProfileFile> profileFileSupplier)415 public void setProfileFile(Supplier<ProfileFile> profileFileSupplier) { 416 profileFile(profileFileSupplier); 417 } 418 419 @Override profileName(String profileName)420 public Builder profileName(String profileName) { 421 this.profileName = profileName; 422 return this; 423 } 424 setProfileName(String profileName)425 public void setProfileName(String profileName) { 426 profileName(profileName); 427 } 428 429 @Override build()430 public InstanceProfileCredentialsProvider build() { 431 return new InstanceProfileCredentialsProvider(this); 432 } 433 } 434 } 435