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.nio.charset.StandardCharsets.UTF_8;
19 
20 import java.io.IOException;
21 import java.net.InetAddress;
22 import java.net.URI;
23 import java.net.UnknownHostException;
24 import java.nio.file.Files;
25 import java.nio.file.Path;
26 import java.nio.file.Paths;
27 import java.time.Instant;
28 import java.time.temporal.ChronoUnit;
29 import java.util.Arrays;
30 import java.util.HashMap;
31 import java.util.List;
32 import java.util.Map;
33 import java.util.Objects;
34 import java.util.Optional;
35 import java.util.function.Predicate;
36 import software.amazon.awssdk.annotations.SdkPublicApi;
37 import software.amazon.awssdk.auth.credentials.internal.ContainerCredentialsRetryPolicy;
38 import software.amazon.awssdk.auth.credentials.internal.HttpCredentialsLoader;
39 import software.amazon.awssdk.auth.credentials.internal.HttpCredentialsLoader.LoadedCredentials;
40 import software.amazon.awssdk.core.SdkSystemSetting;
41 import software.amazon.awssdk.core.exception.SdkClientException;
42 import software.amazon.awssdk.core.util.SdkUserAgent;
43 import software.amazon.awssdk.regions.util.ResourcesEndpointProvider;
44 import software.amazon.awssdk.regions.util.ResourcesEndpointRetryPolicy;
45 import software.amazon.awssdk.utils.ComparableUtils;
46 import software.amazon.awssdk.utils.StringUtils;
47 import software.amazon.awssdk.utils.ToString;
48 import software.amazon.awssdk.utils.Validate;
49 import software.amazon.awssdk.utils.builder.CopyableBuilder;
50 import software.amazon.awssdk.utils.builder.ToCopyableBuilder;
51 import software.amazon.awssdk.utils.cache.CachedSupplier;
52 import software.amazon.awssdk.utils.cache.NonBlocking;
53 import software.amazon.awssdk.utils.cache.RefreshResult;
54 
55 /**
56  * {@link AwsCredentialsProvider} implementation that loads credentials from a local metadata service.
57  *
58  * Currently supported containers:
59  * <ul>
60  *     <li>Amazon Elastic Container Service (ECS)</li>
61  *     <li>AWS Greengrass</li>
62  * </ul>
63  *
64  * <p>The URI path is retrieved from the environment variable "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI" or
65  * "AWS_CONTAINER_CREDENTIALS_FULL_URI" in the container's environment. If the environment variable is not set, this credentials
66  * provider will throw an exception.</p>
67  *
68  * @see <a href="http://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html">Amazon Elastic Container
69  * Service (ECS)</a>
70  */
71 @SdkPublicApi
72 public final class ContainerCredentialsProvider
73     implements HttpCredentialsProvider,
74                ToCopyableBuilder<ContainerCredentialsProvider.Builder, ContainerCredentialsProvider> {
75     private static final Predicate<InetAddress> IS_LOOPBACK_ADDRESS = InetAddress::isLoopbackAddress;
76     private static final Predicate<InetAddress> ALLOWED_HOSTS_RULES = IS_LOOPBACK_ADDRESS;
77     private static final String HTTPS = "https";
78 
79     private static final String ECS_CONTAINER_HOST = "169.254.170.2";
80     private static final String EKS_CONTAINER_HOST_IPV6 = "[fd00:ec2::23]";
81     private static final String EKS_CONTAINER_HOST_IPV4 = "169.254.170.23";
82     private static final List<String> VALID_LOOP_BACK_IPV4 = Arrays.asList(ECS_CONTAINER_HOST, EKS_CONTAINER_HOST_IPV4);
83     private static final List<String> VALID_LOOP_BACK_IPV6 = Arrays.asList(EKS_CONTAINER_HOST_IPV6);
84 
85     private final String endpoint;
86     private final HttpCredentialsLoader httpCredentialsLoader;
87     private final CachedSupplier<AwsCredentials> credentialsCache;
88 
89     private final Boolean asyncCredentialUpdateEnabled;
90 
91     private final String asyncThreadName;
92 
93     /**
94      * @see #builder()
95      */
ContainerCredentialsProvider(BuilderImpl builder)96     private ContainerCredentialsProvider(BuilderImpl builder) {
97         this.endpoint = builder.endpoint;
98         this.asyncCredentialUpdateEnabled = builder.asyncCredentialUpdateEnabled;
99         this.asyncThreadName = builder.asyncThreadName;
100         this.httpCredentialsLoader = HttpCredentialsLoader.create();
101 
102         if (Boolean.TRUE.equals(builder.asyncCredentialUpdateEnabled)) {
103             Validate.paramNotBlank(builder.asyncThreadName, "asyncThreadName");
104             this.credentialsCache = CachedSupplier.builder(this::refreshCredentials)
105                                                   .cachedValueName(toString())
106                                                   .prefetchStrategy(new NonBlocking(builder.asyncThreadName))
107                                                   .build();
108         } else {
109             this.credentialsCache = CachedSupplier.builder(this::refreshCredentials)
110                                                   .cachedValueName(toString())
111                                                   .build();
112         }
113     }
114 
115     /**
116      * Create a builder for creating a {@link ContainerCredentialsProvider}.
117      */
builder()118     public static Builder builder() {
119         return new BuilderImpl();
120     }
121 
122     @Override
toString()123     public String toString() {
124         return ToString.create("ContainerCredentialsProvider");
125     }
126 
refreshCredentials()127     private RefreshResult<AwsCredentials> refreshCredentials() {
128         LoadedCredentials loadedCredentials =
129             httpCredentialsLoader.loadCredentials(new ContainerCredentialsEndpointProvider(endpoint));
130         Instant expiration = loadedCredentials.getExpiration().orElse(null);
131 
132         return RefreshResult.builder(loadedCredentials.getAwsCredentials())
133                             .staleTime(staleTime(expiration))
134                             .prefetchTime(prefetchTime(expiration))
135                             .build();
136     }
137 
staleTime(Instant expiration)138     private Instant staleTime(Instant expiration) {
139         if (expiration == null) {
140             return null;
141         }
142 
143         return expiration.minus(1, ChronoUnit.MINUTES);
144     }
145 
prefetchTime(Instant expiration)146     private Instant prefetchTime(Instant expiration) {
147         Instant oneHourFromNow = Instant.now().plus(1, ChronoUnit.HOURS);
148 
149         if (expiration == null) {
150             return oneHourFromNow;
151         }
152 
153         Instant fifteenMinutesBeforeExpiration = expiration.minus(15, ChronoUnit.MINUTES);
154 
155         return ComparableUtils.minimum(oneHourFromNow, fifteenMinutesBeforeExpiration);
156     }
157 
158     @Override
resolveCredentials()159     public AwsCredentials resolveCredentials() {
160         return credentialsCache.get();
161     }
162 
163     @Override
close()164     public void close() {
165         credentialsCache.close();
166     }
167 
168     @Override
toBuilder()169     public Builder toBuilder() {
170         return new BuilderImpl(this);
171     }
172 
173     static final class ContainerCredentialsEndpointProvider implements ResourcesEndpointProvider {
174         private final String endpoint;
175 
ContainerCredentialsEndpointProvider(String endpoint)176         ContainerCredentialsEndpointProvider(String endpoint) {
177             this.endpoint = endpoint;
178         }
179 
180         @Override
endpoint()181         public URI endpoint() throws IOException {
182             if (!SdkSystemSetting.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI.getStringValue().isPresent() &&
183                 !SdkSystemSetting.AWS_CONTAINER_CREDENTIALS_FULL_URI.getStringValue().isPresent()) {
184                 throw SdkClientException.builder()
185                         .message(String.format("Cannot fetch credentials from container - neither %s or %s " +
186                                  "environment variables are set.",
187                                  SdkSystemSetting.AWS_CONTAINER_CREDENTIALS_FULL_URI.environmentVariable(),
188                                  SdkSystemSetting.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI.environmentVariable()))
189                         .build();
190             }
191 
192             try {
193                 URI resolvedURI = SdkSystemSetting.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI
194                     .getStringValue()
195                     .map(this::createUri)
196                     .orElseGet(() -> URI.create(SdkSystemSetting.AWS_CONTAINER_CREDENTIALS_FULL_URI.getStringValueOrThrow()));
197                 validateURI(resolvedURI);
198                 return resolvedURI;
199             } catch (SdkClientException e) {
200                 throw e;
201             } catch (Exception e) {
202                 throw SdkClientException.builder()
203                                         .message("Unable to fetch credentials from container.")
204                                         .cause(e)
205                                         .build();
206             }
207         }
208 
209         @Override
retryPolicy()210         public ResourcesEndpointRetryPolicy retryPolicy() {
211             return new ContainerCredentialsRetryPolicy();
212         }
213 
214         @Override
headers()215         public Map<String, String> headers() {
216             Map<String, String> requestHeaders = new HashMap<>();
217             requestHeaders.put("User-Agent", SdkUserAgent.create().userAgent());
218             getTokenValue()
219                                                               .filter(StringUtils::isNotBlank)
220                                                               .ifPresent(t -> requestHeaders.put("Authorization", t));
221             return requestHeaders;
222         }
223 
getTokenValue()224         private Optional<String> getTokenValue() {
225             if (SdkSystemSetting
226                 .AWS_CONTAINER_AUTHORIZATION_TOKEN.getStringValue().isPresent()) {
227                 return SdkSystemSetting
228                     .AWS_CONTAINER_AUTHORIZATION_TOKEN
229                     .getStringValue();
230             }
231 
232 
233             return SdkSystemSetting.AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE.getStringValue()
234                                                                           .map(this::readToken);
235 
236         }
237 
readToken(String filePath)238         private String readToken(String filePath) {
239             Path path = Paths.get(filePath);
240             try {
241                 return new String(Files.readAllBytes(path), UTF_8);
242             } catch (IOException e) {
243                 throw SdkClientException.create(String.format("Failed to read %s.", path.toAbsolutePath()), e);
244             }
245         }
246 
createUri(String relativeUri)247         private URI createUri(String relativeUri) {
248             String host = endpoint != null ? endpoint : SdkSystemSetting.AWS_CONTAINER_SERVICE_ENDPOINT.getStringValueOrThrow();
249             return URI.create(host + relativeUri);
250         }
251 
validateURI(URI uri)252         private URI validateURI(URI uri) {
253             if (!isHttps(uri) && !isAllowedHost(uri.getHost())) {
254                 String envVarName = SdkSystemSetting.AWS_CONTAINER_CREDENTIALS_FULL_URI.environmentVariable();
255                 throw SdkClientException.builder()
256                                         .message(String.format("The full URI (%s) contained within environment variable " +
257                                                                "%s has an invalid host. Host should resolve to a loopback " +
258                                                                "address or have the full URI be HTTPS.",
259                                                                uri, envVarName))
260                                         .build();
261             }
262             return uri;
263         }
264 
isHttps(URI endpoint)265         private boolean isHttps(URI endpoint) {
266             return Objects.equals(HTTPS, endpoint.getScheme());
267         }
268 
269         /**
270          * Determines if the addresses for a given host are resolved to a loopback address.
271          * <p>
272          *     This is a best-effort in determining what address a host will be resolved to. DNS caching might be disabled,
273          *     or could expire between this check and when the API is invoked.
274          * </p>
275          * @param host The name or IP address of the host.
276          * @return A boolean specifying whether the host is allowed as an endpoint for credentials loading.
277          */
isAllowedHost(String host)278         private boolean isAllowedHost(String host) {
279             try {
280                 InetAddress[] addresses = InetAddress.getAllByName(host);
281                 return addresses.length > 0 && (
282                     Arrays.stream(addresses).allMatch(this::matchesAllowedHostRules)
283                        || isMetadataServiceEndpoint(host));
284             } catch (UnknownHostException e) {
285                 throw SdkClientException.builder()
286                                         .cause(e)
287                                         .message(String.format("host (%s) could not be resolved to an IP address.", host))
288                                         .build();
289             }
290         }
291 
matchesAllowedHostRules(InetAddress inetAddress)292         private boolean matchesAllowedHostRules(InetAddress inetAddress) {
293             return ALLOWED_HOSTS_RULES.test(inetAddress) ;
294         }
295 
isMetadataServiceEndpoint(String host)296         public boolean isMetadataServiceEndpoint(String host) {
297             String mode = SdkSystemSetting.AWS_EC2_METADATA_SERVICE_ENDPOINT_MODE.getStringValueOrThrow();
298             if ("IPV6".equalsIgnoreCase(mode)) {
299                 return VALID_LOOP_BACK_IPV6.contains(host);
300             }
301             return VALID_LOOP_BACK_IPV4.contains(host);
302         }
303     }
304 
305     /**
306      * A builder for creating a custom a {@link ContainerCredentialsProvider}.
307      */
308     public interface Builder extends HttpCredentialsProvider.Builder<ContainerCredentialsProvider, Builder>,
309                                      CopyableBuilder<Builder, ContainerCredentialsProvider> {
310     }
311 
312     private static final class BuilderImpl implements Builder {
313         private String endpoint;
314         private Boolean asyncCredentialUpdateEnabled;
315         private String asyncThreadName;
316 
BuilderImpl()317         private BuilderImpl() {
318             asyncThreadName("container-credentials-provider");
319         }
320 
BuilderImpl(ContainerCredentialsProvider credentialsProvider)321         private BuilderImpl(ContainerCredentialsProvider credentialsProvider) {
322             this.endpoint = credentialsProvider.endpoint;
323             this.asyncCredentialUpdateEnabled = credentialsProvider.asyncCredentialUpdateEnabled;
324             this.asyncThreadName = credentialsProvider.asyncThreadName;
325         }
326 
327         @Override
endpoint(String endpoint)328         public Builder endpoint(String endpoint) {
329             this.endpoint = endpoint;
330             return this;
331         }
332 
setEndpoint(String endpoint)333         public void setEndpoint(String endpoint) {
334             endpoint(endpoint);
335         }
336 
337         @Override
asyncCredentialUpdateEnabled(Boolean asyncCredentialUpdateEnabled)338         public Builder asyncCredentialUpdateEnabled(Boolean asyncCredentialUpdateEnabled) {
339             this.asyncCredentialUpdateEnabled = asyncCredentialUpdateEnabled;
340             return this;
341         }
342 
setAsyncCredentialUpdateEnabled(boolean asyncCredentialUpdateEnabled)343         public void setAsyncCredentialUpdateEnabled(boolean asyncCredentialUpdateEnabled) {
344             asyncCredentialUpdateEnabled(asyncCredentialUpdateEnabled);
345         }
346 
347         @Override
asyncThreadName(String asyncThreadName)348         public Builder asyncThreadName(String asyncThreadName) {
349             this.asyncThreadName = asyncThreadName;
350             return this;
351         }
352 
setAsyncThreadName(String asyncThreadName)353         public void setAsyncThreadName(String asyncThreadName) {
354             asyncThreadName(asyncThreadName);
355         }
356 
357         @Override
build()358         public ContainerCredentialsProvider build() {
359             return new ContainerCredentialsProvider(this);
360         }
361     }
362 }
363