Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public class RepositoryS3EcsCredentialsRestIT extends AbstractRepositoryS3RestTe
private static final String BASE_PATH = PREFIX + "base_path";
private static final String CLIENT = "ecs_credentials_client";

private static final DynamicAwsCredentials dynamicCredentials = new DynamicAwsCredentials();
private static final DynamicAwsCredentials dynamicCredentials = new DynamicAwsCredentials("*", "s3");

private static final Ec2ImdsHttpFixture ec2ImdsHttpFixture = new Ec2ImdsHttpFixture(
new Ec2ImdsServiceBuilder(Ec2ImdsVersion.V1).newCredentialsConsumer(dynamicCredentials::addValidCredentials)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public class RepositoryS3ImdsV1CredentialsRestIT extends AbstractRepositoryS3Res
private static final String BASE_PATH = PREFIX + "base_path";
private static final String CLIENT = "imdsv1_credentials_client";

private static final DynamicAwsCredentials dynamicCredentials = new DynamicAwsCredentials();
private static final DynamicAwsCredentials dynamicCredentials = new DynamicAwsCredentials("*", "s3");

private static final Ec2ImdsHttpFixture ec2ImdsHttpFixture = new Ec2ImdsHttpFixture(
new Ec2ImdsServiceBuilder(Ec2ImdsVersion.V1).newCredentialsConsumer(dynamicCredentials::addValidCredentials)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public class RepositoryS3ImdsV2CredentialsRestIT extends AbstractRepositoryS3Res
private static final String BASE_PATH = PREFIX + "base_path";
private static final String CLIENT = "imdsv2_credentials_client";

private static final DynamicAwsCredentials dynamicCredentials = new DynamicAwsCredentials();
private static final DynamicAwsCredentials dynamicCredentials = new DynamicAwsCredentials("*", "s3");

private static final Ec2ImdsHttpFixture ec2ImdsHttpFixture = new Ec2ImdsHttpFixture(
new Ec2ImdsServiceBuilder(Ec2ImdsVersion.V2).newCredentialsConsumer(dynamicCredentials::addValidCredentials)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public class RepositoryS3StsCredentialsRestIT extends AbstractRepositoryS3RestTe
private static final String BASE_PATH = PREFIX + "base_path";
private static final String CLIENT = "sts_credentials_client";

private static final DynamicAwsCredentials dynamicCredentials = new DynamicAwsCredentials();
private static final DynamicAwsCredentials dynamicCredentials = new DynamicAwsCredentials("*", "s3");

private static final S3HttpFixture s3HttpFixture = new S3HttpFixture(true, BUCKET, BASE_PATH, dynamicCredentials::isAuthorized);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,12 @@

public class DiscoveryEc2EcsCredentialsIT extends DiscoveryEc2ClusterFormationTestCase {

private static final DynamicAwsCredentials dynamicCredentials = new DynamicAwsCredentials();

private static final String PREFIX = getIdentifierPrefix("DiscoveryEc2EcsCredentialsIT");
private static final String REGION = PREFIX + "-region";
private static final String CREDENTIALS_ENDPOINT = "/ecs_credentials_endpoint_" + PREFIX;

private static final DynamicAwsCredentials dynamicCredentials = new DynamicAwsCredentials(REGION, "ec2");

private static final Ec2ImdsHttpFixture ec2ImdsHttpFixture = new Ec2ImdsHttpFixture(
new Ec2ImdsServiceBuilder(Ec2ImdsVersion.V1).newCredentialsConsumer(dynamicCredentials::addValidCredentials)
.alternativeCredentialsEndpoints(Set.of(CREDENTIALS_ENDPOINT))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,27 @@
import fixture.aws.imds.Ec2ImdsServiceBuilder;
import fixture.aws.imds.Ec2ImdsVersion;

import org.elasticsearch.common.util.LazyInitializable;
import org.elasticsearch.discovery.DiscoveryModule;
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.test.cluster.ElasticsearchCluster;
import org.junit.ClassRule;
import org.junit.rules.RuleChain;
import org.junit.rules.TestRule;

import java.util.List;
import java.util.function.Supplier;

public class DiscoveryEc2InstanceProfileIT extends DiscoveryEc2ClusterFormationTestCase {

private static final DynamicAwsCredentials dynamicCredentials = new DynamicAwsCredentials();
// Lazy-initialized so we can generate it randomly, which is not possible in static context.
private static final Supplier<String> regionSupplier = new LazyInitializable<>(ESTestCase::randomIdentifier)::getOrCompute;

private static final DynamicAwsCredentials dynamicCredentials = new DynamicAwsCredentials(regionSupplier, "ec2");

private static final Ec2ImdsHttpFixture ec2ImdsHttpFixture = new Ec2ImdsHttpFixture(
new Ec2ImdsServiceBuilder(Ec2ImdsVersion.V2).instanceIdentityDocument(
(builder, params) -> builder.field("region", randomIdentifier())
(builder, params) -> builder.field("region", regionSupplier.get())
).newCredentialsConsumer(dynamicCredentials::addValidCredentials)
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,25 +39,34 @@ public static BiPredicate<String, String> fixedAccessKey(String accessKey, Strin
* @param region the name of the AWS region used to sign the request, or {@code *} to skip validation of the region parameter
*/
public static BiPredicate<String, String> mutableAccessKey(Supplier<String> accessKeySupplier, String region, String serviceName) {
return (authorizationHeader, sessionTokenHeader) -> {
if (authorizationHeader == null) {
return false;
}
return (authorizationHeader, sessionTokenHeader) -> authorizationHeader != null
&& isValidAwsV4SignedAuthorizationHeader(accessKeySupplier.get(), region, serviceName, authorizationHeader);
}

final var accessKey = accessKeySupplier.get();
final var expectedPrefix = "AWS4-HMAC-SHA256 Credential=" + accessKey + "/";
if (authorizationHeader.startsWith(expectedPrefix) == false) {
return false;
}
/**
* @return whether the given value is a valid AWS-v4-signed authorization header that matches the given access key, region, and service
* name.
* @see <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-auth-using-authorization-header.html">AWS v4 Signatures</a>
* @param region the name of the AWS region used to sign the request, or {@code *} to skip validation of the region parameter
*/
public static boolean isValidAwsV4SignedAuthorizationHeader(
String accessKey,
String region,
String serviceName,
String authorizationHeader
) {
final var expectedPrefix = "AWS4-HMAC-SHA256 Credential=" + accessKey + "/";
if (authorizationHeader.startsWith(expectedPrefix) == false) {
return false;
}

if (region.equals("*")) {
// skip region validation; TODO eliminate this when region is fixed in all tests
return authorizationHeader.contains("/" + serviceName + "/aws4_request, ");
}
if (region.equals("*")) {
// skip region validation; TODO eliminate this when region is fixed in all tests
return authorizationHeader.contains("/" + serviceName + "/aws4_request, ");
}

final var remainder = authorizationHeader.substring(expectedPrefix.length() + "YYYYMMDD".length() /* skip over date field */);
return remainder.startsWith("/" + region + "/" + serviceName + "/aws4_request, ");
};
final var remainder = authorizationHeader.substring(expectedPrefix.length() + "YYYYMMDD".length() /* skip over date field */);
return remainder.startsWith("/" + region + "/" + serviceName + "/aws4_request, ");
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,63 @@
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Supplier;

/**
* Allows dynamic creation of access-key/session-token credentials for accessing AWS services such as S3. Typically there's one service
* (e.g. IMDS or STS) which creates credentials dynamically and registers them here using {@link #addValidCredentials}, and then the
* fixture uses {@link #isAuthorized} to validate the credentials it receives corresponds with some previously-generated credentials.
*/
public class DynamicAwsCredentials {

/**
* Extra validation that requests are signed using the correct region. Lazy so it can be randomly generated after initialization, since
* randomness is not available in static context.
*/
private final Supplier<String> expectedRegionSupplier;

/**
* Extra validation that requests are directed to the correct service.
*/
private final String expectedServiceName;

/**
* The set of access keys for each session token registered with {@link #addValidCredentials}. It's this way round because the session
* token is a separate header so it's easier to extract.
*/
private final Map<String, Set<String>> validCredentialsMap = ConcurrentCollections.newConcurrentMap();

/**
* @param expectedRegion The region to use for validating the authorization header, or {@code *} to skip this validation.
* @param expectedServiceName The service name that should appear in the authorization header.
*/
public DynamicAwsCredentials(String expectedRegion, String expectedServiceName) {
this(() -> expectedRegion, expectedServiceName);
}

/**
* @param expectedRegionSupplier Supplies the region to use for validating the authorization header, or {@code *} to skip this
* validation.
* @param expectedServiceName The service name that should appear in the authorization header.
*/
public DynamicAwsCredentials(Supplier<String> expectedRegionSupplier, String expectedServiceName) {
this.expectedRegionSupplier = expectedRegionSupplier;
this.expectedServiceName = expectedServiceName;
}

public boolean isAuthorized(String authorizationHeader, String sessionTokenHeader) {
return authorizationHeader != null
&& sessionTokenHeader != null
&& validCredentialsMap.getOrDefault(sessionTokenHeader, Set.of()).stream().anyMatch(authorizationHeader::contains);
&& validCredentialsMap.getOrDefault(sessionTokenHeader, Set.of())
.stream()
.anyMatch(
validAccessKey -> AwsCredentialsUtils.isValidAwsV4SignedAuthorizationHeader(
validAccessKey,
expectedRegionSupplier.get(),
expectedServiceName,
authorizationHeader
)
);
}

public void addValidCredentials(String accessKey, String sessionToken) {
Expand Down