diff --git a/packages/@aws-cdk/aws-elasticsearch/README.md b/packages/@aws-cdk/aws-elasticsearch/README.md index e40fa009a1d3e..9aa5d4baa40ef 100644 --- a/packages/@aws-cdk/aws-elasticsearch/README.md +++ b/packages/@aws-cdk/aws-elasticsearch/README.md @@ -130,3 +130,31 @@ const domain = new es.Domain(this, 'Domain', { const masterUserPassword = domain.masterUserPassword; ``` + +### Using unsigned basic auth + +For convenience, the domain can be configured to allow unsigned HTTP requests +that use basic auth. Unless the domain is configured to be part of a VPC this +means anyone can access the domain using the configured master username and +password. + +To enable unsigned basic auth access the domain is configured with an access +policy that allows anyonmous requests, HTTPS required, node to node encryption, +encryption at rest and fine grained access control. + +If the above settings are not set they will be configured as part of enabling +unsigned basic auth. + +If no master user is configured a default master user is created with the +username `admin`. + +```ts +const domain = new es.Domain(this, 'Domain', { + version: es.ElasticsearchVersion.V7_1, + useUnsignedBasicAuth: true, +}); + +const masterUserPassword = domain.masterUserPassword; +``` + +``` diff --git a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts index c7d13ca999591..74fa93e9a0a79 100644 --- a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts +++ b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts @@ -480,6 +480,20 @@ export interface DomainProps { * @default - fine-grained access control is disabled */ readonly fineGrainedAccessControl?: AdvancedSecurityOptions; + + /** + * Configures the domain so that unsigned basic auth is enabled. If no master user is provided a default master user + * with username `admin` and a dynamically generated password stored in KMS is created. The password can be retrieved + * by getting `masterUserPassword` from the domain instance. + * + * Setting this to true will also add an access policy that allows unsigned + * access, enable node to node encryption, encryption at rest. If conflicting + * settings are encountered (like disabling encryption at rest) enabling this + * setting will cause a failure. + * + * @default - false + */ + readonly useUnsignedBasicAuth?: boolean; } /** @@ -859,8 +873,7 @@ abstract class DomainBase extends cdk.Resource implements IDomain { } /** - - * Metric for the time the cluster status is red. + * Metric for the time the cluster status is red. * * @default maximum over 5 minutes */ @@ -1194,27 +1207,7 @@ export class Domain extends DomainBase implements IDomain { } const elasticsearchVersion = props.version.version; - const elasticsearchVersionNum = parseVersion(elasticsearchVersion); - - function parseVersion(version: string): number { - const firstDot = version.indexOf('.'); - - if (firstDot < 1) { - throw new Error(`Invalid Elasticsearch version: ${version}. Version string needs to start with major and minor version (x.y).`); - } - - const secondDot = version.indexOf('.', firstDot + 1); - - try { - if (secondDot == -1) { - return parseFloat(version); - } else { - return parseFloat(version.substring(0, secondDot)); - } - } catch (error) { - throw new Error(`Invalid Elasticsearch version: ${version}. Version string needs to start with major and minor version (x.y).`); - } - } + const elasticsearchVersionNum = parseVersion(props.version); if ( elasticsearchVersionNum <= 7.7 && @@ -1227,8 +1220,33 @@ export class Domain extends DomainBase implements IDomain { throw new Error(`Unknown Elasticsearch version: ${elasticsearchVersion}`); } + const unsignedBasicAuthEnabled = props.useUnsignedBasicAuth ?? false; + + if (unsignedBasicAuthEnabled) { + if (props.enforceHttps == false) { + throw new Error('You cannot disable HTTPS and use unsigned basic auth'); + } + if (props.nodeToNodeEncryption == false) { + throw new Error('You cannot disable node to node encryption and use unsigned basic auth'); + } + if (props.encryptionAtRest?.enabled == false) { + throw new Error('You cannot disable encryption at rest and use unsigned basic auth'); + } + } + + const unsignedAccessPolicy = new iam.PolicyStatement({ + effect: iam.Effect.ALLOW, + actions: ['es:ESHttp*'], + principals: [new iam.Anyone()], + resources: [cdk.Lazy.stringValue({ produce: () => `${this.domainArn}/*` })], + }); + const masterUserArn = props.fineGrainedAccessControl?.masterUserArn; - const masterUserName = props.fineGrainedAccessControl?.masterUserName; + const masterUserNameProps = props.fineGrainedAccessControl?.masterUserName; + // If basic auth is enabled set the user name to admin if no other user info is supplied. + const masterUserName = unsignedBasicAuthEnabled + ? (masterUserArn == null ? (masterUserNameProps ?? 'admin') : undefined) + : masterUserNameProps; if (masterUserArn != null && masterUserName != null) { throw new Error('Invalid fine grained access control settings. Only provide one of master user ARN or master user name. Not both.'); @@ -1253,12 +1271,12 @@ export class Domain extends DomainBase implements IDomain { : undefined; const encryptionAtRestEnabled = - props.encryptionAtRest?.enabled ?? props.encryptionAtRest?.kmsKey != null; - const nodeToNodeEncryptionEnabled = props.nodeToNodeEncryption ?? false; + props.encryptionAtRest?.enabled ?? (props.encryptionAtRest?.kmsKey != null || unsignedBasicAuthEnabled); + const nodeToNodeEncryptionEnabled = props.nodeToNodeEncryption ?? unsignedBasicAuthEnabled; const volumeSize = props.ebs?.volumeSize ?? 10; const volumeType = props.ebs?.volumeType ?? ec2.EbsDeviceVolumeType.GENERAL_PURPOSE_SSD; const ebsEnabled = props.ebs?.enabled ?? true; - const enforceHttps = props.enforceHttps; + const enforceHttps = props.enforceHttps ?? unsignedBasicAuthEnabled; function isInstanceType(t: string): Boolean { return dedicatedMasterType.startsWith(t) || instanceType.startsWith(t); @@ -1301,8 +1319,11 @@ export class Domain extends DomainBase implements IDomain { } if (elasticsearchVersionNum < 6.7) { + if (unsignedBasicAuthEnabled) { + throw new Error('Using unsigned basic auth requires Elasticsearch version 6.7 or later.'); + } if (advancedSecurityEnabled) { - throw new Error('Fine-grained access logging requires Elasticsearch version 6.7 or later.'); + throw new Error('Fine-grained access control requires Elasticsearch version 6.7 or later.'); } } @@ -1398,6 +1419,9 @@ export class Domain extends DomainBase implements IDomain { // Create the domain this.domain = new CfnDomain(this, 'Resource', { domainName: this.physicalName, + accessPolicies: unsignedBasicAuthEnabled + ? (props.accessPolicies ?? []).concat(unsignedAccessPolicy) + : props.accessPolicies, elasticsearchVersion, elasticsearchClusterConfig: { dedicatedMasterEnabled, @@ -1472,14 +1496,15 @@ export class Domain extends DomainBase implements IDomain { if (props.domainName) { this.node.addMetadata('aws:cdk:hasPhysicalName', props.domainName); } + this.domainName = this.getResourceNameAttribute(this.domain.ref); + + this.domainEndpoint = this.domain.getAtt('DomainEndpoint').toString(); + this.domainArn = this.getResourceArnAttribute(this.domain.attrArn, { service: 'es', resource: 'domain', resourceName: this.physicalName, }); - this.domainName = this.getResourceNameAttribute(this.domain.ref); - - this.domainEndpoint = this.domain.getAtt('DomainEndpoint').toString(); } } @@ -1503,3 +1528,29 @@ function extractNameFromEndpoint(domainEndpoint: string) { const suffix = '-' + domain.split('-').slice(-1)[0]; return domain.split(suffix)[0]; } + +/** + * Converts an Elasticsearch version into a into a decimal number with major and minor version i.e x.y. + * + * @param version The Elasticsearch version object + */ +function parseVersion(version: ElasticsearchVersion): number { + const versionStr = version.version; + const firstDot = versionStr.indexOf('.'); + + if (firstDot < 1) { + throw new Error(`Invalid Elasticsearch version: ${versionStr}. Version string needs to start with major and minor version (x.y).`); + } + + const secondDot = versionStr.indexOf('.', firstDot + 1); + + try { + if (secondDot == -1) { + return parseFloat(versionStr); + } else { + return parseFloat(versionStr.substring(0, secondDot)); + } + } catch (error) { + throw new Error(`Invalid Elasticsearch version: ${versionStr}. Version string needs to start with major and minor version (x.y).`); + } +} diff --git a/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts b/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts index 0f3c3d11b55af..622d1784362d3 100644 --- a/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts +++ b/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts @@ -856,6 +856,152 @@ test('can specify future version', () => { }); }); +describe('unsigned basic auth', () => { + test('can create a domain with unsigned basic auth', () => { + new Domain(stack, 'Domain', { + version: ElasticsearchVersion.V7_7, + useUnsignedBasicAuth: true, + }); + + expect(stack).toHaveResourceLike('AWS::Elasticsearch::Domain', { + AccessPolicies: [{ + action: ['es:ESHttp*'], + principal: { + AWS: ['*'], + }, + resource: [{ + 'Fn::Join': [ + '', + [ + { + 'Fn::GetAtt': [ + 'Domain66AC69E0', + 'Arn', + ], + }, + '/*', + ], + ], + }], + effect: 'Allow', + }], + AdvancedSecurityOptions: { + Enabled: true, + InternalUserDatabaseEnabled: true, + MasterUserOptions: { + MasterUserName: 'admin', + }, + }, + EncryptionAtRestOptions: { + Enabled: true, + }, + NodeToNodeEncryptionOptions: { + Enabled: true, + }, + DomainEndpointOptions: { + EnforceHTTPS: true, + }, + }); + }); + + test('does not overwrite master user ARN configuration', () => { + const masterUserArn = 'arn:aws:iam::123456789012:user/JohnDoe'; + + new Domain(stack, 'Domain', { + version: ElasticsearchVersion.V7_7, + fineGrainedAccessControl: { + masterUserArn, + }, + useUnsignedBasicAuth: true, + }); + + expect(stack).toHaveResourceLike('AWS::Elasticsearch::Domain', { + AdvancedSecurityOptions: { + Enabled: true, + InternalUserDatabaseEnabled: false, + MasterUserOptions: { + MasterUserARN: masterUserArn, + }, + }, + EncryptionAtRestOptions: { + Enabled: true, + }, + NodeToNodeEncryptionOptions: { + Enabled: true, + }, + DomainEndpointOptions: { + EnforceHTTPS: true, + }, + }); + }); + + test('does not overwrite master user name and password', () => { + const masterUserName = 'JohnDoe'; + const password = 'password'; + const masterUserPassword = SecretValue.plainText(password); + + new Domain(stack, 'Domain', { + version: ElasticsearchVersion.V7_1, + fineGrainedAccessControl: { + masterUserName, + masterUserPassword, + }, + useUnsignedBasicAuth: true, + }); + + expect(stack).toHaveResourceLike('AWS::Elasticsearch::Domain', { + AdvancedSecurityOptions: { + Enabled: true, + InternalUserDatabaseEnabled: true, + MasterUserOptions: { + MasterUserName: masterUserName, + MasterUserPassword: password, + }, + }, + EncryptionAtRestOptions: { + Enabled: true, + }, + NodeToNodeEncryptionOptions: { + Enabled: true, + }, + DomainEndpointOptions: { + EnforceHTTPS: true, + }, + }); + }); + + test('fails to create a domain with unsigned basic auth when enforce HTTPS is disabled', () => { + expect(() => new Domain(stack, 'Domain', { + version: ElasticsearchVersion.V7_7, + useUnsignedBasicAuth: true, + enforceHttps: false, + })).toThrow(/You cannot disable HTTPS and use unsigned basic auth/); + }); + + test('fails to create a domain with unsigned basic auth when node to node encryption is disabled', () => { + expect(() => new Domain(stack, 'Domain', { + version: ElasticsearchVersion.V7_7, + useUnsignedBasicAuth: true, + nodeToNodeEncryption: false, + })).toThrow(/You cannot disable node to node encryption and use unsigned basic auth/); + }); + + test('fails to create a domain with unsigned basic auth when encryption at rest is disabled', () => { + expect(() => new Domain(stack, 'Domain', { + version: ElasticsearchVersion.V7_7, + useUnsignedBasicAuth: true, + encryptionAtRest: { enabled: false }, + })).toThrow(/You cannot disable encryption at rest and use unsigned basic auth/); + }); + + test('using unsigned basic auth throws with Elasticsearch < 6.7', () => { + expect(() => new Domain(stack, 'Domain', { + version: ElasticsearchVersion.V6_5, + useUnsignedBasicAuth: true, + })).toThrow(/Using unsigned basic auth requires Elasticsearch version 6\.7 or later./); + }); +}); + function testGrant( expectedActions: string[], diff --git a/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.expected.json b/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.expected.json index b20da010c0e52..1178733807627 100644 --- a/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.expected.json +++ b/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.expected.json @@ -84,7 +84,10 @@ "InstallLatestAwsSdk": true }, "UpdateReplacePolicy": "Delete", - "DeletionPolicy": "Delete" + "DeletionPolicy": "Delete", + "DependsOn": [ + "DomainESLogGroupPolicyCustomResourcePolicyB35C8E41" + ] }, "Domain66AC69E0": { "Type": "AWS::Elasticsearch::Domain", @@ -93,6 +96,7 @@ "Enabled": false }, "DomainEndpointOptions": { + "EnforceHTTPS": false, "TLSSecurityPolicy": "Policy-Min-TLS-1-0-2019-07" }, "EBSOptions": { @@ -138,6 +142,7 @@ } }, "DependsOn": [ + "DomainESLogGroupPolicyCustomResourcePolicyB35C8E41", "DomainESLogGroupPolicy5373A2E8" ] }, @@ -188,7 +193,7 @@ ] } }, - "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleDefaultPolicyD28E1A5E": { + "DomainESLogGroupPolicyCustomResourcePolicyB35C8E41": { "Type": "AWS::IAM::Policy", "Properties": { "PolicyDocument": { @@ -204,7 +209,7 @@ ], "Version": "2012-10-17" }, - "PolicyName": "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleDefaultPolicyD28E1A5E", + "PolicyName": "DomainESLogGroupPolicyCustomResourcePolicyB35C8E41", "Roles": [ { "Ref": "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2" @@ -217,7 +222,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParametersa21506baa8c8c02336b158f9f100fd684e4a423c4d762fa21557d79adde522feS3Bucket1EBE4391" + "Ref": "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3Bucket38F1BB8E" }, "S3Key": { "Fn::Join": [ @@ -230,7 +235,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersa21506baa8c8c02336b158f9f100fd684e4a423c4d762fa21557d79adde522feS3VersionKey89F851D8" + "Ref": "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3VersionKeyCCDC67C0" } ] } @@ -243,7 +248,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersa21506baa8c8c02336b158f9f100fd684e4a423c4d762fa21557d79adde522feS3VersionKey89F851D8" + "Ref": "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3VersionKeyCCDC67C0" } ] } @@ -264,23 +269,22 @@ "Timeout": 120 }, "DependsOn": [ - "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleDefaultPolicyD28E1A5E", "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2" ] } }, "Parameters": { - "AssetParametersa21506baa8c8c02336b158f9f100fd684e4a423c4d762fa21557d79adde522feS3Bucket1EBE4391": { - "Type": "String", - "Description": "S3 bucket for asset \"a21506baa8c8c02336b158f9f100fd684e4a423c4d762fa21557d79adde522fe\"" + "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94ArtifactHash782948FC": { + "Type":"String", + "Description":"Artifact hash for asset \"b64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94\"" }, - "AssetParametersa21506baa8c8c02336b158f9f100fd684e4a423c4d762fa21557d79adde522feS3VersionKey89F851D8": { + "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3Bucket38F1BB8E": { "Type": "String", - "Description": "S3 key for asset version \"a21506baa8c8c02336b158f9f100fd684e4a423c4d762fa21557d79adde522fe\"" + "Description": "S3 bucket for asset \"b64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94\"" }, - "AssetParametersa21506baa8c8c02336b158f9f100fd684e4a423c4d762fa21557d79adde522feArtifactHashC36CC496": { + "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3VersionKeyCCDC67C0": { "Type": "String", - "Description": "Artifact hash for asset \"a21506baa8c8c02336b158f9f100fd684e4a423c4d762fa21557d79adde522fe\"" + "Description": "S3 key for asset version \"b64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94\"" } } }