From fc7fc6844ea19e8e70e5a72eca2de6f764717fe7 Mon Sep 17 00:00:00 2001 From: Eugene Kozlov Date: Thu, 29 Jul 2021 23:08:34 +0000 Subject: [PATCH 1/2] feat: add Secret Management support for repository --- .../aws-rfdk/lib/deadline/lib/repository.ts | 69 +++++++++++++++++++ .../scripts/bash/installDeadlineRepository.sh | 41 ++++++++++- .../lib/deadline/test/repository.test.ts | 59 +++++++++++++++- 3 files changed, 167 insertions(+), 2 deletions(-) diff --git a/packages/aws-rfdk/lib/deadline/lib/repository.ts b/packages/aws-rfdk/lib/deadline/lib/repository.ts index 378f60200..4729abb90 100644 --- a/packages/aws-rfdk/lib/deadline/lib/repository.ts +++ b/packages/aws-rfdk/lib/deadline/lib/repository.ts @@ -45,6 +45,10 @@ import { import { Asset, } from '@aws-cdk/aws-s3-assets'; +import { + ISecret, + Secret, +} from '@aws-cdk/aws-secretsmanager'; import { Annotations, Construct, @@ -271,6 +275,34 @@ 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. + * @default true + */ + 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": , + * "password": , + * } + * Password should contain at least one lowercase letter, one uppercase letter, one symbol and one number. + * + * @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 */ @@ -387,6 +419,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 } /** @@ -508,6 +549,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); @@ -526,6 +572,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 Secret Management', + generateSecretString: { + excludeCharacters: '\"$&\'()-/<>[\\]\`{|}', + includeSpace: false, + passwordLength: 24, + requireEachIncludedType: true, + + generateStringKey: 'password', + secretStringTemplate: JSON.stringify({ username: 'admin' }), + }, + }) : undefined), + }; + this.fileSystem = props.fileSystem ?? (() => { const fs = new EfsFileSystem(this, 'FileSystem', { vpc: props.vpc, @@ -934,6 +997,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, diff --git a/packages/aws-rfdk/lib/deadline/scripts/bash/installDeadlineRepository.sh b/packages/aws-rfdk/lib/deadline/scripts/bash/installDeadlineRepository.sh index 40de48e43..302b1f9e9 100644 --- a/packages/aws-rfdk/lib/deadline/scripts/bash/installDeadlineRepository.sh +++ b/packages/aws-rfdk/lib/deadline/scripts/bash/installDeadlineRepository.sh @@ -18,6 +18,8 @@ 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." + -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 case $opt in @@ -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 @@ -109,6 +115,39 @@ 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") + + len=$(echo ${#SECRET_MANAGEMENT_PASSWORD}) + if test $len -ge 8 ; then + echo "$SECRET_MANAGEMENT_PASSWORD" | grep -q [0-9] + if test $? -eq 0 ; then + echo "$SECRET_MANAGEMENT_PASSWORD" | grep -q [A-Z] + if test $? -eq 0 ; then + echo "$SECRET_MANAGEMENT_PASSWORD" | grep -q [a-z] + if test $? -eq 0 ; then + echo "$SECRET_MANAGEMENT_PASSWORD" | grep -q [~,.,:,@,!,\#,%,*,_,+,-,=,?] + if test $? -eq 0 ; then + SM_STRONG_PASSWORD='true' + fi + fi + fi + fi + fi + if [ -z "${SM_STRONG_PASSWORD+x}" ]; 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}" @@ -138,7 +177,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" diff --git a/packages/aws-rfdk/lib/deadline/test/repository.test.ts b/packages/aws-rfdk/lib/deadline/test/repository.test.ts index a599d170d..4d7a844a6 100644 --- a/packages/aws-rfdk/lib/deadline/test/repository.test.ts +++ b/packages/aws-rfdk/lib/deadline/test/repository.test.ts @@ -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, @@ -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, @@ -1260,3 +1261,59 @@ test('IMountableLinuxFilesystem.usesUserPosixPermissions() = false does not chan // THEN expect(script).not.toMatch('-o 1000:1000'); }); + +test('secret manager enabled', () => { + // 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); + const installerGroup = repository.node.tryFindChild('Installer') as AutoScalingGroup; + expect(installerGroup.userData.render()).toContain(`--installSecretsManagement true ${stack.region} ${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(); +}); \ No newline at end of file From 08bcae490e60294e479bee5588ae8dd0e1331431 Mon Sep 17 00:00:00 2001 From: Eugene Kozlov Date: Mon, 9 Aug 2021 16:28:03 +0000 Subject: [PATCH 2/2] fix: add pasword validation --- .../aws-rfdk/lib/deadline/lib/repository.ts | 27 ++++++++++------ .../scripts/bash/installDeadlineRepository.sh | 31 ++++++------------- .../lib/deadline/test/repository.test.ts | 4 +-- 3 files changed, 28 insertions(+), 34 deletions(-) diff --git a/packages/aws-rfdk/lib/deadline/lib/repository.ts b/packages/aws-rfdk/lib/deadline/lib/repository.ts index 4729abb90..438c6e34f 100644 --- a/packages/aws-rfdk/lib/deadline/lib/repository.ts +++ b/packages/aws-rfdk/lib/deadline/lib/repository.ts @@ -286,9 +286,9 @@ export interface RepositorySecurityGroupsOptions { export interface SecretsManagementProps { /** * Whether or not to enable the Secrets Management feature. - * @default true */ - readonly enabled?: boolean; + 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: @@ -296,7 +296,9 @@ export interface SecretsManagementProps { * "username": , * "password": , * } - * Password should contain at least one lowercase letter, one uppercase letter, one symbol and one number. + * 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 */ @@ -427,7 +429,7 @@ export interface RepositoryProps { * 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 + readonly secretsManagementSettings?: SecretsManagementProps; } /** @@ -518,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 */ @@ -552,7 +559,7 @@ export class Repository extends Construct implements IRepository { /** * Deadline Secrets Management settings. */ - public readonly secretsManagementSettings: SecretsManagementProps + public readonly secretsManagementSettings: SecretsManagementProps; constructor(scope: Construct, id: string, props: RepositoryProps) { super(scope, id); @@ -576,15 +583,15 @@ export class Repository extends Construct implements IRepository { enabled: props.secretsManagementSettings?.enabled ?? true, credentials: props.secretsManagementSettings?.credentials ?? ((props.secretsManagementSettings?.enabled ?? true) ? new Secret(this, 'SMAdminUser', { - description: 'Admin credentials for Secret Management', + description: 'Admin credentials for Deadline Secrets Management', generateSecretString: { - excludeCharacters: '\"$&\'()-/<>[\\]\`{|}', + excludeCharacters: '\"$&\'()/<>[\\]\`{|}', includeSpace: false, passwordLength: 24, requireEachIncludedType: true, generateStringKey: 'password', - secretStringTemplate: JSON.stringify({ username: 'admin' }), + secretStringTemplate: JSON.stringify({ username: Repository.DEFAULT_SECRETS_MANAGEMENT_USERNAME }), }, }) : undefined), }; @@ -999,8 +1006,8 @@ export class Repository extends Construct implements IRepository { 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 ?? ''); + this.secretsManagementSettings.credentials!.grantRead(installerGroup); + installerArgs.push('-c', this.secretsManagementSettings.credentials!.secretArn ?? ''); } if (settings) { diff --git a/packages/aws-rfdk/lib/deadline/scripts/bash/installDeadlineRepository.sh b/packages/aws-rfdk/lib/deadline/scripts/bash/installDeadlineRepository.sh index 302b1f9e9..bef513bca 100644 --- a/packages/aws-rfdk/lib/deadline/scripts/bash/installDeadlineRepository.sh +++ b/packages/aws-rfdk/lib/deadline/scripts/bash/installDeadlineRepository.sh @@ -17,11 +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. + -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" ;; @@ -118,29 +118,16 @@ 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") - - len=$(echo ${#SECRET_MANAGEMENT_PASSWORD}) - if test $len -ge 8 ; then - echo "$SECRET_MANAGEMENT_PASSWORD" | grep -q [0-9] - if test $? -eq 0 ; then - echo "$SECRET_MANAGEMENT_PASSWORD" | grep -q [A-Z] - if test $? -eq 0 ; then - echo "$SECRET_MANAGEMENT_PASSWORD" | grep -q [a-z] - if test $? -eq 0 ; then - echo "$SECRET_MANAGEMENT_PASSWORD" | grep -q [~,.,:,@,!,\#,%,*,_,+,-,=,?] - if test $? -eq 0 ; then - SM_STRONG_PASSWORD='true' - fi - fi - fi - fi - fi - if [ -z "${SM_STRONG_PASSWORD+x}" ]; then + 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 diff --git a/packages/aws-rfdk/lib/deadline/test/repository.test.ts b/packages/aws-rfdk/lib/deadline/test/repository.test.ts index 4d7a844a6..5023b5f58 100644 --- a/packages/aws-rfdk/lib/deadline/test/repository.test.ts +++ b/packages/aws-rfdk/lib/deadline/test/repository.test.ts @@ -1289,7 +1289,7 @@ test('secret manager enabled', () => { // THEN expect(repository.secretsManagementSettings.credentials).toBe(expectedCredentials); const installerGroup = repository.node.tryFindChild('Installer') as AutoScalingGroup; - expect(installerGroup.userData.render()).toContain(`--installSecretsManagement true ${stack.region} ${expectedCredentials.secretArn}`); + expect(installerGroup.userData.render()).toContain(`-r ${stack.region} -c ${expectedCredentials.secretArn}`); }); test('secret manager is enabled by default', () => { @@ -1316,4 +1316,4 @@ test('credentials are undefined when secrets management is disabled', () => { // THEN expect(repository.secretsManagementSettings.credentials).toBeUndefined(); -}); \ No newline at end of file +});