From 1e7aad92c84d9af9ea20f617d3c1726a81b220e0 Mon Sep 17 00:00:00 2001 From: spensireli <5614310+spensireli@users.noreply.github.com> Date: Thu, 31 Jul 2025 22:15:54 -0400 Subject: [PATCH 1/3] feat: fixup kms permissions --- src/control-tower/control-tower-landing-zone.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/control-tower/control-tower-landing-zone.ts b/src/control-tower/control-tower-landing-zone.ts index 6b72b7c..7554cba 100644 --- a/src/control-tower/control-tower-landing-zone.ts +++ b/src/control-tower/control-tower-landing-zone.ts @@ -180,7 +180,7 @@ export class ControlTowerLandingZone extends Construct { const logArchiveAccountId = props.logArchiveAccountId || logArchiveAccount?.accountId; const securityAuditAccountId = props.securityAuditAccountId || securityAuditAccount?.accountId; - const loggingKmsKey = props.loggingBucketKmsKeyArn + const loggingKmsKey: kms.Key | undefined = props.loggingBucketKmsKeyArn ? undefined : new kms.Key(this, 'LoggingKmsKey', { description: 'KMS key for Control Tower logging bucket encryption', From e75b0a2486433f4a923904f0d4463cb60e418536 Mon Sep 17 00:00:00 2001 From: spensireli <5614310+spensireli@users.noreply.github.com> Date: Thu, 31 Jul 2025 22:22:12 -0400 Subject: [PATCH 2/3] feat: kms grants --- src/control-tower/control-tower-landing-zone.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/control-tower/control-tower-landing-zone.ts b/src/control-tower/control-tower-landing-zone.ts index 7554cba..6b72b7c 100644 --- a/src/control-tower/control-tower-landing-zone.ts +++ b/src/control-tower/control-tower-landing-zone.ts @@ -180,7 +180,7 @@ export class ControlTowerLandingZone extends Construct { const logArchiveAccountId = props.logArchiveAccountId || logArchiveAccount?.accountId; const securityAuditAccountId = props.securityAuditAccountId || securityAuditAccount?.accountId; - const loggingKmsKey: kms.Key | undefined = props.loggingBucketKmsKeyArn + const loggingKmsKey = props.loggingBucketKmsKeyArn ? undefined : new kms.Key(this, 'LoggingKmsKey', { description: 'KMS key for Control Tower logging bucket encryption', From 3f0b82a859814ebd90640d688cd82512b5b7cdd5 Mon Sep 17 00:00:00 2001 From: spensireli <5614310+spensireli@users.noreply.github.com> Date: Sat, 2 Aug 2025 09:38:47 -0400 Subject: [PATCH 3/3] feat: controltower role --- API.md | 29 +++++++++++++++++++ .../control-tower-landing-zone.ts | 20 ++++++++++++- 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/API.md b/API.md index 256f712..f7b8b84 100644 --- a/API.md +++ b/API.md @@ -343,6 +343,7 @@ Any object. | **Name** | **Type** | **Description** | | --- | --- | --- | | node | constructs.Node | The tree node. | +| controlTowerAdminRole | aws-cdk-lib.aws_iam.IRole | The Control Tower admin role (either created or referenced). | | landingZoneArn | string | The ARN of the Control Tower landing zone. | | landingZoneId | string | The unique identifier of the Control Tower landing zone. | | logArchiveAccountId | string | The AWS account ID of the log archive account. | @@ -363,6 +364,18 @@ The tree node. --- +##### `controlTowerAdminRole`Required + +```typescript +public readonly controlTowerAdminRole: IRole; +``` + +- *Type:* aws-cdk-lib.aws_iam.IRole + +The Control Tower admin role (either created or referenced). + +--- + ##### `landingZoneArn`Required ```typescript @@ -1123,6 +1136,7 @@ const controlTowerLandingZoneProps: ControlTowerLandingZoneProps = { ... } | --- | --- | --- | | controlTowerStackName | string | The name of the Control Tower stack. | | accessLoggingBucketRetentionDays | number | Number of days to retain access logs in the access logging bucket. | +| createControlTowerAdminRole | boolean | Whether to create the Control Tower admin role if it doesn't already exist. | | governedRegions | string[] | AWS regions where Control Tower governance will be applied. | | logArchiveAccountEmail | string | Email address for the log archive account. | | logArchiveAccountId | string | AWS account ID for the log archive account. | @@ -1163,6 +1177,21 @@ Number of days to retain access logs in the access logging bucket. --- +##### `createControlTowerAdminRole`Optional + +```typescript +public readonly createControlTowerAdminRole: boolean; +``` + +- *Type:* boolean +- *Default:* true + +Whether to create the Control Tower admin role if it doesn't already exist. + +If false, the construct will attempt to reference an existing role with the name 'AWSControlTowerAdmin'. + +--- + ##### `governedRegions`Optional ```typescript diff --git a/src/control-tower/control-tower-landing-zone.ts b/src/control-tower/control-tower-landing-zone.ts index 6b72b7c..068647c 100644 --- a/src/control-tower/control-tower-landing-zone.ts +++ b/src/control-tower/control-tower-landing-zone.ts @@ -98,6 +98,12 @@ export interface ControlTowerLandingZoneProps { * @default - AWS best practices organizational structure */ readonly organizationStructure?: { [key: string]: OrganizationalUnit }; + /** + * Whether to create the Control Tower admin role if it doesn't already exist. + * If false, the construct will attempt to reference an existing role with the name 'AWSControlTowerAdmin'. + * @default - true + */ + readonly createControlTowerAdminRole?: boolean; } /** @@ -127,6 +133,8 @@ export class ControlTowerLandingZone extends Construct { public readonly logArchiveAccountId?: string; /** The AWS account ID of the security audit account */ public readonly securityAuditAccountId?: string; + /** The Control Tower admin role (either created or referenced) */ + public readonly controlTowerAdminRole: iam.IRole; /** * Creates a new Control Tower Landing Zone. @@ -225,6 +233,15 @@ export class ControlTowerLandingZone extends Construct { }, }; + const controlTowerAdminRole = props.createControlTowerAdminRole !== false + ? new iam.Role(this, 'ControlTowerAdminRole', { + roleName: 'AWSControlTowerAdmin', + assumedBy: new iam.ServicePrincipal('controltower.amazonaws.com'), + managedPolicies: [ + iam.ManagedPolicy.fromAwsManagedPolicyName('AWSControlTowerAdmin'), + ], + }) + : iam.Role.fromRoleName(this, 'ControlTowerAdminRole', 'AWSControlTowerAdmin'); const baseManifest = { governedRegions: props.governedRegions ?? [Stack.of(this).region], @@ -251,7 +268,7 @@ export class ControlTowerLandingZone extends Construct { version: '3.3', }); - // Hacky but I want to be sure the KMS key and grant exists before moving forward. + // Hacky but I want to be sure the KMS key and grant exists before moving forward. if (logArchiveAccount) { landingZone.node.addDependency(logArchiveAccount); if (loggingKmsKey) { @@ -268,6 +285,7 @@ export class ControlTowerLandingZone extends Construct { landingZone.node.addDependency(loggingKmsKey); } + this.controlTowerAdminRole = controlTowerAdminRole; this.landingZoneArn = landingZone.attrArn; this.landingZoneId = landingZone.attrLandingZoneIdentifier; this.loggingKmsKeyArn = props.loggingBucketKmsKeyArn ?? loggingKmsKey?.keyArn;