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