Skip to content

Commit

Permalink
Enable unsigned basic auth
Browse files Browse the repository at this point in the history
  • Loading branch information
Stephan Hoermann committed Sep 20, 2020
1 parent caa310d commit 469aa28
Show file tree
Hide file tree
Showing 4 changed files with 274 additions and 45 deletions.
28 changes: 28 additions & 0 deletions packages/@aws-cdk/aws-elasticsearch/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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;
```

```
113 changes: 82 additions & 31 deletions packages/@aws-cdk/aws-elasticsearch/lib/domain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -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 &&
Expand All @@ -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.');
Expand All @@ -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);
Expand Down Expand Up @@ -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.');
}
}

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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();
}
}

Expand All @@ -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).`);
}
}
146 changes: 146 additions & 0 deletions packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[],
Expand Down
Loading

0 comments on commit 469aa28

Please sign in to comment.