diff --git a/packages/@aws-cdk/aws-rds/README.md b/packages/@aws-cdk/aws-rds/README.md index b6883d7a75ccb..897008bc7db59 100644 --- a/packages/@aws-cdk/aws-rds/README.md +++ b/packages/@aws-cdk/aws-rds/README.md @@ -228,6 +228,37 @@ instance.grantConnect(role); // Grant the role connection access to the DB. **Note**: In addition to the setup above, a database user will need to be created to support IAM auth. See https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.IAMDBAuth.DBAccounts.html for setup instructions. +### Kerberos Authentication + +You can also authenticate using Kerberos to a database instance using AWS Managed Microsoft AD for authentication; +See https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/kerberos-authentication.html for more information +and a list of supported versions and limitations. + +The following example shows enabling domain support for a database instance and creating an IAM role to access +Directory Services. + +```ts +const role = new iam.Role(stack, 'RDSDirectoryServicesRole', { + assumedBy: new iam.ServicePrincipal('rds.amazonaws.com'), + managedPolicies: [ + iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AmazonRDSDirectoryServiceAccess'), + ], +}); +const instance = new rds.DatabaseInstance(stack, 'Instance', { + engine: rds.DatabaseInstanceEngine.mysql({ version: rds.MysqlEngineVersion.VER_8_0_19 }), + masterUsername: 'admin', + vpc, + domain: 'd-????????', // The ID of the domain for the instance to join. + domainRole: role, // Optional - will be create automatically if not provided. +}); +``` + +**Note**: In addition to the setup above, you need to make sure that the database instance has network connectivity +to the domain controllers. This includes enabling cross-VPC traffic if in a different VPC and setting up the +appropriate security groups/network ACL to allow traffic between the database instance and domain controllers. +Once configured, see https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/kerberos-authentication.html for details +on configuring users for each available database engine. + ### Metrics Database instances expose metrics (`cloudwatch.Metric`): diff --git a/packages/@aws-cdk/aws-rds/lib/instance-engine.ts b/packages/@aws-cdk/aws-rds/lib/instance-engine.ts index 35ccb3fbea7f2..df715c9e0387a 100644 --- a/packages/@aws-cdk/aws-rds/lib/instance-engine.ts +++ b/packages/@aws-cdk/aws-rds/lib/instance-engine.ts @@ -7,6 +7,13 @@ import { EngineVersion } from './engine-version'; * The options passed to {@link IInstanceEngine.bind}. */ export interface InstanceEngineBindOptions { + /** + * The Active Directory directory ID to create the DB instance in. + * + * @default - none (it's an optional field) + */ + readonly domain?: string; + /** * The timezone of the database, set by the customer. * @@ -184,6 +191,13 @@ class MariaDbInstanceEngine extends InstanceEngineBase { : undefined, }); } + + public bindToInstance(scope: core.Construct, options: InstanceEngineBindOptions): InstanceEngineConfig { + if (options.domain) { + throw new Error(`domain property cannot be configured for ${this.engineType}`); + } + return super.bindToInstance(scope, options); + } } /** diff --git a/packages/@aws-cdk/aws-rds/lib/instance.ts b/packages/@aws-cdk/aws-rds/lib/instance.ts index 7135a84592fd5..bb512d5e79c81 100644 --- a/packages/@aws-cdk/aws-rds/lib/instance.ts +++ b/packages/@aws-cdk/aws-rds/lib/instance.ts @@ -491,6 +491,21 @@ export interface DatabaseInstanceNewProps { * @default - No autoscaling of RDS instance */ readonly maxAllocatedStorage?: number; + + /** + * The Active Directory directory ID to create the DB instance in. + * + * @default - Do not join domain + */ + readonly domain?: string; + + /** + * The IAM role to be used when making API calls to the Directory Service. The role needs the AWS-managed policy + * AmazonRDSDirectoryServiceAccess or equivalent. + * + * @default - The role will be created for you if {@link DatabaseInstanceNewProps#domain} is specified + */ + readonly domainRole?: iam.IRole; } /** @@ -513,6 +528,9 @@ abstract class DatabaseInstanceNew extends DatabaseInstanceBase implements IData private readonly cloudwatchLogsRetention?: logs.RetentionDays; private readonly cloudwatchLogsRetentionRole?: iam.IRole; + private readonly domainId?: string; + private readonly domainRole?: iam.IRole; + protected enableIamAuthentication?: boolean; constructor(scope: Construct, id: string, props: DatabaseInstanceNewProps) { @@ -555,6 +573,16 @@ abstract class DatabaseInstanceNew extends DatabaseInstanceBase implements IData this.cloudwatchLogsRetentionRole = props.cloudwatchLogsRetentionRole; this.enableIamAuthentication = props.iamAuthentication; + if (props.domain) { + this.domainId = props.domain; + this.domainRole = props.domainRole || new iam.Role(this, 'RDSDirectoryServiceRole', { + assumedBy: new iam.ServicePrincipal('rds.amazonaws.com'), + managedPolicies: [ + iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AmazonRDSDirectoryServiceAccess'), + ], + }); + } + this.newCfnProps = { autoMinorVersionUpgrade: props.autoMinorVersionUpgrade, availabilityZone: props.multiAz ? undefined : props.availabilityZone, @@ -587,6 +615,8 @@ abstract class DatabaseInstanceNew extends DatabaseInstanceBase implements IData storageType, vpcSecurityGroups: securityGroups.map(s => s.securityGroupId), maxAllocatedStorage: props.maxAllocatedStorage, + domain: this.domainId, + domainIamRoleName: this.domainRole?.roleName, }; } diff --git a/packages/@aws-cdk/aws-rds/test/test.instance.ts b/packages/@aws-cdk/aws-rds/test/test.instance.ts index 3383f4f93cd55..6a92c763621ed 100644 --- a/packages/@aws-cdk/aws-rds/test/test.instance.ts +++ b/packages/@aws-cdk/aws-rds/test/test.instance.ts @@ -1,4 +1,4 @@ -import { ABSENT, countResources, expect, haveResource, ResourcePart, haveResourceLike } from '@aws-cdk/assert'; +import { ABSENT, countResources, expect, haveResource, ResourcePart, haveResourceLike, anything } from '@aws-cdk/assert'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as targets from '@aws-cdk/aws-events-targets'; import { ManagedPolicy, Role, ServicePrincipal, AccountPrincipal } from '@aws-cdk/aws-iam'; @@ -757,4 +757,125 @@ export = { test.done(); }, + 'domain - sets domain property'(test: Test) { + const domain = 'd-90670a8d36'; + + // WHEN + new rds.DatabaseInstance(stack, 'Instance', { + engine: rds.DatabaseInstanceEngine.sqlServerWeb({ version: rds.SqlServerEngineVersion.VER_14_00_3192_2_V1 }), + vpc, + masterUsername: 'admin', + domain: domain, + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::RDS::DBInstance', { + Domain: domain, + })); + + test.done(); + }, + + 'domain - uses role if provided'(test: Test) { + const domain = 'd-90670a8d36'; + + // WHEN + const role = new Role(stack, 'DomainRole', { assumedBy: new ServicePrincipal('rds.amazonaws.com') }); + new rds.DatabaseInstance(stack, 'Instance', { + engine: rds.DatabaseInstanceEngine.sqlServerWeb({ version: rds.SqlServerEngineVersion.VER_14_00_3192_2_V1 }), + vpc, + masterUsername: 'admin', + domain: domain, + domainRole: role, + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::RDS::DBInstance', { + Domain: domain, + DomainIAMRoleName: stack.resolve(role.roleName), + })); + + test.done(); + }, + + 'domain - creates role if not provided'(test: Test) { + const domain = 'd-90670a8d36'; + + // WHEN + new rds.DatabaseInstance(stack, 'Instance', { + engine: rds.DatabaseInstanceEngine.sqlServerWeb({ version: rds.SqlServerEngineVersion.VER_14_00_3192_2_V1 }), + vpc, + masterUsername: 'admin', + domain: domain, + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::RDS::DBInstance', { + Domain: domain, + DomainIAMRoleName: anything(), + })); + + expect(stack).to(haveResource('AWS::IAM::Role', { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'rds.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + ManagedPolicyArns: [ + { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':iam::aws:policy/service-role/AmazonRDSDirectoryServiceAccess', + ], + ], + }, + ], + })); + + test.done(); + }, + + 'throws when domain is set for mariadb database engine'(test: Test) { + const domainSupportedEngines = [rds.DatabaseInstanceEngine.SQL_SERVER_EE, rds.DatabaseInstanceEngine.SQL_SERVER_EX, + rds.DatabaseInstanceEngine.SQL_SERVER_SE, rds.DatabaseInstanceEngine.SQL_SERVER_WEB, rds.DatabaseInstanceEngine.MYSQL, + rds.DatabaseInstanceEngine.POSTGRES, rds.DatabaseInstanceEngine.ORACLE_EE]; + const domainUnsupportedEngines = [rds.DatabaseInstanceEngine.MARIADB]; + + // THEN + domainSupportedEngines.forEach((engine) => { + test.ok(new rds.DatabaseInstance(stack, `${engine.engineType}-db`, { + engine, + instanceType: ec2.InstanceType.of(ec2.InstanceClass.C5, ec2.InstanceSize.SMALL), + masterUsername: 'master', + domain: 'd-90670a8d36', + vpc, + })); + }); + + domainUnsupportedEngines.forEach((engine) => { + const expectedError = new RegExp(`domain property cannot be configured for ${engine.engineType}`); + + test.throws(() => new rds.DatabaseInstance(stack, `${engine.engineType}-db`, { + engine, + instanceType: ec2.InstanceType.of(ec2.InstanceClass.C5, ec2.InstanceSize.SMALL), + masterUsername: 'master', + domain: 'd-90670a8d36', + vpc, + }), expectedError); + }); + + test.done(); + }, };