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