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
