From 9b2798ce3bd623c3cb898368c1d4827013ab2722 Mon Sep 17 00:00:00 2001 From: Artem Prigoda Date: Mon, 14 Mar 2022 13:27:52 +0100 Subject: [PATCH] [s3-repository] Lookup AWS Region for STS Client from STS endpoint (#84585) If we don't instruct to look up the region from the Endpoint URL, AWSSecurityTokenServiceClient tries to look it up implicitly in a custom way which requires reading the /.aws/config file for which we don't have a file permission. The same approach is used for the general AmazonS3ClientBuilder Resolves #83826, #52625 --- docs/changelog/84585.yaml | 6 + .../repositories/s3/S3Service.java | 39 ++++-- ...IdentityTokenCredentialsProviderTests.java | 115 ++++++++++++++++++ 3 files changed, 150 insertions(+), 10 deletions(-) create mode 100644 docs/changelog/84585.yaml create mode 100644 modules/repository-s3/src/test/java/org/elasticsearch/repositories/s3/CustomWebIdentityTokenCredentialsProviderTests.java diff --git a/docs/changelog/84585.yaml b/docs/changelog/84585.yaml new file mode 100644 index 0000000000000..05ae6ffd946ab --- /dev/null +++ b/docs/changelog/84585.yaml @@ -0,0 +1,6 @@ +pr: 84585 +summary: "[s3-repository] Lookup AWS Region for STS Client from STS endpoint" +area: Snapshot/Restore +type: bug +issues: + - 83826 diff --git a/modules/repository-s3/src/main/java/org/elasticsearch/repositories/s3/S3Service.java b/modules/repository-s3/src/main/java/org/elasticsearch/repositories/s3/S3Service.java index 2492791fd3dac..a971b260e1485 100644 --- a/modules/repository-s3/src/main/java/org/elasticsearch/repositories/s3/S3Service.java +++ b/modules/repository-s3/src/main/java/org/elasticsearch/repositories/s3/S3Service.java @@ -68,7 +68,11 @@ class S3Service implements Closeable { final CustomWebIdentityTokenCredentialsProvider webIdentityTokenCredentialsProvider; S3Service(Environment environment) { - webIdentityTokenCredentialsProvider = new CustomWebIdentityTokenCredentialsProvider(environment); + webIdentityTokenCredentialsProvider = new CustomWebIdentityTokenCredentialsProvider( + environment, + System::getenv, + System::getProperty + ); } /** @@ -282,13 +286,19 @@ public void refresh() { */ static class CustomWebIdentityTokenCredentialsProvider implements AWSCredentialsProvider { + private static final String STS_HOSTNAME = "https://sts.amazonaws.com"; + private STSAssumeRoleWithWebIdentitySessionCredentialsProvider credentialsProvider; private AWSSecurityTokenService stsClient; - CustomWebIdentityTokenCredentialsProvider(Environment environment) { + CustomWebIdentityTokenCredentialsProvider( + Environment environment, + SystemEnvironment systemEnvironment, + JvmEnvironment jvmEnvironment + ) { // Check whether the original environment variable exists. If it doesn't, // the system doesn't support AWS web identity tokens - if (System.getenv(AWS_WEB_IDENTITY_ENV_VAR) == null) { + if (systemEnvironment.getEnv(AWS_WEB_IDENTITY_ENV_VAR) == null) { return; } // Make sure that a readable symlink to the token file exists in the plugin config directory @@ -304,8 +314,8 @@ static class CustomWebIdentityTokenCredentialsProvider implements AWSCredentials if (Files.isReadable(webIdentityTokenFileSymlink) == false) { throw new IllegalStateException("Unable to read a Web Identity Token symlink in the config directory"); } - String roleArn = System.getenv(AWS_ROLE_ARN_ENV_VAR); - String roleSessionName = System.getenv(AWS_ROLE_SESSION_NAME_ENV_VAR); + String roleArn = systemEnvironment.getEnv(AWS_ROLE_ARN_ENV_VAR); + String roleSessionName = systemEnvironment.getEnv(AWS_ROLE_SESSION_NAME_ENV_VAR); if (roleArn == null || roleSessionName == null) { LOGGER.warn( "Unable to use a web identity token for authentication. The AWS_WEB_IDENTITY_TOKEN_FILE environment " @@ -315,11 +325,10 @@ static class CustomWebIdentityTokenCredentialsProvider implements AWSCredentials } AWSSecurityTokenServiceClientBuilder stsClientBuilder = AWSSecurityTokenServiceClient.builder(); - // Just for testing - String customStsEndpoint = System.getProperty("com.amazonaws.sdk.stsMetadataServiceEndpointOverride"); - if (customStsEndpoint != null) { - stsClientBuilder.withEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration(customStsEndpoint, null)); - } + // Custom system property used for specifying a mocked version of the STS for testing + String customStsEndpoint = jvmEnvironment.getProperty("com.amazonaws.sdk.stsMetadataServiceEndpointOverride", STS_HOSTNAME); + // Set the region explicitly via the endpoint URL, so the AWS SDK doesn't make any guesses internally. + stsClientBuilder.withEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration(customStsEndpoint, null)); stsClientBuilder.withCredentials(new AWSStaticCredentialsProvider(new AnonymousAWSCredentials())); stsClient = SocketAccess.doPrivileged(stsClientBuilder::build); try { @@ -357,4 +366,14 @@ public void shutdown() throws IOException { } } } + + @FunctionalInterface + interface SystemEnvironment { + String getEnv(String name); + } + + @FunctionalInterface + interface JvmEnvironment { + String getProperty(String key, String defaultValue); + } } diff --git a/modules/repository-s3/src/test/java/org/elasticsearch/repositories/s3/CustomWebIdentityTokenCredentialsProviderTests.java b/modules/repository-s3/src/test/java/org/elasticsearch/repositories/s3/CustomWebIdentityTokenCredentialsProviderTests.java new file mode 100644 index 0000000000000..ab289eda5f47a --- /dev/null +++ b/modules/repository-s3/src/test/java/org/elasticsearch/repositories/s3/CustomWebIdentityTokenCredentialsProviderTests.java @@ -0,0 +1,115 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.repositories.s3; + +import com.amazonaws.auth.AWSCredentials; +import com.sun.net.httpserver.HttpServer; + +import org.apache.logging.log4j.LogManager; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.core.SuppressForbidden; +import org.elasticsearch.env.Environment; +import org.elasticsearch.mocksocket.MockHttpServer; +import org.elasticsearch.rest.RestStatus; +import org.elasticsearch.test.ESTestCase; +import org.junit.Assert; +import org.mockito.Mockito; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Map; + +public class CustomWebIdentityTokenCredentialsProviderTests extends ESTestCase { + + private static final String ROLE_ARN = "arn:aws:iam::123456789012:role/FederatedWebIdentityRole"; + private static final String ROLE_NAME = "sts-fixture-test"; + + @SuppressForbidden(reason = "HTTP server is used for testing") + public void testCreateWebIdentityTokenCredentialsProvider() throws Exception { + HttpServer httpServer = MockHttpServer.createHttp(new InetSocketAddress(InetAddress.getLoopbackAddress().getHostAddress(), 0), 0); + httpServer.createContext("/", exchange -> { + try (exchange) { + exchange.getResponseHeaders().add("Content-Type", "text/xml; charset=UTF-8"); + byte[] response = """ + + + amzn1.account.AF6RHO7KZU5XRVQJGXK6HB56KR2A + client.5498841531868486423.1548@apps.example.com + + %s + AROACLKWSDQRAOEXAMPLE:%s + + + sts_session_token + secret_access_key + %s + sts_access_key + + SourceIdentityValue + www.amazon.com + + + ad4156e9-bce1-11e2-82e6-6b6efEXAMPLE + + + """.formatted( + ROLE_ARN, + ROLE_NAME, + ZonedDateTime.now().plusDays(1L).format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ")) + ).getBytes(StandardCharsets.UTF_8); + exchange.sendResponseHeaders(RestStatus.OK.getStatus(), response.length); + exchange.getResponseBody().write(response); + } + }); + httpServer.start(); + + Path configDirectory = Files.createTempDirectory("web-identity-token-test"); + Files.createDirectory(configDirectory.resolve("repository-s3")); + Files.writeString(configDirectory.resolve("repository-s3/aws-web-identity-token-file"), "YXdzLXdlYi1pZGVudGl0eS10b2tlbi1maWxl"); + Environment environment = Mockito.mock(Environment.class); + Mockito.when(environment.configFile()).thenReturn(configDirectory); + + // No region is set, but the SDK shouldn't fail because of that + Map environmentVariables = Map.of( + "AWS_WEB_IDENTITY_TOKEN_FILE", + "/var/run/secrets/eks.amazonaws.com/serviceaccount/token", + "AWS_ROLE_ARN", + ROLE_ARN, + "AWS_ROLE_SESSION_NAME", + ROLE_NAME + ); + Map systemProperties = Map.of( + "com.amazonaws.sdk.stsMetadataServiceEndpointOverride", + "http://" + httpServer.getAddress().getHostName() + ":" + httpServer.getAddress().getPort() + ); + var webIdentityTokenCredentialsProvider = new S3Service.CustomWebIdentityTokenCredentialsProvider( + environment, + environmentVariables::get, + systemProperties::getOrDefault + ); + try { + AWSCredentials credentials = S3Service.buildCredentials( + LogManager.getLogger(S3Service.class), + S3ClientSettings.getClientSettings(Settings.EMPTY, randomAlphaOfLength(8)), + webIdentityTokenCredentialsProvider + ).getCredentials(); + + Assert.assertEquals("sts_access_key", credentials.getAWSAccessKeyId()); + Assert.assertEquals("secret_access_key", credentials.getAWSSecretKey()); + } finally { + webIdentityTokenCredentialsProvider.shutdown(); + httpServer.stop(0); + } + } +}