cloudformation/secure-contact.template.yaml (332 lines of code) (raw):

AWSTemplateFormatVersion: '2010-09-09' Description: Run the SecureContact Monitor in AWS Metadata: AWS::CloudFormation::Interface: ParameterGroups: - Label: default: Security Parameters: - AccessCidr - Label: default: Networking Parameters: - VpcId - LoadBalancerSubnets - InstanceSubnets - Label: default: AWS Configuration Parameters: - AMI - Stage - PublicBucket Parameters: AccessCidr: Description: A CIDR from which access to the instance is allowed Default: /account/services/kp-public-cidr Type: AWS::SSM::Parameter::Value<String> VpcId: Description: ID of the VPC onto which to launch the stack Default: /account/vpc/primary/id Type: AWS::SSM::Parameter::Value<AWS::EC2::VPC::Id> LoadBalancerSubnets: Description: Subnets to use in VPC for public ELB Default: /account/vpc/primary/subnets/public Type: AWS::SSM::Parameter::Value<List<AWS::EC2::Subnet::Id>> InstanceSubnets: Description: Subnets to use in VPC for instances Default: /account/vpc/primary/subnets/private Type: AWS::SSM::Parameter::Value<List<AWS::EC2::Subnet::Id>> AMI: Description: Base AMI for SecureContact instances Type: AWS::EC2::Image::Id Stage: Description: Application stage Type: String AllowedValues: - PROD - CODE - DEV Default: CODE PublicBucket: Description: Name of the public S3 bucket that hosts the website Type: String AlertEmail: Description: Email notified if the healtcheck failed Type: String App: Type: String Default: secure-contact-monitor Stack: Type: String Resources: SecureDropBucketParameter: Type: AWS::SSM::Parameter Properties: Description: S3 bucket that holds the site content Name: !Sub /${App}/${Stage}/securedrop-public-bucket Type: String Value: !Ref PublicBucket # ----------------------- # # LOADBALANCER # # ----------------------- # LoadBalancerSecurityGroup: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: Secure Contact Security Group VpcId: Ref: VpcId SecurityGroupIngress: - IpProtocol: tcp FromPort: 443 ToPort: 443 CidrIp: !Ref AccessCidr - IpProtocol: tcp FromPort: 80 ToPort: 80 CidrIp: !Ref AccessCidr Tags: - Key: Stack Value: !Ref Stack - Key: App Value: !Ref App - Key: Stage Value: !Ref Stage - Key: Name Value: !Sub ${AWS::StackName} LoadBalancer LoadBalancer: Type: AWS::ElasticLoadBalancing::LoadBalancer Properties: Listeners: - LoadBalancerPort: 80 InstancePort: 80 Protocol: HTTP HealthCheck: Target: HTTP:80/index.html HealthyThreshold: 2 UnhealthyThreshold: 10 Interval: 30 Timeout: 20 Subnets: Ref: LoadBalancerSubnets SecurityGroups: - Ref: LoadBalancerSecurityGroup Tags: - Key: Stack Value: !Ref Stack - Key: App Value: !Ref App - Key: Stage Value: !Ref Stage - Key: Name Value: !Sub ${AWS::StackName} # ----------------------- # # APPLICATION SERVER # # ----------------------- # SecureContactInstanceProfile: Type: AWS::IAM::InstanceProfile Properties: Path: / Roles: - !Ref SecureContactInstanceRole SecureContactInstanceRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Statement: - Effect: Allow Principal: Service: - ec2.amazonaws.com Action: - sts:AssumeRole Path: / Policies: - PolicyName: instance-policy PolicyDocument: Statement: # fetch parameters according to stage - Effect: Allow Action: - ssm:GetParameter - ssm:GetParameters - ssm:GetParametersByPath Resource: - !Sub arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/securedrop-url - !Sub arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/securedrop-url-human - !Sub arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/secure-contact/${Stage}/* # send email alerts from validated addresses - Effect: Allow Action: - ses:SendEmail Resource: "*" # allow service to update the website configuration - Effect: Allow Resource: - !Sub arn:aws:s3:::${PublicBucket} - !Sub arn:aws:s3:::${PublicBucket}/* Action: - S3:PutBucketWebsite - S3:PutObject # Minimal policy to run commands via ssm and use ssm-scala SSMRunCommandPolicy: Type: AWS::IAM::Policy Properties: PolicyName: ssm-run-command PolicyDocument: Statement: - Effect: Allow Resource: "*" Action: - ec2messages:AcknowledgeMessage - ec2messages:DeleteMessage - ec2messages:FailMessage - ec2messages:GetEndpoint - ec2messages:GetMessages - ec2messages:SendReply - ssm:UpdateInstanceInformation - ssm:ListInstanceAssociations - ssm:DescribeInstanceProperties - ssm:DescribeDocumentParameters - ssmmessages:CreateControlChannel - ssmmessages:CreateDataChannel - ssmmessages:OpenControlChannel - ssmmessages:OpenDataChannel Roles: - !Ref SecureContactInstanceRole # Describe instance tags, to find out its own stack, app, stage DescribeTagsPolicy: Type: AWS::IAM::Policy Properties: PolicyName: describe-tags-policy PolicyDocument: Statement: - Effect: Allow Resource: "*" Action: - autoscaling:DescribeAutoScalingInstances - autoscaling:DescribeAutoScalingGroups - ec2:DescribeRegions - ec2:DescribeTags Roles: - !Ref SecureContactInstanceRole InstanceSecurityGroup: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: SecureContact EC2 instance VpcId: Ref: VpcId SecurityGroupIngress: # allow ELB to talk to instance - IpProtocol: tcp FromPort: 80 ToPort: 80 SourceSecurityGroupId: Ref: LoadBalancerSecurityGroup SecurityGroupEgress: # allow instance to make http requests - IpProtocol: tcp FromPort: 80 ToPort: 80 CidrIp: 0.0.0.0/0 - IpProtocol: tcp FromPort: 443 ToPort: 443 CidrIp: 0.0.0.0/0 Tags: - Key: Stack Value: !Ref Stack - Key: App Value: !Ref App - Key: Stage Value: !Ref Stage - Key: Name Value: !Sub ${AWS::StackName} Instance LaunchConfig: Type: AWS::AutoScaling::LaunchConfiguration Properties: ImageId: Ref: AMI SecurityGroups: - Ref: InstanceSecurityGroup InstanceType: t4g.micro IamInstanceProfile: Ref: SecureContactInstanceProfile AssociatePublicIpAddress: false MetadataOptions: HttpTokens: required UserData: 'Fn::Base64': !Sub | #!/bin/bash -ev TOKEN=`curl -X PUT "http://169.254.169.254/latest/api/token" \ -H "X-aws-ec2-metadata-token-ttl-seconds: 21600"` \ && curl -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/ echo ${Stage} > /etc/stage apt-get update && apt-get install -y apt-transport-https # restart tor service sudo systemctl restart tor # Check that tor is running before we proceed until $(curl -s -o /dev/null --head --fail --socks5-hostname 127.0.0.1:9050 https://check.torproject.org); do printf '.' sleep 3 done # install application mkdir /secure-contact git clone https://github.com/guardian/secure-contact /secure-contact # install python packages and run monitor once cd /secure-contact pip3 install -r /secure-contact/requirements.txt python3 -m src.monitor # fix permissions chown -R www-data /secure-contact/ # add nginx configuration cd /etc/nginx/sites-available/ cat <<SECURE_CONTACT_CONF > secure-contact server { listen 80 default_server; listen [::]:80 default_server ipv6only=on; root /secure-contact/build; index index.html; add_header Strict-Transport-Security "max-age=15552000"; location ~ /(data|lib|tmp|tpl)/ { deny all; return 404; } } SECURE_CONTACT_CONF cd /etc/nginx/sites-enabled/ ln -s /etc/nginx/sites-available/secure-contact secure-contact nginx -s reload # schedule monitoring service to run on every hour touch /secure-contact/cron-lastrun.log chown www-data /secure-contact/cron-lastrun.log echo '*/30 * * * * cd /secure-contact && python3 -m src.monitor >> /secure-contact/cron-lastrun.log 2>&1' | crontab -u www-data - AutoscalingGroup: Type: AWS::AutoScaling::AutoScalingGroup Properties: VPCZoneIdentifier: Ref: InstanceSubnets LaunchConfigurationName: Ref: LaunchConfig MinSize: 0 MaxSize: 2 HealthCheckType: ELB HealthCheckGracePeriod: 600 LoadBalancerNames: - Ref: LoadBalancer Tags: - Key: Stack Value: !Ref Stack PropagateAtLaunch: true - Key: App Value: !Ref App PropagateAtLaunch: true - Key: Stage Value: !Ref Stage PropagateAtLaunch: true - Key: Name Value: !Sub ${AWS::StackName} PropagateAtLaunch: true NotificationTopic: Type: AWS::SNS::Topic Properties: DisplayName: !Sub Secure Contact ${Stage} notification topic Subscription: - Endpoint: !Ref AlertEmail Protocol: email NotificationTopicPolicy: Type: AWS::SNS::TopicPolicy Properties: PolicyDocument: Statement: - Effect: Allow Action: - SNS:Publish Principal: AWS: !Ref AWS::AccountId Resource: "*" Topics: - !Ref NotificationTopic Outputs: LoadBalancerUrl: Value: !GetAtt LoadBalancer.DNSName TopicName: Value: !GetAtt NotificationTopic.TopicName