Skip to content
This repository has been archived by the owner on Apr 11, 2024. It is now read-only.

Commit

Permalink
Add option to use AWS secret in auth header for Replayer requests (op…
Browse files Browse the repository at this point in the history
…ensearch-project#265)

* MIGRATIONS-1191: Add support for auth secret in Replayer and IaC

Signed-off-by: Tanner Lewis <[email protected]>
  • Loading branch information
lewijacn authored Aug 16, 2023
1 parent 2c8e3ab commit a99e537
Show file tree
Hide file tree
Showing 9 changed files with 225 additions and 37 deletions.
15 changes: 3 additions & 12 deletions TrafficCapture/trafficReplayer/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ repositories {
dependencies {
implementation project(':captureProtobufs')

implementation group: 'com.amazonaws.secretsmanager', name: 'aws-secretsmanager-caching-java', version: '1.0.2'
implementation group: 'com.beust', name: 'jcommander', version: '1.82'
implementation group: 'com.bazaarvoice.jolt', name: 'jolt-core', version: '0.1.7'
implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.15.0'
Expand All @@ -60,6 +61,8 @@ dependencies {

testImplementation project(':testUtilities')
testImplementation group: 'org.apache.httpcomponents.client5', name: 'httpclient5', version: '5.2.1'
testImplementation 'org.mockito:mockito-core:4.6.1'
testImplementation 'org.mockito:mockito-junit-jupiter:4.6.1'
}

application {
Expand All @@ -72,18 +75,6 @@ jar {
}
}

task uberJar(type: Jar) {
manifest {
attributes 'Main-Class': application.mainClass
}
from {
configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) }
}
archiveBaseName = project.name + "-uber"
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
with jar
}

tasks.named('test') {
useJUnitPlatform()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package org.opensearch.migrations.replay;

import com.amazonaws.secretsmanager.caching.SecretCache;
import lombok.extern.slf4j.Slf4j;

import java.nio.charset.Charset;
import java.util.Base64;

@Slf4j
public class AWSAuthService implements AutoCloseable {

private final SecretCache secretCache;

public AWSAuthService(SecretCache secretCache) {
this.secretCache = secretCache;
}

public AWSAuthService() {
this(new SecretCache());
}

// SecretId here can be either the unique name of the secret or the secret ARN
public String getSecret(String secretId) {
return secretCache.getSecretString(secretId);
}

/**
* This method returns a Basic Auth header string, with the username:password Base64 encoded
* @param username The plaintext username
* @param secretId The unique name of the secret or the secret ARN from AWS Secrets Manager. Its retrieved value
* will fill the password part of the Basic Auth header
* @return Basic Auth header string
*/
public String getBasicAuthHeaderFromSecret(String username, String secretId) {
String authHeaderString = username + ":" + getSecret(secretId);
return "Basic " + Base64.getEncoder().encodeToString(authHeaderString.getBytes(Charset.defaultCharset()));
}

@Override
public void close() {
secretCache.close();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,24 @@ public static boolean validateRequiredKafkaParams(String brokers, String topic,
return true;
}

private static String validateAndReturnAuthHeader(String authHeaderValue, AWSAuthService awsAuthService, String awsAuthHeaderUser, String awsAuthHeaderSecret) {
if (authHeaderValue != null && (awsAuthHeaderUser != null || awsAuthHeaderSecret != null)) {
throw new ParameterException("[--auth-header-value] and [--aws-auth-header-user, --aws-auth-header-secret] are mutually exclusive " +
"sets of authorization header parameters");
}
if (authHeaderValue == null && awsAuthHeaderUser == null && awsAuthHeaderSecret == null) {
return null;
}
if (authHeaderValue != null) {
return authHeaderValue;
}

if (awsAuthHeaderUser == null || awsAuthHeaderSecret == null) {
throw new ParameterException("[--aws-auth-header-user, --aws-auth-header-secret] must both be provided if specified");
}
return awsAuthService.getBasicAuthHeaderFromSecret(awsAuthHeaderUser, awsAuthHeaderSecret);
}

static class Parameters {
@Parameter(required = true,
arity = 1,
Expand All @@ -111,8 +129,18 @@ static class Parameters {
@Parameter(required = false,
names = {"--auth-header-value"},
arity = 1,
description = "Value to use for the \"authorization\" header of each request")
description = "Prepared value to use for the \"authorization\" header of each request")
String authHeaderValue;
@Parameter(required = false,
names = {"--aws-auth-header-user"},
arity = 1,
description = "Plaintext username to use for the username section of a constructed \"authorization\" header for each request")
String awsAuthHeaderUser;
@Parameter(required = false,
names = {"--aws-auth-header-secret"},
arity = 1,
description = "Secret ARN or Secret name from AWS Secrets Manager to use for the password section of a constructed \"authorization\" header for each request")
String awsAuthHeaderSecret;
@Parameter(required = false,
names = {"-o", "--output"},
arity=1,
Expand Down Expand Up @@ -185,7 +213,14 @@ public static void main(String[] args) throws IOException, InterruptedException,
return;
}

var tr = new TrafficReplayer(uri, params.authHeaderValue, params.allowInsecureConnections);
String authHeader;
// This one time retrieval does not warrant a need for our underlying AWSAuthService cache, however in the future it is
// likely this structure will change, and we will have other needs for this
try (AWSAuthService awsAuthService = params.awsAuthHeaderSecret != null ? new AWSAuthService() : null) {
authHeader = validateAndReturnAuthHeader(params.authHeaderValue, awsAuthService, params.awsAuthHeaderUser, params.awsAuthHeaderSecret);
}

var tr = new TrafficReplayer(uri, authHeader, params.allowInsecureConnections);
try (OutputStream outputStream = params.outputFilename == null ? System.out :
new FileOutputStream(params.outputFilename, true)) {
try (var bufferedOutputStream = new BufferedOutputStream(outputStream)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package org.opensearch.migrations.replay;

import com.amazonaws.secretsmanager.caching.SecretCache;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
public class AWSAuthServiceTest {

@Mock
private SecretCache secretCache;

@Test
public void testBasicAuthHeaderFromSecret() {
String testSecretId = "testSecretId";
String testUsername = "testAdmin";
String expectedResult = "Basic dGVzdEFkbWluOmFkbWluUGFzcw==";

when(secretCache.getSecretString(testSecretId)).thenReturn("adminPass");

AWSAuthService awsAuthService = new AWSAuthService(secretCache);
String header = awsAuthService.getBasicAuthHeaderFromSecret(testUsername, testSecretId);
Assertions.assertEquals(expectedResult, header);
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {CfnOutput, RemovalPolicy, SecretValue, Stack} from "aws-cdk-lib";
import {IKey, Key} from "aws-cdk-lib/aws-kms";
import {PolicyStatement} from "aws-cdk-lib/aws-iam";
import {ILogGroup, LogGroup} from "aws-cdk-lib/aws-logs";
import {Secret} from "aws-cdk-lib/aws-secretsmanager";
import {ISecret, Secret} from "aws-cdk-lib/aws-secretsmanager";
import {StackPropsExt} from "./stack-composer";


Expand Down Expand Up @@ -55,8 +55,9 @@ export class OpensearchServiceDomainCdkStack extends Stack {
const earKmsKey: IKey|undefined = props.encryptionAtRestKmsKeyARN && props.encryptionAtRestEnabled ?
Key.fromKeyArn(this, "earKey", props.encryptionAtRestKmsKeyARN) : undefined

let adminUserSecret: SecretValue|undefined = props.fineGrainedManagerUserSecretManagerKeyARN ?
Secret.fromSecretCompleteArn(this, "managerSecret", props.fineGrainedManagerUserSecretManagerKeyARN).secretValue : undefined
let adminUserSecret: ISecret|undefined = props.fineGrainedManagerUserSecretManagerKeyARN ?
Secret.fromSecretCompleteArn(this, "managerSecret", props.fineGrainedManagerUserSecretManagerKeyARN) : undefined


const appLG: ILogGroup|undefined = props.appLogGroup && props.appLogEnabled ?
LogGroup.fromLogGroupArn(this, "appLogGroup", props.appLogGroup) : undefined
Expand All @@ -67,7 +68,11 @@ export class OpensearchServiceDomainCdkStack extends Stack {
// Enable demo mode setting
if (props.enableDemoAdmin) {
adminUserName = "admin"
adminUserSecret = SecretValue.unsafePlainText("Admin123!")
adminUserSecret = new Secret(this, "demoUserSecret", {
secretName: "demo-user-secret",
// This is unsafe and strictly for ease of use in a demo mode setup
secretStringValue: SecretValue.unsafePlainText("Admin123!")
})
}
const zoneAwarenessConfig: ZoneAwarenessConfig|undefined = props.availabilityZoneCount ?
{enabled: true, availabilityZoneCount: props.availabilityZoneCount} : undefined
Expand All @@ -88,7 +93,7 @@ export class OpensearchServiceDomainCdkStack extends Stack {
fineGrainedAccessControl: {
masterUserArn: props.fineGrainedManagerUserARN,
masterUserName: adminUserName,
masterUserPassword: adminUserSecret
masterUserPassword: adminUserSecret ? adminUserSecret.secretValue : undefined
},
nodeToNodeEncryption: props.nodeToNodeEncryptionEnabled,
encryptionAtRest: {
Expand Down Expand Up @@ -116,8 +121,18 @@ export class OpensearchServiceDomainCdkStack extends Stack {

this.domainEndpoint = domain.domainEndpoint

const exports = [
`export MIGRATION_DOMAIN_ENDPOINT=${this.domainEndpoint}`
]
if (domain.masterUserPassword && !adminUserSecret) {
console.log("A master user was configured without an existing Secrets Manager secret, will not export MIGRATION_DOMAIN_USER_NAME and MIGRATION_DOMAIN_USER_SECRET_ARN for Copilot")
}
else if (domain.masterUserPassword && adminUserSecret) {
exports.push(`export MIGRATION_DOMAIN_USER_NAME=${adminUserName}`)
exports.push(`export MIGRATION_DOMAIN_USER_SECRET_ARN=${adminUserSecret.secretArn}`)
}
new CfnOutput(this, 'CopilotDomainExports', {
value: `export MIGRATION_DOMAIN_ENDPOINT=${this.domainEndpoint}`,
value: exports.join(";"),
description: 'Exported Domain resource values created by CDK that are needed by Copilot container deployments',
});
}
Expand Down
32 changes: 24 additions & 8 deletions deployment/copilot/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,31 +39,43 @@ Options:
Deploy migration solution infrastructure composed of resources deployed by CDK and Copilot
Options:
--skip-bootstrap Skip one-time setup of installing npm package, bootstrapping CDK, and building Docker images.
--skip-copilot-init Skip one-time Copilot initialization of app, environments, and services
--copilot-app-name [string, default: migration-copilot] Specify the Copilot application name to use for deployment
--destroy-region Destroy all CDK and Copilot CloudFormation stacks deployed, excluding the Copilot app level stack, for the given region and return to a clean state.
--destroy-all-copilot Destroy Copilot app and all Copilot CloudFormation stacks deployed for the given app across all regions.
-r, --region [string, default: us-east-1] Specify the AWS region to deploy the CloudFormation stacks and resources.
-s, --stage [string, default: dev] Specify the stage name to associate with the deployed resources
--skip-bootstrap Skip one-time setup of installing npm package, bootstrapping CDK, and building Docker images.
--skip-copilot-init Skip one-time Copilot initialization of app, environments, and services
--copilot-app-name [string, default: migration-copilot] Specify the Copilot application name to use for deployment
--destroy-env Destroy all CDK and Copilot CloudFormation stacks deployed, excluding the Copilot app level stack, for the given env/stage and return to a clean state.
--destroy-all-copilot Destroy Copilot app and all Copilot CloudFormation stacks deployed for the given app across all regions.
--auth-header-value [string, default: null] Prepared "authorization" header to provide the Replayer, i.e. Basic YWRtaW46QWRtaW4xMjMh. This will override a CDK configured FGAC master user auth header if setup
--aws-auth-header-user [string, default: null] Plaintext username to provide the Replayer to construct an "authorization" header. Used in conjunction with --aws-auth-header-secret. This will override a CDK configured FGAC master user auth header if setup
--aws-auth-header-secret [string, default: null] Secret ARN or Secret name from AWS Secrets Manager to provide the Replayer to construct an "authorization" header. Used in conjunction with --aws-auth-header-user. This will override a CDK configured FGAC master user auth header if setup
-r, --region [string, default: us-east-1] Specify the AWS region to deploy the CloudFormation stacks and resources.
-s, --stage [string, default: dev] Specify the stage name to associate with the deployed resources
```

Requirements:
* AWS credentials have been configured
* CDK and Copilot CLIs have been installed

#### How is an Authorization header set for requests from the Replayer to the target cluster?

There is a level of precedence that will determine which or if any Auth header should be added to outgoing Replayer requests, which is listed below:
1. `[--auth-header-value]` or `[--aws-auth-header-user, --aws-auth-header-secret]` are provided to the deployment script and will be used
2. If the CDK deploys a target cluster with a configured FGAC user (see `fineGrainedManagerUserName` and `fineGrainedManagerUserSecretManagerKeyARN` CDK context options [here](../cdk/opensearch-service-migration/README.md)) or is running in demo mode (see `enableDemoAdmin` CDK context option), this username and secret key will be used for the Auth header
3. Lastly, the Replayer will not use an explicit Auth header and instead use the same Auth header from the capture source cluster request, if one exists

### Deploy commands one at a time

The following sections list out commands line-by-line for deploying this solution

#### Importing values from CDK
The typical use case for this Copilot app is to initially use the `opensearch-service-migration` CDK to deploy the surrounding infrastructure (VPC, OpenSearch Domain, Managed Kafka (MSK)) that Copilot requires, and then deploy the desired Copilot services. Documentation for setting up and deploying these resources can be found in the CDK [README](../cdk/opensearch-service-migration/README.md).

The provided CDK will output export commands once deployed that can be ran on a given deployment machine to meet the required environment variables this Copilot app uses:
The provided CDK will output export commands once deployed that can be ran on a given deployment machine to meet the required environment variables this Copilot app uses i.e.:
```
export MIGRATION_DOMAIN_SG_ID=sg-123;
export MIGRATION_DOMAIN_ENDPOINT=vpc-aos-domain-123.us-east-1.es.amazonaws.com;
export MIGRATION_DOMAIN_USER_NAME=admin
export MIGRATION_DOMAIN_USER_SECRET_ARN=arn:aws:secretsmanager:us-east-1:123456789123:secret:demo-user-secret-123abc
export MIGRATION_VPC_ID=vpc-123;
export MIGRATION_CAPTURE_MSK_SG_ID=sg-123;
export MIGRATION_COMPARATOR_EFS_ID=fs-123;
Expand All @@ -74,6 +86,10 @@ export MIGRATION_PUBLIC_SUBNETS=subnet-123,subnet-124;
export MIGRATION_PRIVATE_SUBNETS=subnet-125,subnet-126;
export MIGRATION_KAFKA_BROKER_ENDPOINTS=b-1-public.loggingmskcluster.123.45.kafka.us-east-1.amazonaws.com:9198,b-2-public.loggingmskcluster.123.46.kafka.us-east-1.amazonaws.com:9198
```
Additionally, if not using the deploy script, the following export is needed for the Replayer service:
```
export MIGRATION_REPLAYER_COMMAND=/bin/sh -c "/runJavaWithClasspath.sh org.opensearch.migrations.replay.TrafficReplayer $MIGRATION_DOMAIN_ENDPOINT --insecure --kafka-traffic-brokers $MIGRATION_KAFKA_BROKER_ENDPOINTS --kafka-traffic-topic logging-traffic-topic --kafka-traffic-group-id default-logging-group --kafka-traffic-enable-msk-auth --aws-auth-header-user $MIGRATION_DOMAIN_USER_NAME --aws-auth-header-secret $MIGRATION_DOMAIN_USER_SECRET_ARN | nc traffic-comparator 9220"
```

#### Setting up existing Copilot infrastructure

Expand Down
Loading

0 comments on commit a99e537

Please sign in to comment.