Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(deadline): add Secret Management support for Repository #514

Merged
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
76 changes: 76 additions & 0 deletions packages/aws-rfdk/lib/deadline/lib/repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ import {
import {
Asset,
} from '@aws-cdk/aws-s3-assets';
import {
ISecret,
Secret,
} from '@aws-cdk/aws-secretsmanager';
import {
Annotations,
Construct,
Expand Down Expand Up @@ -271,6 +275,36 @@ export interface RepositorySecurityGroupsOptions {
readonly installer?: ISecurityGroup;
}

/**
* Settings used by Deadline Secrets Management, a feature introduced in Deadline 10.1.10 for securely managing storage
* and access of Secrets for your render farm.
* More details at:
* https://docs.thinkboxsoftware.com/products/deadline/10.1/1_User%20Manual/manual/secrets-management/deadline-secrets-management.html
* Using Secrets Management requires TLS to be enabled between the RenderQueue and its clients. If this feature is enabled, the
* `externalTLS` on the `RenderQueueTrafficEncryptionProps` interface on the RenderQueue cannot be disabled.
*/
export interface SecretsManagementProps {
/**
* Whether or not to enable the Secrets Management feature.
*/
readonly enabled: boolean;

/**
* A Secret containing the username and password to use for the admin role.
* The contents of this secret must be a JSON document with the keys "username" and "password". ex:
* {
* "username": <admin user name>,
* "password": <admin user password>,
* }
* Password should be at least 8 characters long and contain at least one lowercase letter, one uppercase letter, one symbol and one number.
* In the case when the password does not meet the requirements, the repository construct will fail to deploy.
* It is highly recommended that you leave this parameter undefined to enable the automatic generation of a strong password.
*
* @default: A random username and password will be generated in a Secret with ID `SMAdminUser` and will need to be retrieved from AWS Secrets Manager if it is needed
*/
readonly credentials?: ISecret;
}

/**
* Properties for the Deadline repository
*/
Expand Down Expand Up @@ -387,6 +421,15 @@ export interface RepositoryProps {
* @default Repository settings are not imported.
*/
readonly repositorySettings?: Asset;

/**
* Define the settings used by Deadline Secrets Management, a feature introduced in Deadline 10.1.10 for securely managing storage
* and access of Secrets for your render farm.
* More details at:
* https://docs.thinkboxsoftware.com/products/deadline/10.1/1_User%20Manual/manual/secrets-management/deadline-secrets-management.html
* @default: Secrets Management will be enabled and a username and password will be automatically generated if none are supplied.
*/
readonly secretsManagementSettings?: SecretsManagementProps;
}

/**
Expand Down Expand Up @@ -477,6 +520,11 @@ export class Repository extends Construct implements IRepository {
*/
private static REPOSITORY_OWNER = { uid: 1000, gid: 1000 };

/**
* Default username for auto generated admin credentials in Secret Manager.
*/
private static DEFAULT_SECRETS_MANAGEMENT_USERNAME: string = 'admin';

/**
* @inheritdoc
*/
Expand Down Expand Up @@ -508,6 +556,11 @@ export class Repository extends Construct implements IRepository {
*/
private readonly installerGroup: AutoScalingGroup;

/**
* Deadline Secrets Management settings.
*/
public readonly secretsManagementSettings: SecretsManagementProps;

constructor(scope: Construct, id: string, props: RepositoryProps) {
super(scope, id);

Expand All @@ -526,6 +579,23 @@ export class Repository extends Construct implements IRepository {

this.version = props.version;

this.secretsManagementSettings = {
enabled: props.secretsManagementSettings?.enabled ?? true,
credentials: props.secretsManagementSettings?.credentials ??
((props.secretsManagementSettings?.enabled ?? true) ? new Secret(this, 'SMAdminUser', {
description: 'Admin credentials for Deadline Secrets Management',
generateSecretString: {
excludeCharacters: '\"$&\'()/<>[\\]\`{|}',
includeSpace: false,
passwordLength: 24,
requireEachIncludedType: true,

generateStringKey: 'password',
secretStringTemplate: JSON.stringify({ username: Repository.DEFAULT_SECRETS_MANAGEMENT_USERNAME }),
},
}) : undefined),
};

this.fileSystem = props.fileSystem ?? (() => {
const fs = new EfsFileSystem(this, 'FileSystem', {
vpc: props.vpc,
Expand Down Expand Up @@ -934,6 +1004,12 @@ export class Repository extends Construct implements IRepository {
'-v', version.linuxFullVersionString(),
];

if (this.secretsManagementSettings.enabled) {
installerArgs.push('-r', Stack.of(this.secretsManagementSettings.credentials ?? this).region);
this.secretsManagementSettings.credentials!.grantRead(installerGroup);
installerArgs.push('-c', this.secretsManagementSettings.credentials!.secretArn ?? '');
}

if (settings) {
const repositorySettingsFilePath = installerGroup.userData.addS3DownloadCommand({
bucket: settings.bucket,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@ Required arguments:

Optional arguments
-s Deadline Repository settings file to import.
-o The UID[:GID] that this script will chown the Repository files for. If GID is not specified, it defults to be the same as UID."
-o The UID[:GID] that this script will chown the Repository files for. If GID is not specified, it defults to be the same as UID.
-c Secret management admin credentials ARN. If this parameter is specified, secrets management will be enabled.
-r Region where stacks are deployed. Required to get secret management credentials."

while getopts "i:p:v:s:o:" opt; do
while getopts "i:p:v:s:o:c:r:" opt; do
case $opt in
i) S3PATH="$OPTARG"
;;
Expand All @@ -31,6 +33,10 @@ while getopts "i:p:v:s:o:" opt; do
;;
o) DEADLINE_REPOSITORY_OWNER="$OPTARG"
;;
c) SECRET_MANAGEMENT_ARN="$OPTARG"
;;
r) AWS_REGION="$OPTARG"
;;
/?)
echo "$USAGE"
exit 1
Expand Down Expand Up @@ -109,6 +115,26 @@ if [ ! -z "${DEADLINE_REPOSITORY_SETTINGS_FILE+x}" ]; then
fi
fi

SECRET_MANAGEMENT_ARG=''
if [ ! -z "${SECRET_MANAGEMENT_ARN+x}" ]; then
sudo yum install -y jq
SM_SECRET_VALUE=$(aws secretsmanager get-secret-value --secret-id=$SECRET_MANAGEMENT_ARN --region=$AWS_REGION)
SM_SECRET_STRING=$(jq -r '.SecretString' <<< "$SM_SECRET_VALUE")
SECRET_MANAGEMENT_USER=$(jq -r '.username' <<< "$SM_SECRET_STRING")
SECRET_MANAGEMENT_PASSWORD=$(jq -r '.password' <<< "$SM_SECRET_STRING")
if !([[ ${#SECRET_MANAGEMENT_PASSWORD} -ge 8 ]] &&
echo $SECRET_MANAGEMENT_PASSWORD | grep -q [0-9] &&
echo $SECRET_MANAGEMENT_PASSWORD | grep -q [a-z] &&
echo $SECRET_MANAGEMENT_PASSWORD | grep -q [A-Z] &&
echo $SECRET_MANAGEMENT_PASSWORD | grep -q [^[:alnum:]])
then
echo "ERROR: Admin password is too weak. It must be at least 8 characters long and contain at least one lowercase letter, one uppercase letter, one symbol and one digit."
exit 1
fi
echo "Secret management is enabled. Credentials are stored in secret: $SECRET_MANAGEMENT_ARN"
SECRET_MANAGEMENT_ARG="--installSecretsManagement true --secretsAdminName \"$SECRET_MANAGEMENT_USER\" --secretsAdminPassword \"$SECRET_MANAGEMENT_PASSWORD\""
fi

if [[ -n "${DEADLINE_REPOSITORY_OWNER+x}" ]]; then
if [[ ! "$DEADLINE_REPOSITORY_OWNER" =~ ^[0-9]+(:[0-9]+)?$ ]]; then
echo "ERROR: Deadline Repository owner is invalid: ${DEADLINE_REPOSITORY_OWNER}"
Expand Down Expand Up @@ -138,7 +164,7 @@ if [[ -n "${DEADLINE_REPOSITORY_OWNER+x}" ]]; then
fi
fi

$REPO_INSTALLER --mode unattended --setpermissions false --prefix "$PREFIX" --installmongodb false --backuprepo false ${INSTALLER_DB_ARGS_STRING} $REPOSITORY_SETTINGS_ARG_STRING
$REPO_INSTALLER --mode unattended --setpermissions false --prefix "$PREFIX" --installmongodb false --backuprepo false ${INSTALLER_DB_ARGS_STRING} $REPOSITORY_SETTINGS_ARG_STRING $SECRET_MANAGEMENT_ARG

if [[ -n "${REPOSITORY_OWNER_UID+x}" ]]; then
echo "Changing ownership of Deadline Repository files to UID=$REPOSITORY_OWNER_UID GID=$REPOSITORY_OWNER_GID"
Expand Down
59 changes: 58 additions & 1 deletion packages/aws-rfdk/lib/deadline/test/repository.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
} from '@aws-cdk/aws-efs';
import { Bucket } from '@aws-cdk/aws-s3';
import { Asset } from '@aws-cdk/aws-s3-assets';
import { Secret } from '@aws-cdk/aws-secretsmanager';
import {
App,
CfnElement,
Expand Down Expand Up @@ -1069,7 +1070,7 @@ describe('tagging', () => {
'AWS::EC2::SecurityGroup': 3,
'AWS::DocDB::DBClusterParameterGroup': 1,
'AWS::DocDB::DBSubnetGroup': 1,
'AWS::SecretsManager::Secret': 1,
'AWS::SecretsManager::Secret': 2,
'AWS::DocDB::DBCluster': 1,
'AWS::DocDB::DBInstance': 1,
'AWS::IAM::Role': 1,
Expand Down Expand Up @@ -1260,3 +1261,59 @@ test('IMountableLinuxFilesystem.usesUserPosixPermissions() = false does not chan
// THEN
expect(script).not.toMatch('-o 1000:1000');
});

test('secret manager enabled', () => {
horsmand marked this conversation as resolved.
Show resolved Hide resolved
// GIVEN
const expectedCredentials = new Secret(stack, 'CustomSMAdminUser', {
description: 'Custom admin credentials for the Secret Management',
generateSecretString: {
excludeCharacters: '\"$&\'()-/<>[\\]\`{|}',
includeSpace: false,
passwordLength: 24,
requireEachIncludedType: true,
generateStringKey: 'password',
secretStringTemplate: JSON.stringify({ username: 'admin' }),
},
});

// WHEN
const repository = new Repository(stack, 'Repository', {
vpc,
version,
secretsManagementSettings: {
enabled: true,
credentials: expectedCredentials,
},
});

// THEN
expect(repository.secretsManagementSettings.credentials).toBe(expectedCredentials);
horsmand marked this conversation as resolved.
Show resolved Hide resolved
const installerGroup = repository.node.tryFindChild('Installer') as AutoScalingGroup;
expect(installerGroup.userData.render()).toContain(`-r ${stack.region} -c ${expectedCredentials.secretArn}`);
});

test('secret manager is enabled by default', () => {
// WHEN
const repository = new Repository(stack, 'Repository', {
vpc,
version,
});

// THEN
expect(repository.secretsManagementSettings.enabled).toBeTruthy();
expect(repository.secretsManagementSettings.credentials).toBeDefined();
});

test('credentials are undefined when secrets management is disabled', () => {
// WHEN
const repository = new Repository(stack, 'Repository', {
vpc,
version,
secretsManagementSettings: {
enabled: false,
},
});

// THEN
expect(repository.secretsManagementSettings.credentials).toBeUndefined();
});