• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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