AWSTemplateFormatVersion: 2010-09-09 Description: >- Cloudformation Template to create a highly available SSH bastion host Metadata: AWS::CloudFormation::Interface: ParameterGroups: - Label: default: Network Configuration Parameters: - ParentVPCStack - RemoteAccessCIDR - Label: default: Amazon EC2 Configuration Parameters: - KeyPairName - BastionInstanceType - LogsRetentionInDays - NotificationList - Label: default: Linux Bastion Configuration Parameters: - BastionTenancy - EnableBanner - BastionBanner - EnableTCPForwarding - EnableX11Forwarding ParameterLabels: BastionTenancy: default: Bastion Tenancy BastionBanner: default: Bastion Banner BastionInstanceType: default: Bastion Instance Type EnableBanner: default: Enable Banner EnableTCPForwarding: default: Enable TCP Forwarding EnableX11Forwarding: default: Enable X11 Forwarding KeyPairName: default: Key Pair Name RemoteAccessCIDR: default: Allowed Bastion External Access CIDR AltInitScript: default: Custom Bootstrap Script OSImageOverride: default: AMI override NotificationList: default: SNS Notification Email Parameters: ParentVPCStack: Description: 'Stack name of parent VPC stack based on VPC-3AZs yaml template. Refer Cloudformation dashboard in AWS Console to get this.' Type: String NotificationList: Type: String Description: The Email notification list is used to configure a SNS topic for sending cloudwatch alarm notifications AllowedPattern: '^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$' ConstraintDescription: provide a valid email address. LogsRetentionInDays: Description: Specify the number of days you want to retain log events Type: Number Default: 14 AllowedValues: [1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1827, 3653] BastionBanner: Default: default Description: Banner text to display upon login. Leave at default or provide AWS S3 location for the file containing Banner text. Type: String ConstraintDescription: URL must begin with s3:// BastionTenancy: Description: 'VPC Tenancy in which bastion host will be launched. Options: ''dedicated'' or ''default''' Type: String Default: default AllowedValues: - dedicated - default BastionInstanceType: AllowedValues: - t3.micro - t3.small - t3.medium - t3.large - t3.xlarge - t3.2xlarge - m5.large - m5.xlarge - m5.2xlarge - m5.4xlarge Default: t3.micro Description: Amazon EC2 instance type for the bastion instance Type: String EnableBanner: AllowedValues: - 'true' - 'false' Default: 'true' Description: >- To include a banner to be displayed when connecting via SSH to the bastion, set this parameter to true Type: String EnableTCPForwarding: Type: String Description: Enable/Disable TCP Forwarding Default: 'false' AllowedValues: - 'true' - 'false' EnableX11Forwarding: Type: String Description: Enable/Disable X11 Forwarding Default: 'false' AllowedValues: - 'true' - 'false' KeyPairName: Description: >- Enter a Public/private key pair. If you do not have one in this AWS Region, create it before continuing Type: 'AWS::EC2::KeyPair::KeyName' RemoteAccessCIDR: 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: CIDR block parameter must be in the form x.x.x.x/x Description: Allowed CIDR block in the x.x.x.x/x format for external SSH access to the bastion host Type: String AltInitScript: AllowedPattern: ^http.*|^$ ConstraintDescription: URL must begin with http Description: Optional. Specify custom bootstrap script AWS S3 location to run during bastion host setup Default: '' Type: String OSImageOverride: Description: Optional. Specify a region specific image to use for the instance Type: String Default: '' Mappings: AWSAMIRegionMap: af-south-1: AMZNLINUX2: ami-0936d2754993c364e ap-northeast-1: AMZNLINUX2: ami-0ca38c7440de1749a ap-northeast-2: AMZNLINUX2: ami-0f2c95e9fe3f8f80e ap-northeast-3: AMZNLINUX2: ami-06e9ad0943b200859 ap-south-1: AMZNLINUX2: ami-010aff33ed5991201 ap-southeast-1: AMZNLINUX2: ami-02f26adf094f51167 ap-southeast-2: AMZNLINUX2: ami-0186908e2fdeea8f3 ca-central-1: AMZNLINUX2: ami-0101734ab73bd9e15 eu-central-1: AMZNLINUX2: ami-043097594a7df80ec me-south-1: AMZNLINUX2: ami-0880769bc15eeec4f ap-east-1: AMZNLINUX2: ami-0aca22cb23f122f27 eu-north-1: AMZNLINUX2: ami-050fdc53cf6ba8f7f eu-south-1: AMZNLINUX2: ami-0f447354763f0eaac eu-west-1: AMZNLINUX2: ami-063d4ab14480ac177 eu-west-2: AMZNLINUX2: ami-06dc09bb8854cbde3 eu-west-3: AMZNLINUX2: ami-0b3e57ee3b63dd76b sa-east-1: AMZNLINUX2: ami-05373777d08895384 us-east-1: AMZNLINUX2: ami-0d5eff06f840b45e9 us-gov-west-1: AMZNLINUX2: ami-0bbf3595bb2fb39ec us-gov-east-1: AMZNLINUX2: ami-0cc17d57bec8c6017 us-east-2: AMZNLINUX2: ami-077e31c4939f6a2f3 us-west-1: AMZNLINUX2: ami-04468e03c37242e1e us-west-2: AMZNLINUX2: ami-0cf6f5c8a62fa5da6 cn-north-1: AMZNLINUX2: ami-0c52e2685c7218558 cn-northwest-1: AMZNLINUX2: ami-05b9b6d6acf8ae9b6 LinuxAMINameMap: Amazon-Linux2-HVM: Code: AMZNLINUX2 OS: Amazon Conditions: UseAlternativeInitialization: !Not - !Equals - !Ref AltInitScript - '' UseOSImageOverride: !Not - !Equals - !Ref OSImageOverride - '' DefaultBanner: !Equals [!Ref BastionBanner, 'default'] Resources: EC2SNSTopic: Type: AWS::SNS::Topic Properties: KmsMasterKeyId: 'alias/aws/sns' Subscription: - Endpoint: !Ref NotificationList Protocol: email BastionMainLogGroup: Type: 'AWS::Logs::LogGroup' Properties: RetentionInDays: !Ref LogsRetentionInDays SSHMetricFilter: Type: 'AWS::Logs::MetricFilter' Properties: LogGroupName: !Ref BastionMainLogGroup FilterPattern: USER_LOGIN MetricTransformations: - MetricName: SSHCommandCount MetricValue: 1 MetricNamespace: !Join - / - - AWSQuickStart - !Ref 'AWS::StackName' BastionSecurityGroup: Type: 'AWS::EC2::SecurityGroup' Properties: GroupDescription: !Ref 'AWS::StackName' VpcId: {'Fn::ImportValue': !Sub '${ParentVPCStack}-VPC'} SecurityGroupIngress: - IpProtocol: tcp FromPort: '22' ToPort: '22' CidrIp: !Ref RemoteAccessCIDR - IpProtocol: icmp FromPort: '-1' ToPort: '-1' CidrIp: !Ref RemoteAccessCIDR Tags: - Key: Name Value: !Sub '${AWS::StackName}-BastionSecurityGroup' BastionHostRole: Type: 'AWS::IAM::Role' Properties: Policies: - PolicyDocument: Version: 2012-10-17 Statement: - Action: - 's3:GetObject' Resource: !Sub 'arn:${AWS::Partition}:s3:::aws-quickstart-${AWS::Region}/quickstart-linux-bastion/*' Effect: Allow PolicyName: aws-quick-start-s3-policy - PolicyDocument: Version: 2012-10-17 Statement: - Action: - 'logs:CreateLogStream' - 'logs:GetLogEvents' - 'logs:PutLogEvents' - 'logs:DescribeLogGroups' - 'logs:DescribeLogStreams' - 'logs:PutRetentionPolicy' - 'logs:PutMetricFilter' - 'logs:CreateLogGroup' Resource: !Sub "arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:${BastionMainLogGroup}:*" Effect: Allow PolicyName: bastion-cloudwatch-logs-policy - PolicyDocument: Version: 2012-10-17 Statement: - Action: - 'ec2:AssociateAddress' - 'ec2:DescribeAddresses' Resource: - '*' Effect: Allow PolicyName: bastion-eip-policy - PolicyDocument: Version: 2012-10-17 Statement: - Action: - 'ec2:DescribeInstances' - 'ec2:DescribeTags' Resource: - '*' Effect: Allow PolicyName: ec2-selfaccess-policy - PolicyDocument: Version: 2012-10-17 Statement: - Action: - 'iam:ListAccountAliases' - 'autoscaling:DescribeAutoScalingInstances' - 'autoscaling:DescribeAutoScalingGroups' - 'autoscaling:DescribeLifecycle*' Resource: - '*' Effect: Allow PolicyName: ASGSelfAccessPolicy - PolicyDocument: Version: 2012-10-17 Statement: - Action: - 'autoscaling:CompleteLifecycleAction' - 'autoscaling:RecordLifecycleActionHeartbeat' Resource: - !Sub 'arn:${AWS::Partition}:autoscaling:${AWS::Region}:${AWS::AccountId}:autoScalingGroup:*:autoScalingGroupName/${AWS::StackName}*' Effect: Allow PolicyName: ASGLifeCycleAccessPolicy ManagedPolicyArns: - !Sub 'arn:${AWS::Partition}:iam::aws:policy/AmazonSSMManagedInstanceCore' - !Sub 'arn:${AWS::Partition}:iam::aws:policy/CloudWatchAgentServerPolicy' Path: / AssumeRolePolicyDocument: Statement: - Action: - 'sts:AssumeRole' Principal: Service: - !Sub 'ec2.${AWS::URLSuffix}' Effect: Allow Version: 2012-10-17 BastionHostProfile: Type: 'AWS::IAM::InstanceProfile' Properties: Roles: - !Ref BastionHostRole Path: / EIP: Type: 'AWS::EC2::EIP' Properties: Domain: vpc BastionAutoScalingGroup: Type: 'AWS::AutoScaling::AutoScalingGroup' Properties: DesiredCapacity: '1' LaunchConfigurationName: !Ref BastionLaunchConfiguration LifecycleHookSpecificationList: - LifecycleTransition: 'autoscaling:EC2_INSTANCE_LAUNCHING' LifecycleHookName: instance-patching-reboot HeartbeatTimeout: 3600 MaxSize: '1' MinSize: '1' Tags: - Key: Name Value: !Sub - '${AWS::StackName}-BastionHost-${CidrBlock}' - CidrBlock: {'Fn::ImportValue': !Sub '${ParentVPCStack}-CidrBlock'} PropagateAtLaunch: true NotificationConfigurations: - TopicARN: !Ref EC2SNSTopic NotificationTypes: - 'autoscaling:EC2_INSTANCE_LAUNCH_ERROR' - 'autoscaling:EC2_INSTANCE_TERMINATE_ERROR' VPCZoneIdentifier: !Split [',', {'Fn::ImportValue': !Sub '${ParentVPCStack}-SubnetsPublic'}] CreationPolicy: ResourceSignal: Count: 1 Timeout: PT10M UpdatePolicy: AutoScalingRollingUpdate: PauseTime: PT10M SuspendProcesses: - HealthCheck - ReplaceUnhealthy - AZRebalance - AlarmNotification - ScheduledActions WaitOnResourceSignals: true BastionLaunchConfiguration: Type: 'AWS::AutoScaling::LaunchConfiguration' Metadata: 'AWS::CloudFormation::Authentication': S3AccessCreds: type: S3 roleName: !Ref BastionHostRole buckets: - !Sub 'aws-quickstart-${AWS::Region}' 'AWS::CloudFormation::Init': config: files: /tmp/auditd.rules: mode: '000550' owner: root group: root content: | -a exit,always -F arch=b64 -S execve -a exit,always -F arch=b32 -S execve /tmp/auditing_configure.sh: source: !Sub 'https://aws-quickstart-${AWS::Region}.s3.${AWS::Region}.${AWS::URLSuffix}/quickstart-linux-bastion/scripts/auditing_configure.sh' mode: '000550' owner: root group: root authentication: S3AccessCreds /tmp/bastion_bootstrap.sh: source: !If - UseAlternativeInitialization - !Ref AltInitScript - !Sub 'https://aws-quickstart-${AWS::Region}.s3.${AWS::Region}.${AWS::URLSuffix}/quickstart-linux-bastion/scripts/bastion_bootstrap.sh' mode: '000550' owner: root group: root authentication: S3AccessCreds /home/ec2-user/.psqlrc: content: !Sub | \set PROMPT1 '%n@%m %~%R%# ' \pset pager off \set COMP_KEYWORD_CASE upper \set VERBOSITY verbose \x auto \set HISTCONTROL ignorespace \set HISTFILE ~/.psql_history- :DBNAME \set HISTSIZE 5000 \set version 'SELECT version();' \set extensions 'select * from pg_available_extensions;' mode: "000644" owner: "root" group: "root" commands: a-add_auditd_rules: cwd: '/tmp/' env: BASTION_OS: !FindInMap [LinuxAMINameMap, 'Amazon-Linux2-HVM', OS] command: "./auditing_configure.sh" b-bootstrap: cwd: '/tmp/' command: !Sub - "REGION=${AWS::Region} URL_SUFFIX=${AWS::URLSuffix} BANNER_REGION=${AWS::Region} ./bastion_bootstrap.sh --banner ${BannerUrl} --enable ${EnableBanner} --tcp-forwarding ${EnableTCPForwarding} --x11-forwarding ${EnableX11Forwarding}" - BannerUrl: !If - DefaultBanner - !Sub 's3://aws-quickstart-${AWS::Region}/quickstart-linux-bastion/scripts/banner_message.txt' - !Ref BastionBanner Properties: AssociatePublicIpAddress: 'true' PlacementTenancy: !Ref BastionTenancy KeyName: !Ref KeyPairName IamInstanceProfile: !Ref BastionHostProfile ImageId: !If - UseOSImageOverride - !Ref OSImageOverride - !FindInMap - AWSAMIRegionMap - !Ref 'AWS::Region' - !FindInMap - LinuxAMINameMap - 'Amazon-Linux2-HVM' - Code SecurityGroups: - !Ref BastionSecurityGroup InstanceType: !Ref BastionInstanceType UserData: Fn::Base64: !Sub | #!/bin/bash set -x export PATH=$PATH:/usr/local/bin MYINSTANCEID="$(wget -q -O - http://169.254.169.254/latest/meta-data/instance-id)" MYREGION="$(curl -s 169.254.169.254/latest/meta-data/placement/availability-zone | sed 's/.$//')" NAMEOFASG=$(aws ec2 describe-tags --region $MYREGION --filters {"Name=resource-id,Values=$MYINSTANCEID","Name=key,Values=aws:autoscaling:groupName"} --output=text | cut -f5) PATCHDONEFLAG=/root/patchingrebootwasdone.flg # Utility for checking if a restart is needed by any of the patching that was done yum update yum-utils #cfn signaling functions function cfn_fail { cfn-signal -e 1 --stack ${AWS::StackName} --region ${AWS::Region} --resource BastionAutoScalingGroup exit 1 } function cfn_success { cfn-signal -e 0 --stack ${AWS::StackName} --region ${AWS::Region} --resource BastionAutoScalingGroup exit 0 } if [ ! -f $PATCHDONEFLAG ]; then curl -s https://packagecloud.io/install/repositories/akopytov/sysbench/script.rpm.sh yum install git -y || apt-get install -y git || zypper -n install git rpm --import https://repo.mysql.com/RPM-GPG-KEY-mysql-2022 yum install -y https://dev.mysql.com/get/mysql57-community-release-el7-11.noarch.rpm amazon-linux-extras enable postgresql14 && yum clean metadata && sudo yum install -y postgresql-contrib postgresql libpq-devel mysql-community-client until git clone https://github.com/aws-quickstart/quickstart-linux-utilities.git ; do echo "Retrying"; done cd /quickstart-linux-utilities; source quickstart-cfn-tools.source; qs_update-os || qs_err; qs_bootstrap_pip || qs_err " pip bootstrap failed "; qs_aws-cfn-bootstrap || qs_err " cfn bootstrap failed "; needs-restarting -r if [ $? -gt 0 ]; then # Resetting userdata semaphore so that userdata will process again on restart rm /var/lib/cloud/instances/*/sem/config_scripts_user touch $PATCHDONEFLAG reboot sleep 30 fi else cd /quickstart-linux-utilities; source quickstart-cfn-tools.source; fi EIP_LIST="${EIP}" CLOUDWATCHGROUP=${BastionMainLogGroup} cfn-init -v --stack '${AWS::StackName}' --resource BastionLaunchConfiguration --region ${AWS::Region} || cfn_fail aws --region ${AWS::Region} autoscaling complete-lifecycle-action --lifecycle-action-result CONTINUE --instance-id $MYINSTANCEID --lifecycle-hook-name instance-patching-reboot --auto-scaling-group-name $NAMEOFASG [ $(qs_status) == 0 ] && cfn_success || cfn_fail CPUTooHighAlarm: Type: 'AWS::CloudWatch::Alarm' Properties: AlarmDescription: 'Average CPU utilization over last 10 minutes higher than 80%' Namespace: 'AWS/EC2' MetricName: CPUUtilization Statistic: Average Period: 600 EvaluationPeriods: 1 ComparisonOperator: GreaterThanThreshold Threshold: 80 AlarmActions: - Ref: EC2SNSTopic Dimensions: - Name: AutoScalingGroupName Value: !Ref BastionAutoScalingGroup Outputs: TemplateID: Description: 'Template ID' Value: 'VPC-SSH-Bastion' BastionAutoScalingGroup: Description: Auto Scaling Group Reference ID Value: !Ref BastionAutoScalingGroup Export: Name: !Sub '${AWS::StackName}-BastionAutoScalingGroup' EIP: Description: The public IP address of the SSH bastion host/instance Value: !Ref EIP Export: Name: !Sub '${AWS::StackName}-EIP' SSHCommand: Description: SSH command line Value: !Join - '' - - 'ssh -i "' - !Ref KeyPairName - '.pem" ' - 'ec2-user@' - !Ref EIP CloudWatchLogs: Description: CloudWatch Logs GroupName. Your SSH logs will be stored here. Value: !Ref BastionMainLogGroup Export: Name: !Sub '${AWS::StackName}-CloudWatchLogs' BastionSecurityGroupID: Description: Use this Security Group to reference incoming traffic from the SSH bastion host/instance Value: !Ref BastionSecurityGroup Export: Name: !Sub '${AWS::StackName}-BastionSecurityGroupID'