--- AWSTemplateFormatVersion: 2010-09-09 Description: Provides nesting for required stacks to deploy a full sample web application with reverse proxy, ELBs, IAM, and other resources (for demonstration/POC/testing) (qs-1sflpub1k) Metadata: cfn-lint: config: ignore_checks: - W9006 AWS::CloudFormation::Interface: ParameterGroups: - Label: default: Server DNS configuration Parameters: - AWSPublicFQDN - AWSHostedZoneID - SSLCertificateARN - Label: default: AWS Quick Start Configuration Parameters: - QSS3BucketName - QSS3BucketRegion - QSS3KeyPrefix - QSTagKey - QSTagValue - Label: default: Security Configuration Parameters: - DBPassword - KeyPairName - LogBucket - Label: default: Network Configuration Parameters: - BastionSecurityGroup - PublicSubnetIds - PrivateSubnetAppIds - PrivateSubnetDBIds - SourceCidr - VpcCidr - VpcId ParameterLabels: AWSHostedZoneID: default: DNS Hosted Zone ID AWSPublicFQDN: default: Full DNS Name for Web App BastionSecurityGroup: default: Bastion Host Security Group ID DBPassword: default: Database Password KeyPairName: default: EC2 Key Pair LogBucket: default: Log Bucket PublicSubnetIds: default: Public Subnet IDs PrivateSubnetAppIds: default: App Tier Private Subnet IDs PrivateSubnetDBIds: default: DB Tier Private Subnet IDs QSS3BucketName: default: Quick Start S3 Bucket Name QSS3BucketRegion: default: Quick Start S3 bucket region QSS3KeyPrefix: default: Quick Start S3 Key Prefix QSTagKey: default: Quick Start Tag key QSTagValue: default: Quick Start Tag value SSLCertificateARN: default: SSL Certificate ARN (Requires matching DNS name) SourceCidr: default: Source CIDR for Access VpcCidr: default: VPC CIDR Block VpcId: default: VPC ID Parameters: AWSHostedZoneID: Description: DNS Zone ID to contain the server's DNS entry. Type: String AWSPublicFQDN: Description: Web app will be reachable at this address. Type: String BastionSecurityGroup: Type: AWS::EC2::SecurityGroup::Id Description: EC2 Security Group for the bastion host. DBPassword: Description: Mixed alphanumeric and must be between 8 and 28 characters and contain at least one capital letter NoEcho: true Type: String MinLength: 8 MaxLength: 28 AllowedPattern: '[a-zA-Z0-9!^*\-_+]*' ConstraintDescription: Can only contain alphanumeric characters or the following special characters !^*-_+, between 8 and 28 characters KeyPairName: Type: AWS::EC2::KeyPair::KeyName Description: Name of an existing EC2 KeyPair to enable SSH access to app servers ConstraintDescription: Must be the name of an existing EC2 Key Pair LogBucket: Type: String Description: Bucket to write logs to PublicSubnetIds: Type: List Description: Public subnets in your VPC PrivateSubnetAppIds: Description: Private Subnet Ids to launch App Servers in Type: List PrivateSubnetDBIds: Description: Private Subnet Ids to launch DB Servers in Type: List 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. This string 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-hitrust-csf 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 QSTagKey: Type: String Description: Tag key to identify resources from this Quick Start. QSTagValue: Type: String Description: Tag value to identify resources from this Quick Start. SourceCidr: 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])(\\/([0-9]|[1-2][0-9]|3[0-2]))$" ConstraintDescription: Must be a valid IP CIDR range of the form x.x.x.x/x. Description: IP address/range to allow access from Type: String SSLCertificateARN: Type: String Default: "" Description: The Amazon Resource Name for the existing SSL cert you wish to use; empty for none 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])(\\/([0-9]|[1-2][0-9]|3[0-2]))$" ConstraintDescription: Must be a valid IP CIDR range of the form x.x.x.x/x. Type: String Description: VPC CIDR VpcId: Type: AWS::EC2::VPC::Id Description: Amazon VPC ID Mappings: # Storage: # Defaults: # ArchiveTier: GLACIER # WebContent: # LogPrefix: web-content/ Elb: Defaults: HealthCheckGracePeriod: 300 Database: Defaults: User: HitrustExampleDbUser Name: HiTrustWordpressDB InstanceClass: GovCloud: db.t2.small NonGovCloud: db.t3.small Ec2: Defaults: InstanceSize: t3.small AWSAMIRegionMap: AMI: AMZNLINUX2HVM: amzn2-ami-hvm-2.0.20211103.0-x86_64-gp2 # ap-east-1: # AMZNLINUX2HVM: ami-570c7726 ap-northeast-1: AMZNLINUX2HVM: ami-0e60b6d05dc38ff11 ap-northeast-2: AMZNLINUX2HVM: ami-0a5a6128e65676ebb ap-south-1: AMZNLINUX2HVM: ami-0f1fb91a596abf28d ap-southeast-1: AMZNLINUX2HVM: ami-024221a59c9020e72 ap-southeast-2: AMZNLINUX2HVM: ami-043e0add5c8665836 ca-central-1: AMZNLINUX2HVM: ami-0730e12069ab20c26 eu-central-1: AMZNLINUX2HVM: ami-030e490c34394591b eu-north-1: AMZNLINUX2HVM: ami-08b0de3847e24ff84 eu-west-1: AMZNLINUX2HVM: ami-09d4a659cdd8677be eu-west-2: AMZNLINUX2HVM: ami-0fc15d50d39e4503c eu-west-3: AMZNLINUX2HVM: ami-05f0a049e7aeb407c sa-east-1: AMZNLINUX2HVM: ami-03a343b9045171cec us-east-1: AMZNLINUX2HVM: ami-04ad2567c9e3d7893 us-east-2: AMZNLINUX2HVM: ami-0dd0ccab7e2801812 # us-gov-east-1: # AMZNLINUX2HVM: ami-a2d938d3 # us-gov-west-1: # AMZNLINUX2HVM: ami-e9a9d388 us-west-1: AMZNLINUX2HVM: ami-0074ef78ecb07948c us-west-2: AMZNLINUX2HVM: ami-00be885d550dcee43 Conditions: GovCloudCondition: !Or [!Equals [!Ref "AWS::Region", 'us-gov-west-1'], !Equals [!Ref "AWS::Region", 'us-gov-east-1']] GovCloudEastCondition: !Equals [!Ref "AWS::Region", 'us-gov-east-1'] GovCloudWestCondition: !Equals [!Ref "AWS::Region", 'us-gov-west-1'] UsingDefaultBucket: !Equals [!Ref QSS3BucketName, 'aws-quickstart'] Resources: # Create the Elb ElbStack: Type: AWS::CloudFormation::Stack Properties: TemplateURL: !Sub - https://${S3Bucket}.s3.${S3Region}.${AWS::URLSuffix}/${QSS3KeyPrefix}templates/elb.template - S3Region: !If [UsingDefaultBucket, !Ref 'AWS::Region', !Ref QSS3BucketRegion] S3Bucket: !If [UsingDefaultBucket, !Sub '${QSS3BucketName}-${AWS::Region}', !Ref QSS3BucketName] Parameters: AWSHostedZoneID: !Ref AWSHostedZoneID AWSPublicFQDN: !Ref AWSPublicFQDN LogBucket: !Ref LogBucket PublicSubnetIds: !Join [',', !Ref PublicSubnetIds] QSS3BucketName: !Ref QSS3BucketName QSS3BucketRegion: !Ref QSS3BucketRegion QSS3KeyPrefix: !Ref QSS3KeyPrefix QSTagKey: !Ref QSTagKey QSTagValue: !Ref QSTagValue SourceCIDR: !Ref SourceCidr SSLCertificateARN: !Ref SSLCertificateARN VpcCidr: !Ref VpcCidr VpcId: !Ref VpcId Tags: - Key: !Ref QSTagKey Value: !Ref QSTagValue AppInstanceSecurityGroup: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: Enable HTTPS from the load balancer only SecurityGroupIngress: - IpProtocol: tcp FromPort: 443 ToPort: 443 SourceSecurityGroupId: !GetAtt ElbStack.Outputs.ElbSecurityGroup - IpProtocol: tcp FromPort: 22 ToPort: 22 SourceSecurityGroupId: !Ref BastionSecurityGroup Tags: - Key: !Ref QSTagKey Value: !Ref QSTagValue VpcId: !Ref VpcId # Configure Database AppDatabase: Type: AWS::CloudFormation::Stack Properties: TemplateURL: !Sub - https://${S3Bucket}.s3.${S3Region}.${AWS::URLSuffix}/${QSS3KeyPrefix}templates/database.template - S3Region: !If [UsingDefaultBucket, !Ref 'AWS::Region', !Ref QSS3BucketRegion] S3Bucket: !If [UsingDefaultBucket, !Sub '${QSS3BucketName}-${AWS::Region}', !Ref QSS3BucketName] Parameters: DBInstanceClass: !If [GovCloudCondition, !FindInMap [Database, InstanceClass, GovCloud], !FindInMap [Database, InstanceClass, NonGovCloud]] DBMasterUser: !FindInMap [Database, Defaults, User] DBName: !FindInMap [Database, Defaults, Name] DBPassword: !Ref DBPassword DBPrivateSubnets: !Join [",", !Ref PrivateSubnetDBIds] QSTagKey: !Ref QSTagKey QSTagValue: !Ref QSTagValue SecurityGroupAppInstance: !Ref AppInstanceSecurityGroup VpcId: !Ref VpcId Tags: - Key: !Ref QSTagKey Value: !Ref QSTagValue AppInstanceRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - ec2.amazonaws.com Action: - sts:AssumeRole Path: / Policies: - PolicyName: CreateTags PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - ec2:Describe* - ec2:CreateTags Resource: "*" - PolicyName: GetObj PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - s3:GetObject Resource: !Sub - arn:${AWS::Partition}:s3:::${S3Bucket}/${QSS3KeyPrefix}* - S3Bucket: !If [UsingDefaultBucket, !Sub '${QSS3BucketName}-${AWS::Region}', !Ref QSS3BucketName] AppInstanceProfile: Type: AWS::IAM::InstanceProfile Properties: Roles: - !Ref AppInstanceRole # Launch configuration AppLaunchConfig: Type: AWS::AutoScaling::LaunchConfiguration DependsOn: - AppDatabase Metadata: AWS::CloudFormation::Init: configSets: wordpress_install: - install_cfn - install_wordpress install_cfn: files: /etc/cfn/cfn-hup.conf: content: !Sub | [main] stack=${AWS::StackId} region=${AWS::Region} mode: '000400' owner: root group: root /etc/cfn/hooks.d/cfn-auto-reloader.conf: content: !Sub | [cfn-auto-reloader-hook] triggers=post.update path=Resources.AppLaunchConfig.Metadata.AWS::CloudFormation::Init action=/opt/aws/bin/cfn-init -v --stack ${AWS::StackName} --resource AppLaunchConfig --configsets wordpress_install --region ${AWS::Region} mode: '000400' owner: root group: root services: sysvinit: cfn-hup: enabled: true ensureRunning: true files: - /etc/cfn/cfn-hup.conf - /etc/cfn/hooks.d/cfn-auto-reloader.conf install_wordpress: packages: yum: mariadb: [] httpd: [] sources: /var/www/html: https://wordpress.org/latest.tar.gz files: /tmp/httpd/ssl.conf: content: | Listen 443 DocumentRoot /var/www/html LoadModule ssl_module /etc/httpd/modules/mod_ssl.so SSLEngine on SSLCertificateFile /etc/ssl/certs/common.crt SSLCertificateKeyFile /etc/ssl/private/common.key mode: '000755' owner: root group: root /var/www/html/wordpress/wp-config.php: content: !Sub - | > /home/ec2-user/tmp.log else echo "Creating /etc/ssl/private directory" >> /home/ec2-user/tmp.log mkdir /etc/ssl/private fi openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/ssl/private/common.key -out /etc/ssl/certs/common.crt -subj "/C=US/ST=Washington/L=Seattle/O=NonProductionTestCert/CN=NonProductionTestCert" /opt/aws/bin/cfn-init -v --stack ${AWS::StackName} --resource AppLaunchConfig --configsets wordpress_install --region ${AWS::Region} cp /tmp/httpd/ssl.conf /etc/httpd/conf.d/ssl.conf service httpd restart ###################################################################### # NOTE: UPDATE THESE VALUES ACCORDING TO THE COMPLIANCE BODY # ###################################################################### LANDING_PAGE="/var/www/html/landing.html" COMPLIANCE_BODY_LABEL="HITRUST" COMPLIANCE_SURVEY_LINK="hitrust" COMPLIANCE_MATRIX_FILENAME="HITRUST-CSF-Security-Controls-Mapping.xlsx" ###################################################################### # Download the landing page. aws s3 cp s3://${QSS3BucketName}/${QSS3KeyPrefix}assets/landing/landing.html $LANDING_PAGE # Replace relative image links with links to the production S3 source. sed -i 's|images|https://${QSS3BucketName}.${QSS3Region}.amazonaws.com/${QSS3KeyPrefix}assets/landing/images|g' $LANDING_PAGE # Inject the landing page branding label. sed -i "s|{compliance-body}|$COMPLIANCE_BODY_LABEL|g" $LANDING_PAGE # Inject the survey link parameter. sed -i "s|{compliance-body-survey-link}|$COMPLIANCE_SURVEY_LINK|g" $LANDING_PAGE # Inject the security control matrix file location. sed -i "s|{compliance-body-matrix}|https://${QSS3BucketName}.${QSS3Region}.amazonaws.com/${QSS3KeyPrefix}assets/$COMPLIANCE_MATRIX_FILENAME|g" $LANDING_PAGE /opt/aws/bin/cfn-signal -e $? --stack ${AWS::StackName} --resource AppASG --region ${AWS::Region} - QSS3Region: !If [ GovCloudEastCondition, s3-us-gov-east-1, !If [ GovCloudWestCondition, s3-us-gov-west-1, s3 ] ] AppASG: Type: AWS::AutoScaling::AutoScalingGroup Properties: HealthCheckGracePeriod: !FindInMap [ Elb, Defaults, HealthCheckGracePeriod ] HealthCheckType: ELB MinSize: '2' MaxSize: '4' Tags: - Key: !Ref QSTagKey Value: !Ref QSTagValue PropagateAtLaunch: true LaunchConfigurationName: !Ref AppLaunchConfig TargetGroupARNs: - !GetAtt ElbStack.Outputs.ElbTargetGroupHTTPS VPCZoneIdentifier: !Ref PrivateSubnetAppIds CreationPolicy: ResourceSignal: Count: 1 Timeout: PT15M AutoScalingUpApp: Type: AWS::AutoScaling::ScalingPolicy Properties: AdjustmentType: ChangeInCapacity AutoScalingGroupName: !Ref AppASG Cooldown: '1' ScalingAdjustment: 1 AutoScalingDownApp: Type: AWS::AutoScaling::ScalingPolicy Properties: AdjustmentType: ChangeInCapacity AutoScalingGroupName: !Ref AppASG Cooldown: '1' ScalingAdjustment: -1 CWAlarmHighCPUApp: Type: AWS::CloudWatch::Alarm Properties: EvaluationPeriods: 1 Statistic: Average Threshold: 50 AlarmDescription: Alarm if CPU too high or metric disappears indicating instance is down Period: 60 AlarmActions: - !Ref AutoScalingDownApp Namespace: AWS/EC2 Dimensions: - Name: AutoScalingGroupName Value: !Ref AppASG ComparisonOperator: GreaterThanThreshold MetricName: CPUUtilization CWAlarmLowCPUApp: Type: AWS::CloudWatch::Alarm Properties: EvaluationPeriods: 1 Statistic: Average Threshold: 10 AlarmDescription: Alarm if CPU too low, remove an app server Period: 60 AlarmActions: - !Ref AutoScalingDownApp Namespace: AWS/EC2 Dimensions: - Name: AutoScalingGroupName Value: !Ref AppASG ComparisonOperator: LessThanThreshold MetricName: CPUUtilization Outputs: LoadBalancerDNSName: Value: !GetAtt ElbStack.Outputs.ElbDns Description: DNS Name for the load balancer RecordSet: Value: !GetAtt ElbStack.Outputs.RecordSet Description: Domain Name of the Record Set ApplicationURL: Description: Public DNS name to reach cluster Value: !Sub https://${AWSPublicFQDN} LandingPageURL: Value: !Sub https://${AWSPublicFQDN}/landing.html Description: Landing Page