AWSTemplateFormatVersion: "2010-09-09" Description: This template creates 2 Windows 2019 Active Directory Domain Controllers into private subnets in separate Availability Zones inside a VPC. The default Domain Administrator password will be the one retrieved from the instance. For adding members to the domain, ensure that they are launched into the domain member security group created by this template and then configure them to use the AD instances fixed private IP addresses as the DNS server. **WARNING** This template creates Amazon EC2 Windows instance and related resources. You will be billed for the AWS resources used if you create a stack from this template. (qs-1raijs4re) Metadata: AWS::CloudFormation::Interface: ParameterGroups: - Label: default: Network Configuration Parameters: - VPCCIDR - VPCID - PrivateSubnet1ID - PrivateSubnet2ID - Label: default: Amazon EC2 Configuration Parameters: - ADServer1InstanceType - ADServer1NetBIOSName - ADServer1PrivateIP - ADServer2InstanceType - ADServer2NetBIOSName - ADServer2PrivateIP - KeyPairName - WS2019FULLBASE - Label: default: Microsoft Active Directory Configuration Parameters: - AdministratorPassword - DomainAdminPassword - DomainAdminUser - DomainDNSName - DomainNetBIOSName - RestoreModePassword - Label: default: S3 Bucket Configuration Parameters: - CreateS3Buckets - GPOS3BucketName - LogsS3BucketName - CRLS3BucketName - Label: default: Backup and Patching Configuration Parameters: - BackupPolicy - PatchWindow - Label: default: AWS Quick Start Configuration Parameters: - QSS3BucketName - QSS3KeyPrefix - QSS3BucketRegion - CreateS3Buckets - Label: default: Encryption Configuration Parameters: - KMSKeyId ParameterLabels: AdministratorPassword: default: Domain Administator Password ADServer1InstanceType: default: Domain Controller 1 Instance Type ADServer1NetBIOSName: default: Domain Controller 1 NetBIOS Name ADServer1PrivateIP: default: Domain Controller 1 Private IP Address ADServer2InstanceType: default: Domain Controller 2 Instance Type ADServer2NetBIOSName: default: Domain Controller 2 NetBIOS Name ADServer2PrivateIP: default: Domain Controller 2 Private IP Address BackupPolicy: defualt: Backup Policy CreateS3Buckets: default: Create new S3 buckets CRLS3BucketName: default: Bucket Name for the Certificate Revocation Lists (CRLs) DomainAdminPassword: default: Alternate Domain Admin Password DomainAdminUser: default: Alternate Domain Admin User Name DomainDNSName: default: Domain DNS Name DomainNetBIOSName: default: Domain NetBIOS Name GPOS3BucketName: default: Bucket Name for Uploading Group Policy Object (GPO) Packages KeyPairName: default: Key Pair Name KMSKeyId: default: KMS Key Id LogsS3BucketName: default: Bucket Name for Storing Access Log Files PatchWindow: default: Patch Window PrivateSubnet1ID: default: Private Subnet 1 ID PrivateSubnet2ID: default: Private Subnet 2 ID RestoreModePassword: default: AD Restore Mode Password # SourceLocation: # default: Source Location S3 URL # SourceLocationBucket: # default: Source Location Bucket ARN QSS3BucketName: default: Quick Start S3 Bucket Name QSS3BucketRegion: default: Quick Start S3 Bucket Region QSS3KeyPrefix: default: Quick Start S3 Key Prefix VPCCIDR: default: VPC CIDR VPCID: default: VPC ID WS2019FULLBASE: default: SSM Parameter Value to grab the lastest AMI ID Parameters: AdministratorPassword: AllowedPattern: (?=^.{6,255}$)((?=.*\d)(?=.*[A-Z])(?=.*[a-z])|(?=.*\d)(?=.*[^A-Za-z0-9])(?=.*[a-z])|(?=.*[^A-Za-z0-9])(?=.*[A-Z])(?=.*[a-z])|(?=.*\d)(?=.*[A-Z])(?=.*[^A-Za-z0-9]))^.* Description: Password for the Domain Administrator Account. User name is Administrator. Must be at least 8 characters containing letters, numbers and symbols MaxLength: "32" MinLength: "8" NoEcho: "true" Type: String ADServer1InstanceType: AllowedValues: - t2.large - t3.large - m4.large - m4.xlarge - m4.2xlarge - m4.4xlarge - m5.large - m5.xlarge - m5.2xlarge - m5.4xlarge Default: m4.xlarge Description: Amazon EC2 instance type for the first Active Directory instance Type: String ADServer1NetBIOSName: AllowedPattern: '[a-zA-Z0-9\-]+' Default: DC1 Description: NetBIOS name of the first Active Directory server (up to 15 characters) MaxLength: "15" MinLength: "1" Type: String ADServer1PrivateIP: AllowedPattern: ^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$ Default: 10.0.0.10 Description: Fixed private IP for the first Active Directory server located in Availability Zone 1 Type: String ADServer2InstanceType: AllowedValues: - t2.large - t3.large - m4.large - m4.xlarge - m4.2xlarge - m4.4xlarge - m5.large - m5.xlarge - m5.2xlarge - m5.4xlarge Default: m4.xlarge Description: Amazon EC2 instance type for the second Active Directory instance Type: String ADServer2NetBIOSName: AllowedPattern: '[a-zA-Z0-9\-]+' Default: DC2 Description: NetBIOS name of the second Active Directory server (up to 15 characters) MaxLength: "15" MinLength: "1" Type: String ADServer2PrivateIP: AllowedPattern: ^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$ Default: 10.0.32.10 Description: Fixed private IP for the second Active Directory server located in Availability Zone 2 Type: String BackupPolicy: AllowedValues: - "standard" - "dev" - "none" Default: "standard" Description: Select a valid backup policy to employ. Type: String CreateS3Buckets: AllowedValues: - "yes" - "no" Default: "yes" Description: Create new S3 buckets. Type: String CRLS3BucketName: AllowedPattern: ^[0-9a-zA-Z]+([0-9a-zA-Z-]*[0-9a-zA-Z])*$ ConstraintDescription: CRL bucket name can include numbers, lowercase letters, uppercase letters, and hyphens (-). It cannot start or end with a hyphen (-). Default: ad-crl-bucket Description: S3 bucket name for storing Certificate Revocation Lists (CRLs) and Certificates. CRL bucket name can include numbers, lowercase letters, uppercase letters, and hyphens (-). It cannot start or end with a hyphen (-). Type: String DomainAdminPassword: AllowedPattern: (?=^.{6,255}$)((?=.*\d)(?=.*[A-Z])(?=.*[a-z])|(?=.*\d)(?=.*[^A-Za-z0-9])(?=.*[a-z])|(?=.*[^A-Za-z0-9])(?=.*[A-Z])(?=.*[a-z])|(?=.*\d)(?=.*[A-Z])(?=.*[^A-Za-z0-9]))^.* Description: Password for the Alternate Admin User. This is different tnan "Administrator". Must be at least 8 characters containing letters, numbers and symbols MaxLength: "32" MinLength: "8" NoEcho: "true" Type: String DomainAdminUser: AllowedPattern: "[a-zA-Z0-9]*" Default: Admin Description: User name for the account that will be added as Domain Administrator. This is separate from the default "Administrator" account MaxLength: "25" MinLength: "5" Type: String DomainDNSName: AllowedPattern: '[a-zA-Z0-9\-]+\..+' Default: example.com Description: Fully qualified domain name (FQDN) of the forest root domain e.g. example.com MaxLength: "255" MinLength: "2" Type: String DomainNetBIOSName: AllowedPattern: '[a-zA-Z0-9\-]+' Default: example Description: NetBIOS name of the domain (up to 15 characters) for users of earlier versions of Windows e.g. EXAMPLE MaxLength: "15" MinLength: "1" Type: String GPOS3BucketName: AllowedPattern: ^[0-9a-zA-Z]+([0-9a-zA-Z-]*[0-9a-zA-Z])*$ ConstraintDescription: GPO bucket name can include numbers, lowercase letters, uppercase letters, and hyphens (-). It cannot start or end with a hyphen (-). Default: crl-bucket Description: S3 bucket name for upload the DISA STIG GPO packages. CRL bucket name can include numbers, lowercase letters, uppercase letters, and hyphens (-). It cannot start or end with a hyphen (-). Type: String KeyPairName: Description: Public/private key pairs allow you to securely connect to your instance after it launches Type: AWS::EC2::KeyPair::KeyName KMSKeyId: Type: String Description: KMS Key Id for use with encryption where appropriate LambdaZipsBucket: AllowedPattern: ^[0-9a-zA-Z]+([0-9a-zA-Z-]*[0-9a-zA-Z])*$ ConstraintDescription: S3 bucket name where Lambda function zip files are location. Can include numbers, lowercase letters, uppercase letters, and hyphens (-). It cannot start or end with a hyphen (-). Default: "quickstart-lambda-zips" Description: S3 bucket name for storing Lambda fuction zip files. The bucket name can include numbers, lowercase letters, uppercase letters, and hyphens (-). It cannot start or end with a hyphen (-). Type: String LogsS3BucketName: AllowedPattern: ^[0-9a-zA-Z]+([0-9a-zA-Z-]*[0-9a-zA-Z])*$ ConstraintDescription: Logs bucket name can include numbers, lowercase letters, uppercase letters, and hyphens (-). It cannot start or end with a hyphen (-). Default: gpo-bucket Description: S3 bucket name for storing logs. Logs bucket name can include numbers, lowercase letters, uppercase letters, and hyphens (-). It cannot start or end with a hyphen (-). Type: String PatchWindow: AllowedValues: - "prod" - "test" - "manual" Default: "prod" Description: Select a valid patch window schema. Type: String PrivateSubnet1ID: Description: ID of the private subnet 1 in Availability Zone 1 (e.g., subnet-a0246dcd) Type: String Default: "" PrivateSubnet2ID: Description: ID of the private subnet 2 in Availability Zone 2 (e.g., subnet-a0246dcd) Type: String Default: "" RestoreModePassword: AllowedPattern: (?=^.{6,255}$)((?=.*\d)(?=.*[A-Z])(?=.*[a-z])|(?=.*\d)(?=.*[^A-Za-z0-9])(?=.*[a-z])|(?=.*[^A-Za-z0-9])(?=.*[A-Z])(?=.*[a-z])|(?=.*\d)(?=.*[A-Z])(?=.*[^A-Za-z0-9]))^.* Description: Password for the domain admin user. Must be at least 8 characters containing letters, numbers and symbols MaxLength: "32" MinLength: "8" NoEcho: "true" Type: String S3VPCEndpointId: Type: String Description: S3 VPC endpoint ID to allow access for certificates and CRLs # SourceLocation: # Description: Full S3 URL which references the location (directory) where source files may be found (with no trailing "/"). # Type: String # SourceLocationBucket: # Description: Enter the "Source Location URL" Bucket ARN. This is to ensure appropriate access is allowed. # Type: String QSS3BucketName: AllowedPattern: ^[0-9a-zA-Z]+([0-9a-zA-Z-]*[0-9a-zA-Z])*$ ConstraintDescription: Quick Start bucket name can include numbers, lowercase letters, uppercase letters, and hyphens (-). It cannot start or end with a hyphen (-). Default: aws-quickstart Description: S3 bucket name for the Quick Start assets. Quick Start bucket name can include numbers, lowercase letters, uppercase letters, and hyphens (-). It cannot start or end with a hyphen (-). Type: String QSS3BucketRegion: Default: us-east-1 Description: The AWS Region where the Quick Start S3 bucket (QSS3BucketName) is hosted. When using your own bucket, you must specify this value. Type: String QSS3KeyPrefix: AllowedPattern: ^[0-9a-zA-Z-/]*$ ConstraintDescription: Quick Start key prefix can include numbers, lowercase letters, uppercase letters, hyphens (-), and forward slash (/). Default: quickstart-cmmc-microsoft-activedirectory/ Description: S3 key prefix for the Quick Start assets. Quick Start key prefix can include numbers, lowercase letters, uppercase letters, hyphens (-), and forward slash (/). Type: String VPCCIDR: AllowedPattern: ^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/(1[6-9]|2[0-8]))$ ConstraintDescription: CIDR block parameter must be in the form x.x.x.x/16-28 Default: 10.0.0.0/16 Description: CIDR Block for the VPC Type: String VPCID: Description: ID of the VPC (e.g., vpc-0343606e) Type: String Default: "" WS2019FULLBASE: Type: AWS::EC2::Image::Id Rules: SubnetsInVPC: Assertions: - Assert: !EachMemberIn - !ValueOfAll - AWS::EC2::Subnet::Id - VpcId - !RefAll "AWS::EC2::VPC::Id" AssertDescription: All subnets must in the VPC Conditions: UsingDefaultBucket: !Equals [!Ref QSS3BucketName, "aws-quickstart"] NewS3Buckets: !Equals [!Ref CreateS3Buckets, "yes"] Resources: ADAutomationLogs: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub "/cwe/ActiveDirectory/ADSetup-SSMAutomation/${AWS::StackName}" RetentionInDays: 30 ADLogsBucket: Type: AWS::S3::Bucket DeletionPolicy: Retain Condition: NewS3Buckets Properties: AccessControl: LogDeliveryWrite BucketName: !Ref LogsS3BucketName BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: "aws:kms" KMSMasterKeyID: !Ref KMSKeyId LoggingConfiguration: LogFilePrefix: !Sub "s3-${LogsS3BucketName}" ADLogsBucketPolicy: Type: AWS::S3::BucketPolicy Condition: NewS3Buckets Properties: Bucket: !Ref ADLogsBucket PolicyDocument: Statement: - Action: - "s3:PutObject" - "s3:GetObjectAcl" Effect: Allow Principal: Service: - s3.amazonaws.com AWS: !Sub ${AWS::AccountId} Resource: !Sub "arn:${AWS::Partition}:s3:::${ADLogsBucket}/*" DHCPOptions: Type: AWS::EC2::DHCPOptions DependsOn: - DomainController1 - DomainController2 Properties: DomainName: !Ref "DomainDNSName" DomainNameServers: - !Ref "ADServer1PrivateIP" - !Ref "ADServer2PrivateIP" Tags: - Key: Domain Value: !Ref "DomainDNSName" VPCDHCPOptionsAssociation: Type: AWS::EC2::VPCDHCPOptionsAssociation Properties: VpcId: !Ref "VPCID" DhcpOptionsId: !Ref "DHCPOptions" ActiveDirectoryDS: Type: AWS::SSM::Document Properties: DocumentType: Automation Content: schemaVersion: "0.3" description: "Deploy AD with SSM Automation" # Role that is utilized to perform the steps within the Automation Document. In this case to be able to Signal CFN and Describe Instances. assumeRole: "{{AutomationAssumeRole}}" # Gathering parameters needed to configure DCs parameters: ADServer1NetBIOSName: default: "DC1" description: "NetBIOS name of the first Active Directory server (up to 15 characters)" type: "String" ADServer1PrivateIP: default: "10.0.0.10" description: "Fixed private IP for the first Active Directory server located in Availability Zone 1" type: "String" ADServer2NetBIOSName: default: "DC2" description: "NetBIOS name of the first Active Directory server (up to 15 characters)" type: "String" ADServer2PrivateIP: default: "10.0.32.10" description: "Fixed private IP for the first Active Directory server located in Availability Zone 1" type: "String" VPCCIDR: default: "10.0.0.0/16" description: "CIDR block for private subnet 1 located in Availability Zone 1." type: "String" ADAdminSecParamName: description: "AWS Secrets Manager ARN that has Password and User namer for the domain administrator." type: "String" ADAltUserSecParamName: description: "AWS Secrets Manager ARN for the account that will be added as Domain Administrator. This is separate from the default Administrator account" type: "String" RestoreModeSecParamName: description: "AWS Secrets Manager ARN for the Restore Mode Password" type: "String" DomainDNSName: default: "example.com" description: "Fully qualified domain name (FQDN) of the forest root domain e.g. example.com" type: "String" DomainNetBIOSName: default: "example" description: "NetBIOS name of the domain (up to 15 characters) for users of earlier versions of Windows e.g. EXAMPLE" type: "String" QSS3BucketName: default: "aws-quickstart" description: "S3 bucket name for the Quick Start assets. Quick Start bucket name can include numbers, lowercase letters, uppercase letters, and hyphens (-). It cannot start or end with a hyphen (-)." type: "String" QSS3BucketRegion: default: "us-east-1" description: "The AWS Region where the Quick Start S3 bucket (QSS3BucketName) is hosted. When using your own bucket, you must specify this value." type: "String" QSS3KeyPrefix: default: "quickstart-microsoft-activedirectory/" description: "S3 key prefix for the Quick Start assets. Quick Start key prefix can include numbers, lowercase letters, uppercase letters, hyphens (-), and forward slash (/)." type: "String" StackName: default: "" description: "Stack Name Input for cfn resource signal" type: "String" URLSuffix: default: "amazonaws.com" description: "AWS URL suffix" type: "String" AutomationAssumeRole: default: "" description: "(Optional) The ARN of the role that allows Automation to perform the actions on your behalf." type: "String" mainSteps: # This step grabs the Instance IDs for both nodes that will be configured as DCs and Instance IDs for the for next steps. - name: "dcsInstanceIds" action: aws:executeAwsApi onFailure: "step:signalfailure" nextStep: "dcsInstallDscModules" inputs: Service: ec2 Api: DescribeInstances Filters: - Name: "tag:Name" Values: ["{{ADServer1NetBIOSName}}", "{{ADServer2NetBIOSName}}"] - Name: "tag:aws:cloudformation:stack-name" Values: ["{{StackName}}"] - Name: "instance-state-name" Values: ["running"] outputs: - Name: InstanceIds Selector: "$.Reservations..Instances..InstanceId" Type: "StringList" # Installs needed Powershell DSC Modules and components on both nodes. - name: "dcsInstallDscModules" action: "aws:runCommand" onFailure: "step:signalfailure" nextStep: "dcsLCMConfig" inputs: DocumentName: "AWS-RunRemoteScript" InstanceIds: - "{{dcsInstanceIds.InstanceIds}}" CloudWatchOutputConfig: CloudWatchOutputEnabled: "true" CloudWatchLogGroupName: !Ref "ADAutomationLogs" Parameters: sourceType: "S3" sourceInfo: !Sub - '{"path": "https://${S3Bucket}.s3.${S3Region}.{{URLSuffix}}/{{QSS3KeyPrefix}}scripts/install-ad-modules.ps1"}' - S3Bucket: !If - UsingDefaultBucket - !Sub "${QSS3BucketName}-${AWS::Region}" - !Ref QSS3BucketName S3Region: !If - UsingDefaultBucket - !Ref AWS::Region - !Ref QSS3BucketRegion commandLine: "./install-ad-modules.ps1" # Configures Local Configuration Manager on each of the nodes. - name: "dcsLCMConfig" action: "aws:runCommand" onFailure: "step:signalfailure" nextStep: "dc1InstanceId" inputs: DocumentName: "AWS-RunRemoteScript" InstanceIds: - "{{dcsInstanceIds.InstanceIds}}" CloudWatchOutputConfig: CloudWatchOutputEnabled: "true" CloudWatchLogGroupName: !Ref "ADAutomationLogs" Parameters: sourceType: "S3" sourceInfo: !Sub - '{"path": "https://${S3Bucket}.s3.${S3Region}.{{URLSuffix}}/{{QSS3KeyPrefix}}scripts/LCM-Config.ps1"}' - S3Bucket: !If - UsingDefaultBucket - !Sub "${QSS3BucketName}-${AWS::Region}" - !Ref QSS3BucketName S3Region: !If - UsingDefaultBucket - !Ref AWS::Region - !Ref QSS3BucketRegion commandLine: "./LCM-Config.ps1" # This step grabs the Instance ID for the node that will be configured as the first DC in the new domain. - name: "dc1InstanceId" action: aws:executeAwsApi onFailure: "step:signalfailure" nextStep: "createDC1Mof" inputs: Service: ec2 Api: DescribeInstances Filters: - Name: "tag:Name" Values: ["{{ADServer1NetBIOSName}}"] - Name: "tag:aws:cloudformation:stack-name" Values: ["{{StackName}}"] - Name: "instance-state-name" Values: ["running"] outputs: - Name: InstanceId Selector: "$.Reservations[0].Instances[0].InstanceId" Type: "String" # Generates MOF file on first DC Node to be processed by LCM. - name: "createDC1Mof" action: "aws:runCommand" onFailure: "step:signalfailure" nextStep: "configDC1" inputs: DocumentName: "AWS-RunRemoteScript" InstanceIds: - "{{dc1InstanceId.InstanceId}}" CloudWatchOutputConfig: CloudWatchOutputEnabled: "true" CloudWatchLogGroupName: !Ref "ADAutomationLogs" Parameters: sourceType: "S3" sourceInfo: !Sub - '{"path": "https://${S3Bucket}.s3.${S3Region}.{{URLSuffix}}/{{QSS3KeyPrefix}}scripts/ConfigDC1-SSM.ps1"}' - S3Bucket: !If - UsingDefaultBucket - !Sub "${QSS3BucketName}-${AWS::Region}" - !Ref QSS3BucketName S3Region: !If - UsingDefaultBucket - !Ref AWS::Region - !Ref QSS3BucketRegion commandLine: "./ConfigDC1-SSM.ps1 -ADServer1NetBIOSName {{ADServer1NetBIOSName}} -DomainNetBIOSName {{DomainNetBIOSName}} -DomainDNSName {{DomainDNSName}} -ADAdminSSMParam {{ADAdminSecParamName}} -ADAltUserSSMParam {{ADAltUserSecParamName}} -RestoreModeSSMParam {{RestoreModeSecParamName}} -SiteName {{global:REGION}} -VPCCIDR {{VPCCIDR}} -Region {{global:REGION}}" # Kicks off DSC Configuration and loops\reboots until Node matches Configuration defined in MOF file. - name: "configDC1" action: aws:runCommand onFailure: "step:signalfailure" nextStep: "dc2InstanceId" inputs: DocumentName: AWS-RunPowerShellScript InstanceIds: - "{{dc1InstanceId.InstanceId}}" CloudWatchOutputConfig: CloudWatchOutputEnabled: "true" CloudWatchLogGroupName: !Ref "ADAutomationLogs" Parameters: commands: - | function DscStatusCheck () { $LCMState = (Get-DscLocalConfigurationManager).LCMState if ($LCMState -eq 'PendingConfiguration' -Or $LCMState -eq 'PendingReboot') { 'returning 3010, should continue after reboot' exit 3010 } else { 'Completed' } } Start-DscConfiguration 'C:\CWE\ConfigDC1' -Wait -Verbose -Force DscStatusCheck # This step grabs the Instance ID for the node that will be configured as the second DC in the new domain. - name: "dc2InstanceId" action: aws:executeAwsApi onFailure: "step:signalfailure" nextStep: "createDC2Mof" inputs: Service: ec2 Api: DescribeInstances Filters: - Name: "tag:Name" Values: ["{{ADServer2NetBIOSName}}"] - Name: "tag:aws:cloudformation:stack-name" Values: ["{{StackName}}"] - Name: "instance-state-name" Values: ["running"] outputs: - Name: InstanceId Selector: "$.Reservations[0].Instances[0].InstanceId" Type: "String" # Generates MOF file on second DC Node to be processed by LCM. - name: "createDC2Mof" action: "aws:runCommand" onFailure: "step:signalfailure" nextStep: "configDC2" inputs: DocumentName: "AWS-RunRemoteScript" InstanceIds: - "{{dc2InstanceId.InstanceId}}" CloudWatchOutputConfig: CloudWatchOutputEnabled: "true" CloudWatchLogGroupName: !Ref "ADAutomationLogs" Parameters: sourceType: "S3" sourceInfo: !Sub - '{"path": "https://${S3Bucket}.s3.${S3Region}.{{URLSuffix}}/{{QSS3KeyPrefix}}scripts/ConfigDC2-SSM.ps1"}' - S3Bucket: !If - UsingDefaultBucket - !Sub "${QSS3BucketName}-${AWS::Region}" - !Ref QSS3BucketName S3Region: !If - UsingDefaultBucket - !Ref AWS::Region - !Ref QSS3BucketRegion commandLine: "./ConfigDC2-SSM.ps1 -ADServer2NetBIOSName {{ADServer2NetBIOSName}} -DomainNetBIOSName {{DomainNetBIOSName}} -DomainDNSName {{DomainDNSName}} -ADServer1PrivateIP {{ADServer1PrivateIP}} -ADAdminSSMParam {{ADAdminSecParamName}} -Region {{global:REGION}}" # Kicks off DSC Configuration and loops\reboots until Node matches Configuration defined in MOF file. - name: "configDC2" action: aws:runCommand onFailure: "step:signalfailure" nextStep: "DnsConfig" inputs: DocumentName: AWS-RunPowerShellScript InstanceIds: - "{{dc2InstanceId.InstanceId}}" CloudWatchOutputConfig: CloudWatchOutputEnabled: "true" CloudWatchLogGroupName: !Ref "ADAutomationLogs" Parameters: commands: - | function DscStatusCheck () { $LCMState = (Get-DscLocalConfigurationManager).LCMState if ($LCMState -eq 'PendingConfiguration' -Or $LCMState -eq 'PendingReboot') { 'returning 3010, should continue after reboot' exit 3010 } else { 'Completed' } } Start-DscConfiguration 'C:\CWE\ConfigDC2' -Wait -Verbose -Force DscStatusCheck # Ensure that AD servers point to themselves for DNS - name: "DnsConfig" action: "aws:runCommand" onFailure: "step:signalfailure" inputs: DocumentName: "AWS-RunRemoteScript" InstanceIds: - "{{dc2InstanceId.InstanceId}}" CloudWatchOutputConfig: CloudWatchOutputEnabled: "true" CloudWatchLogGroupName: !Ref "ADAutomationLogs" Parameters: sourceType: S3 sourceInfo: !Sub - '{"path": "https://${S3Bucket}.s3.${S3Region}.{{URLSuffix}}/{{QSS3KeyPrefix}}scripts/Dns-Config.ps1"}' - S3Bucket: !If - UsingDefaultBucket - !Sub "${QSS3BucketName}-${AWS::Region}" - !Ref QSS3BucketName S3Region: !If - UsingDefaultBucket - !Ref AWS::Region - !Ref QSS3BucketRegion commandLine: "./Dns-Config.ps1 -ADServer1NetBIOSName {{ADServer1NetBIOSName}} -ADServer2NetBIOSName {{ADServer2NetBIOSName}} -ADServer1PrivateIP {{ADServer1PrivateIP}} -ADServer2PrivateIP {{ADServer2PrivateIP}} -DomainDNSName {{DomainDNSName}} -ADAdminSecParam {{ADAdminSecParamName}} -Region {{global:REGION}}" # Determines if CFN Needs to be Signaled or if Work flow should just end - name: CFNSignalEnd action: aws:branch inputs: Choices: - NextStep: signalsuccess Not: Variable: "{{StackName}}" StringEquals: "" - NextStep: sleepend Variable: "{{StackName}}" StringEquals: "" # If all steps complete successfully signals CFN of Success - name: "signalsuccess" action: "aws:executeAwsApi" isEnd: True inputs: Service: cloudformation Api: SignalResource LogicalResourceId: "DomainController2" StackName: "{{StackName}}" Status: SUCCESS UniqueId: "{{dc2InstanceId.InstanceId}}" # If CFN Signl Not Needed this sleep ends work flow - name: "sleepend" action: "aws:sleep" isEnd: True inputs: Duration: PT1S # If any steps fails signals CFN of Failure - name: "signalfailure" action: "aws:executeAwsApi" inputs: Service: cloudformation Api: SignalResource LogicalResourceId: "DomainController2" StackName: "{{StackName}}" Status: FAILURE UniqueId: "{{dc2InstanceId.InstanceId}}" ADDSRole: Type: AWS::IAM::Role Properties: Policies: - PolicyDocument: Version: "2012-10-17" Statement: - Action: - s3:GetObject - s3:ListBucket - s3:GetEncryptionConfiguration Resource: - !Sub "arn:${AWS::Partition}:s3:::${QSS3BucketName}" - !Sub "arn:${AWS::Partition}:s3:::${QSS3BucketName}/*" Effect: Allow - Action: - kms:Decrypt Resource: !Sub "arn:${AWS::Partition}:kms:${AWS::Region}:${AWS::AccountId}:key/${KMSKeyId}" Effect: Allow PolicyName: CWE-S3-Policy - PolicyDocument: Version: "2012-10-17" Statement: - Action: - s3:GetObject Resource: - !Sub "arn:${AWS::Partition}:s3:::aws-ssm-${AWS::Region}/*" - !Sub "arn:${AWS::Partition}:s3:::aws-windows-downloads-${AWS::Region}/*" - !Sub "arn:${AWS::Partition}:s3:::amazon-ssm-${AWS::Region}/*" - !Sub "arn:${AWS::Partition}:s3:::amazon-ssm-packages-${AWS::Region}/*" - !Sub "arn:${AWS::Partition}:s3:::${AWS::Region}-birdwatcher-prod/*" - !Sub "arn:${AWS::Partition}:s3:::patch-baseline-snapshot-${AWS::Region}/*" Effect: Allow PolicyName: ssm-custom-s3-policy - PolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Action: - cloudformation:SignalResource Resource: !Sub "arn:${AWS::Partition}:cloudformation:${AWS::Region}:${AWS::AccountId}:stack/${AWS::StackName}/*" - Effect: Allow Action: - ec2:DescribeInstances - ec2:DescribeInstanceStatus - ssm:* Resource: "*" # according to docs, you must use * with ec2:DescribeInstances and ec2:DescribeInstanceStatus PolicyName: AD-SSM-Automation Path: / AssumeRolePolicyDocument: Statement: - Action: - sts:AssumeRole Principal: Service: - ec2.amazonaws.com - ssm.amazonaws.com Effect: Allow Version: "2012-10-17" ADSsmPassRolePolicy: Type: AWS::IAM::Policy Properties: PolicyName: AD-SSM-PassRole PolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Action: - iam:PassRole Resource: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/${ADDSRole}" Roles: - !Ref "ADDSRole" ADServerRole: Type: AWS::IAM::Role Properties: Policies: - PolicyDocument: Version: "2012-10-17" Statement: - Action: - s3:GetObject - s3:ListBucket - s3:GetEncryptionConfiguration Resource: - !Sub "arn:${AWS::Partition}:s3:::${QSS3BucketName}" - !Sub "arn:${AWS::Partition}:s3:::${QSS3BucketName}/*" Effect: Allow PolicyName: cwe-s3-policy - PolicyDocument: Version: "2012-10-17" Statement: - Action: - s3:GetObject Resource: - !Sub "arn:${AWS::Partition}:s3:::aws-ssm-${AWS::Region}/*" - !Sub "arn:${AWS::Partition}:s3:::aws-windows-downloads-${AWS::Region}/*" - !Sub "arn:${AWS::Partition}:s3:::amazon-ssm-${AWS::Region}/*" - !Sub "arn:${AWS::Partition}:s3:::amazon-ssm-packages-${AWS::Region}/*" - !Sub "arn:${AWS::Partition}:s3:::${AWS::Region}-birdwatcher-prod/*" - !Sub "arn:${AWS::Partition}:s3:::patch-baseline-snapshot-${AWS::Region}/*" Effect: Allow PolicyName: ssm-custom-s3-policy - PolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Action: - secretsmanager:GetSecretValue - secretsmanager:DescribeSecret Resource: - !Ref "ADAdminSecrets" - !Ref "RestoreModeSecrets" - !Ref "ADAltUserSecrets" - Effect: Allow Action: - kms:Decrypt Resource: - !Sub "arn:${AWS::Partition}:kms:${AWS::Region}:${AWS::AccountId}:key/${KMSKeyId}" - Effect: Allow Action: - ssm:StartAutomationExecution Resource: !Sub "arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:automation-definition/*" PolicyName: AD-SSM-Secrets - PolicyDocument: Version: "2012-10-17" Statement: - Action: - ssm:SendCommand Resource: !Sub arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:document/${GPOImportSSMDoc} Effect: Allow PolicyName: "GPO-SendSSMCommand-SSM" - PolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Action: - iam:PassRole Resource: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/${ADDSRole}" PolicyName: AD-SSM-PassRole Path: / ManagedPolicyArns: - !Sub "arn:${AWS::Partition}:iam::aws:policy/AmazonSSMManagedInstanceCore" - !Sub "arn:${AWS::Partition}:iam::aws:policy/CloudWatchAgentServerPolicy" AssumeRolePolicyDocument: Statement: - Action: - sts:AssumeRole Principal: Service: - ec2.amazonaws.com Effect: Allow Version: "2012-10-17" ADServerProfile: Type: AWS::IAM::InstanceProfile Properties: Roles: - !Ref "ADServerRole" Path: / ADAdminSecrets: Type: AWS::SecretsManager::Secret Properties: Description: Administrator Password for AD KmsKeyId: !Ref "KMSKeyId" Name: !Sub "ADAdministratorSecret-${AWS::StackName}" SecretString: !Sub '{"username":"Administrator","password":"${AdministratorPassword}"}' RestoreModeSecrets: Type: AWS::SecretsManager::Secret Properties: Description: Restore Mode Password for AD KmsKeyId: !Ref "KMSKeyId" Name: !Sub "RestoreModeSecrets-${AWS::StackName}" SecretString: !Sub '{"username":"RestoreMode","password":"${RestoreModePassword}"}' ADAltUserSecrets: Type: AWS::SecretsManager::Secret Properties: Description: Alternate AD Admin User from AD KmsKeyId: !Ref "KMSKeyId" Name: !Sub "ADAltUserSecrets-${AWS::StackName}" SecretString: !Sub '{"username":"${DomainAdminUser}","password":"${DomainAdminPassword}"}' DomainController1: Type: AWS::EC2::Instance Properties: ImageId: !Ref "WS2019FULLBASE" IamInstanceProfile: !Ref "ADServerProfile" InstanceType: !Ref "ADServer1InstanceType" SubnetId: !Ref "PrivateSubnet1ID" Tags: - Key: Name Value: !Ref "ADServer1NetBIOSName" - Key: BackupPolicy Value: !Ref "BackupPolicy" - Key: PatchWindow Value: !Ref "PatchWindow" BlockDeviceMappings: - DeviceName: /dev/sda1 Ebs: Encrypted: true KmsKeyId: !Ref "KMSKeyId" VolumeSize: 100 VolumeType: gp2 SecurityGroupIds: - !Ref "DomainControllersSG" PrivateIpAddress: !Ref "ADServer1PrivateIP" KeyName: !Ref "KeyPairName" DomainController2: Type: AWS::EC2::Instance DependsOn: DomainController1 CreationPolicy: ResourceSignal: Timeout: PT60M Count: 1 Properties: ImageId: !Ref "WS2019FULLBASE" IamInstanceProfile: !Ref "ADServerProfile" InstanceType: !Ref "ADServer2InstanceType" SubnetId: !Ref "PrivateSubnet2ID" Tags: - Key: Name Value: !Ref "ADServer2NetBIOSName" - Key: BackupPolicy Value: !Ref "BackupPolicy" - Key: PatchWindow Value: !Ref "PatchWindow" BlockDeviceMappings: - DeviceName: /dev/sda1 Ebs: Encrypted: true KmsKeyId: !Ref "KMSKeyId" VolumeSize: 100 VolumeType: gp2 SecurityGroupIds: - !Ref "DomainControllersSG" PrivateIpAddress: !Ref "ADServer2PrivateIP" KeyName: !Ref "KeyPairName" UserData: !Base64 Fn::Join: - "" - - "\n" - "Start-SSMAutomationExecution -DocumentName " - !Sub '"${ActiveDirectoryDS}"' - " -Parameter @{" - '"ADServer1NetBIOSName"=' - !Sub '"${ADServer1NetBIOSName}"' - ';"ADServer2NetBIOSName"=' - !Sub '"${ADServer2NetBIOSName}"' - ';"ADServer1PrivateIP"=' - !Sub '"${ADServer1PrivateIP}"' - ';"ADServer2PrivateIP"=' - !Sub '"${ADServer2PrivateIP}"' - ';"DomainDNSName"=' - !Sub '"${DomainDNSName}"' - ';"DomainNetBIOSName"=' - !Sub '"${DomainNetBIOSName}"' - ';"VPCCIDR"=' - !Sub '"${VPCCIDR}"' - ';"QSS3BucketName"=' - !If [ UsingDefaultBucket, !Sub '"${QSS3BucketName}-${AWS::Region}"', !Sub '"${QSS3BucketName}"', ] - ';"QSS3BucketRegion"=' - !If [ UsingDefaultBucket, !Sub '"${AWS::Region}"', !Sub '"${QSS3BucketRegion}"', ] - ';"QSS3KeyPrefix"=' - !Sub '"${QSS3KeyPrefix}"' - ';"ADAdminSecParamName"=' - !Sub '"${ADAdminSecrets}"' - ';"ADAltUserSecParamName"=' - !Sub '"${ADAltUserSecrets}"' - ';"RestoreModeSecParamName"=' - !Sub '"${RestoreModeSecrets}"' - ';"StackName"=' - !Sub '"${AWS::StackName}"' - ';"URLSuffix"=' - !Sub '"${AWS::URLSuffix}"' - ';"AutomationAssumeRole"=' - !Sub '"arn:${AWS::Partition}:iam::${AWS::AccountId}:role/${ADDSRole}"' - "}" - "\n" - "\n" DomainControllersSG: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: Domain Controllers Security Group VpcId: Ref: VPCID SecurityGroupIngress: - IpProtocol: tcp FromPort: 5985 ToPort: 5985 CidrIp: !Ref VPCCIDR - IpProtocol: tcp FromPort: 80 ToPort: 80 CidrIp: !Ref VPCCIDR - IpProtocol: tcp FromPort: 53 ToPort: 53 CidrIp: !Ref VPCCIDR - IpProtocol: udp FromPort: 53 ToPort: 53 CidrIp: !Ref VPCCIDR - IpProtocol: tcp FromPort: 3389 ToPort: 3389 CidrIp: !Ref VPCCIDR - IpProtocol: udp FromPort: 123 ToPort: 123 SourceSecurityGroupId: !Ref DomainMembersSG - IpProtocol: tcp FromPort: 135 ToPort: 135 SourceSecurityGroupId: !Ref DomainMembersSG - IpProtocol: tcp FromPort: 9389 ToPort: 9389 SourceSecurityGroupId: !Ref DomainMembersSG - IpProtocol: udp FromPort: 138 ToPort: 138 SourceSecurityGroupId: !Ref DomainMembersSG - IpProtocol: tcp FromPort: 445 ToPort: 445 SourceSecurityGroupId: !Ref DomainMembersSG - IpProtocol: udp FromPort: 445 ToPort: 445 SourceSecurityGroupId: !Ref DomainMembersSG - IpProtocol: udp FromPort: 464 ToPort: 464 SourceSecurityGroupId: !Ref DomainMembersSG - IpProtocol: tcp FromPort: 464 ToPort: 464 SourceSecurityGroupId: !Ref DomainMembersSG - IpProtocol: tcp FromPort: 49152 ToPort: 65535 SourceSecurityGroupId: !Ref DomainMembersSG - IpProtocol: udp FromPort: 49152 ToPort: 65535 SourceSecurityGroupId: !Ref DomainMembersSG - IpProtocol: tcp FromPort: 389 ToPort: 389 SourceSecurityGroupId: !Ref DomainMembersSG - IpProtocol: udp FromPort: 389 ToPort: 389 SourceSecurityGroupId: !Ref DomainMembersSG - IpProtocol: tcp FromPort: 636 ToPort: 636 SourceSecurityGroupId: !Ref DomainMembersSG - IpProtocol: tcp FromPort: 3268 ToPort: 3268 SourceSecurityGroupId: !Ref DomainMembersSG - IpProtocol: tcp FromPort: 3269 ToPort: 3269 SourceSecurityGroupId: !Ref DomainMembersSG - IpProtocol: tcp FromPort: 9389 ToPort: 9389 SourceSecurityGroupId: !Ref DomainMembersSG - IpProtocol: tcp FromPort: 88 ToPort: 88 SourceSecurityGroupId: !Ref DomainMembersSG - IpProtocol: udp FromPort: 88 ToPort: 88 SourceSecurityGroupId: !Ref DomainMembersSG - IpProtocol: udp FromPort: 5355 ToPort: 5355 SourceSecurityGroupId: !Ref DomainMembersSG - IpProtocol: udp FromPort: 137 ToPort: 137 SourceSecurityGroupId: !Ref DomainMembersSG - IpProtocol: tcp FromPort: 139 ToPort: 139 SourceSecurityGroupId: !Ref DomainMembersSG - IpProtocol: tcp FromPort: 5722 ToPort: 5722 SourceSecurityGroupId: !Ref DomainMembersSG - IpProtocol: icmp FromPort: -1 ToPort: -1 SourceSecurityGroupId: !Ref DomainMembersSG SecurityGroupEgress: - IpProtocol: "-1" FromPort: 0 ToPort: 65535 CidrIp: 0.0.0.0/0 Description: Allow all outbound traffic DomainMembersSG: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: Domain Members VpcId: !Ref VPCID SecurityGroupIngress: - IpProtocol: tcp FromPort: 5985 ToPort: 5985 CidrIp: !Ref VPCCIDR - IpProtocol: tcp FromPort: 3389 ToPort: 3389 CidrIp: !Ref VPCCIDR - IpProtocol: tcp FromPort: 53 ToPort: 53 CidrIp: !Sub ${ADServer2PrivateIP}/32 - IpProtocol: udp FromPort: 53 ToPort: 53 CidrIp: !Sub ${ADServer2PrivateIP}/32 - IpProtocol: tcp FromPort: 53 ToPort: 53 CidrIp: !Sub ${ADServer1PrivateIP}/32 - IpProtocol: udp FromPort: 53 ToPort: 53 CidrIp: !Sub ${ADServer1PrivateIP}/32 - IpProtocol: tcp FromPort: 49152 ToPort: 65535 CidrIp: !Sub ${ADServer1PrivateIP}/32 - IpProtocol: udp FromPort: 49152 ToPort: 65535 CidrIp: !Sub ${ADServer1PrivateIP}/32 - IpProtocol: tcp FromPort: 49152 ToPort: 65535 CidrIp: !Sub ${ADServer2PrivateIP}/32 - IpProtocol: udp FromPort: 49152 ToPort: 65535 CidrIp: !Sub ${ADServer2PrivateIP}/32 - IpProtocol: tcp FromPort: 88 ToPort: 88 CidrIp: !Sub ${ADServer1PrivateIP}/32 - IpProtocol: udp FromPort: 88 ToPort: 88 CidrIp: !Sub ${ADServer1PrivateIP}/32 - IpProtocol: tcp FromPort: 88 ToPort: 88 CidrIp: !Sub ${ADServer2PrivateIP}/32 - IpProtocol: udp FromPort: 88 ToPort: 88 CidrIp: !Sub ${ADServer2PrivateIP}/32 - IpProtocol: tcp FromPort: 445 ToPort: 445 CidrIp: !Sub ${ADServer1PrivateIP}/32 - IpProtocol: udp FromPort: 445 ToPort: 445 CidrIp: !Sub ${ADServer1PrivateIP}/32 - IpProtocol: tcp FromPort: 445 ToPort: 445 CidrIp: !Sub ${ADServer2PrivateIP}/32 - IpProtocol: udp FromPort: 445 ToPort: 445 CidrIp: !Sub ${ADServer2PrivateIP}/32 - IpProtocol: tcp FromPort: 389 ToPort: 389 CidrIp: !Sub ${ADServer1PrivateIP}/32 - IpProtocol: udp FromPort: 389 ToPort: 389 CidrIp: !Sub ${ADServer1PrivateIP}/32 - IpProtocol: tcp FromPort: 389 ToPort: 389 CidrIp: !Sub ${ADServer2PrivateIP}/32 - IpProtocol: udp FromPort: 389 ToPort: 389 CidrIp: !Sub ${ADServer2PrivateIP}/32 - IpProtocol: tcp FromPort: 636 ToPort: 636 CidrIp: !Sub ${ADServer1PrivateIP}/32 - IpProtocol: tcp FromPort: 636 ToPort: 636 CidrIp: !Sub ${ADServer2PrivateIP}/32 SecurityGroupEgress: - IpProtocol: "-1" FromPort: 0 ToPort: 65535 CidrIp: 0.0.0.0/0 Description: Allow all outbound traffic DCSecurityGroupIngress: Type: AWS::EC2::SecurityGroupIngress Properties: Description: Security Group Rule between Domain Controllers GroupId: !Ref DomainControllersSG IpProtocol: "-1" FromPort: -1 ToPort: -1 SourceSecurityGroupId: !Ref DomainControllersSG # SQS queue that S3 will send messages to when a new # file is uploaded to the GPOS3Bucket GPOQueue: Type: AWS::SQS::Queue Properties: KmsMasterKeyId: !Ref KMSKeyId RedrivePolicy: deadLetterTargetArn: !GetAtt GPODLQueue.Arn maxReceiveCount: 3 # Queue policy for the GPOQueue that allows the S3 bucket to send # messages to it GPOQueuePolicy: Type: AWS::SQS::QueuePolicy Properties: PolicyDocument: Statement: - Action: - "sqs:SendMessage" Effect: Allow Principal: Service: "s3.amazonaws.com" Condition: ArnLike: "aws:SourceArn": !Sub "arn:${AWS::Partition}:s3:*:*:${GPOS3BucketName}" StringEquals: "aws:SourceAccount": !Ref "AWS::AccountId" Resource: !GetAtt GPOQueue.Arn Queues: - !Ref GPOQueue # Dead-letter queue for the GPOQueue GPODLQueue: Type: AWS::SQS::Queue Properties: KmsMasterKeyId: !Ref KMSKeyId # S3 bucket where users will upload the DISA STIG GPO packages # to trigger the GPO import process GPOS3Bucket: Type: AWS::S3::Bucket DeletionPolicy: Retain Condition: NewS3Buckets DependsOn: GPOQueuePolicy Properties: AccessControl: Private BucketName: !Ref GPOS3BucketName BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: "aws:kms" KMSMasterKeyID: !Ref KMSKeyId NotificationConfiguration: QueueConfigurations: - Event: "s3:ObjectCreated:Put" Queue: !GetAtt GPOQueue.Arn LoggingConfiguration: DestinationBucketName: !Ref ADLogsBucket LogFilePrefix: !Sub "s3-${GPOS3BucketName}" # S3 bucket where Certificate Revocation Lists (CRL) are stored # Also used to store and transfer certificate files between the Root CA # and Subordinate CA during setup CRLS3Bucket: Type: AWS::S3::Bucket DeletionPolicy: Retain Condition: NewS3Buckets Properties: AccessControl: Private BucketName: !Ref CRLS3BucketName LoggingConfiguration: DestinationBucketName: !Ref LogsS3BucketName LogFilePrefix: !Sub "s3-${CRLS3BucketName}" # Bucket policy for the CRL S3 bucket # Allows anything using the S3 VPC Endpoint to read/write objects CRLS3BucketPolicy: Type: AWS::S3::BucketPolicy Condition: NewS3Buckets Properties: Bucket: !Ref CRLS3Bucket PolicyDocument: Statement: - Action: - "s3:GetObject" - "s3:PutObject" Effect: Allow Principal: "*" Condition: StringEquals: "aws:SourceVpce": !Ref S3VPCEndpointId Resource: !Sub "arn:${AWS::Partition}:s3:::${CRLS3Bucket}/*" # Bucket policy for the GPOS3Bucket that only allows access to # read objects if coming through the S3 VPC Endpoint GPOS3BucketPolicy: Type: AWS::S3::BucketPolicy Condition: NewS3Buckets Properties: Bucket: !Ref GPOS3Bucket PolicyDocument: Statement: - Action: - "s3:GetObject" Effect: Allow Principal: "*" Condition: StringEquals: "aws:SourceVpce": !Ref S3VPCEndpointId Resource: !Sub "arn:${AWS::Partition}:s3:::${GPOS3Bucket}/*" # CloudWatch log group for the GPO import process GPOImportLogs: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub "/cwe/ActiveDirectory/GPOImport-SSMCommand/${AWS::StackName}" RetentionInDays: 30 # SMM Run Command document that will download the GPO package from S3 # and then unzip and import the GPO backups GPOImportSSMDoc: Type: "AWS::SSM::Document" Properties: Name: Import-DISA-STIG-GPOs DocumentType: Command Content: schemaVersion: "2.2" description: "Import/update DISA STIG GPOs" # Gathering parameters needed to configure DCs parameters: GPOS3BucketName: description: "S3 bucket name for the GPO packages. The bucket name can include numbers, lowercase letters, uppercase letters, and hyphens (-). It cannot start or end with a hyphen (-)." type: "String" GPOS3Key: description: "The key for the DISA STIG GPO package in the S3 bucket" type: "String" Region: description: "The region that the stack is created in" type: "String" mainSteps: # Downloads the GPO package and imports the GPO policy backups into Active Directory - name: "importGPOs" action: aws:runPowerShellScript inputs: runCommand: - | $tempDir = 'C:\Temp' $gpoDir = 'C:\GPOPackages' $tempPath = Join-Path -Path $tempDir -ChildPath {{GPOS3Key}} Read-S3Object -BucketName {{GPOS3BucketName}} -Key {{GPOS3Key}} -File $tempPath -Endpoint 'https://s3-fips.{{Region}}.amazonaws.com' -Region {{Region}} $gpoPath = Join-Path -Path $gpoDir -ChildPath ([IO.Path]::GetFileNameWithoutExtension('{{GPOS3Key}}')) Expand-Archive $tempPath -DestinationPath $gpoPath -Force $gposPath = Join-Path -Path $gpoPath -ChildPath 'DoD Windows Server 2019 MS and DC V1R4\GPOs' $manifestPath = Join-Path -Path $gposPath -ChildPath 'manifest.xml' [xml]$manifest = Get-Content $manifestPath $backups = $manifest.Backups.BackupInst foreach($backup in $backups) { Import-GPO -BackupId $backup.ID.InnerText -TargetName $backup.GPODisplayName.InnerText -Path $gposPath -CreateIfNeeded } # Lambda function that triggers the SSM run command for importing the # GPO packages when a new package is uploaded to S3 # Also sets the appropriate SSM Parameter Store values GPOSendImportCommand: Type: AWS::Lambda::Function Properties: Description: "Runs an SSM document to install/update the DISA STIG GPOs in Active Directory" Handler: index.handler Role: !GetAtt GPOSendImportCommandExecutionRole.Arn Runtime: nodejs12.x Environment: Variables: INSTANCE_ID: !Ref DomainController1 Code: ZipFile: | const AWS = require("aws-sdk"); const ssm = new AWS.SSM(); exports.handler = async (event) => { try { await Promise.all( event.Records.map(async (record) => { const body = JSON.parse(record.body); await Promise.all(body.Records.map(async (rec) => { await sendSSMCommand(rec); })); }) ); } catch (error) { console.log(error); } } async function sendSSMCommand(record) { try { const srcBucket = record.s3.bucket.name; const srcKey = decodeURIComponent(record.s3.object.key.replace(/\+/g, " ")); const region = process.env.AWS_REGION; const instanceID = process.env.INSTANCE_ID; const params = { DocumentName: "Import-DISA-STIG-GPOs", InstanceIds: [instanceID], Parameters: { 'GPOS3BucketName': [srcBucket], 'GPOS3Key': [srcKey], 'Region': [region] }, TimeoutSeconds: 3600 } await ssm.sendCommand(params).promise(); await updatePackageCommand(srcKey); await updateFailureCommand(); } catch (error) { console.log(error); } } async function updatePackageCommand(fileName) { try { fileName = fileName.replace('.zip', ''); const ssmPackageParams = { Name: "GPOLastPackage", Value: fileName, Overwrite: true } await ssm.putParameter(ssmPackageParams).promise(); } catch (error) { console.log(error); } } async function updateFailureCommand() { try { const ssmFailureParams = { Name: "GPOFailureSent", Value: "false", Overwrite: true } await ssm.putParameter(ssmFailureParams).promise(); } catch (error) { console.log(error); } } # Maps the GPOQueue as an event source for the GPOSendImportCommand Lambda function GPOSendImportCommandEventSource: Type: "AWS::Lambda::EventSourceMapping" Properties: Enabled: true EventSourceArn: !GetAtt GPOQueue.Arn FunctionName: !GetAtt GPOSendImportCommand.Arn # IAM execution role for the GPOSendImport command Lambda function GPOSendImportCommandExecutionRole: Type: AWS::IAM::Role Properties: Path: / AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Principal: Service: - lambda.amazonaws.com Action: - "sts:AssumeRole" ManagedPolicyArns: - !Sub "arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" - !Sub "arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaSQSQueueExecutionRole" Policies: - PolicyName: "GPO-SendSSMCommand-SSM" PolicyDocument: Version: "2012-10-17" Statement: - Action: - ssm:SendCommand Resource: - !Sub arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:document/${GPOImportSSMDoc} - !Sub arn:${AWS::Partition}:ec2:${AWS::Region}:${AWS::AccountId}:instance/${DomainController1} Effect: Allow - Action: - ssm:PutParameter Resource: - !Sub arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${GPOLastPackageParameter} - !Sub arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${GPOFailureSentParameter} Effect: Allow - PolicyName: "GPO-SQS-Read" PolicyDocument: Version: "2012-10-17" Statement: - Action: - kms:Decrypt Resource: !Sub "arn:${AWS::Partition}:kms:${AWS::Region}:${AWS::AccountId}:key/${KMSKeyId}" Effect: Allow # SNS topic for GPO package notifications GPONotificationTopic: Type: AWS::SNS::Topic Properties: TopicName: DISA-STIG-GPO_PackageDownloadNotifications KmsMasterKeyId: !Ref KMSKeyId # SSM Parameter Store parameter that holds the value of the last # GPO package that was imported GPOLastPackageParameter: Type: "AWS::SSM::Parameter" Properties: Type: String Description: "The last DISA STIG GPO package that was uploaded and imported into AD" Value: "U_STIG_GPO_Package_July_2020" Name: GPOLastPackage # SSM Parameter Store parameter that holds the value that # identifies whether a failure notification has been sent # This to prevent spamming of failure notifications GPOFailureSentParameter: Type: "AWS::SSM::Parameter" Properties: Type: String Description: "Identifies whether or not a notifcation has been sent when a new DISA STIG GRO package could not be found via automation" Value: "false" Name: GPOFailureSent # IAM execution role for the GPOCheckNewPackages Lambda function GPOCheckForNewPackagesExecutionRole: Type: AWS::IAM::Role Properties: Path: / AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Principal: Service: - lambda.amazonaws.com Action: - "sts:AssumeRole" ManagedPolicyArns: - !Sub "arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" Policies: - PolicyName: "GPO-SSMParameters-ReadWrite" PolicyDocument: Version: "2012-10-17" Statement: - Action: - ssm:PutParameter - ssm:GetParameter Resource: - !Sub arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${GPOLastPackageParameter} - !Sub arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${GPOFailureSentParameter} Effect: Allow - PolicyName: "GPO-PublishSNS" PolicyDocument: Version: "2012-10-17" Statement: - Action: - sns:Publish Resource: - !Ref GPONotificationTopic Effect: Allow - PolicyName: "GPO-KMS" PolicyDocument: Version: "2012-10-17" Statement: - Action: - kms:GenerateDataKey - kms:Decrypt Resource: !Sub "arn:${AWS::Partition}:kms:${AWS::Region}:${AWS::AccountId}:key/${KMSKeyId}" Effect: Allow # Lambda function that checks the DISA website for a new GPO package # and sends the appropriate notifications to the SNS topic GPOCheckForNewPackages: Type: AWS::Lambda::Function Properties: Description: "Checks the DISA website for new DISA STIG GPO packages and sends notifications" Handler: index.handler Role: !GetAtt GPOCheckForNewPackagesExecutionRole.Arn Runtime: nodejs12.x KmsKeyArn: !Sub arn:${AWS::Partition}:kms:${AWS::Region}:${AWS::AccountId}:key/${KMSKeyId} Environment: Variables: LAST_PKG_PARAM: !Ref GPOLastPackageParameter FAILURE_SENT_PARAM: !Ref GPOFailureSentParameter TOPIC_ARN: !Ref GPONotificationTopic Code: S3Bucket: !Ref LambdaZipsBucket S3Key: !Sub "${QSS3KeyPrefix}archives/GPOPackagesFunction.zip" # CloudWatch rule that will execute the Lambda function on a set interval # Interval is the 1st day of the month in a new quarter GPOScheduledRule: Type: "AWS::Events::Rule" Properties: Description: "Scheduled cron job for executing the Lambda function that checks the DISA website for new STIG GPO packages" Name: TriggerGPOPackageCheck ScheduleExpression: "cron(0 0 1 JAN,APR,JULY,OCT ? *)" State: ENABLED Targets: - Arn: !GetAtt GPOCheckForNewPackages.Arn Id: GPOPackageFunction # Lambda permission that allows the GPOScheduledRule to invoke it GPOScheduleRulePermission: Type: AWS::Lambda::Permission Properties: Action: "lambda:InvokeFunction" FunctionName: !Ref GPOCheckForNewPackages Principal: "events.amazonaws.com" SourceArn: !GetAtt GPOScheduledRule.Arn Outputs: DomainAdmin: Value: !Join - "" - - !Ref "DomainNetBIOSName" - \ - !Ref "DomainAdminUser" Description: Domain administrator account DomainControllersSGID: Value: !Ref "DomainControllersSG" Description: Domain Controllers Security Group ID DomainMemberSGID: Value: !Ref "DomainMembersSG" Description: Domain Member Security Group ID SecretsArn: Value: !Ref "ADAdminSecrets" Postdeployment: Description: See the deployment guide for post-deployment information. Value: https://aws-quickstart.github.io/quickstart-cmmc-microsoft-activedirectory/