├── .ruby-version ├── Gemfile ├── roles ├── sentry │ ├── vars │ │ └── main.yml │ ├── handlers │ │ └── main.yml │ ├── defaults │ │ └── main.yml │ ├── templates │ │ ├── nginx_main_conf.j2 │ │ ├── supervisor-sentry.conf.j2 │ │ ├── nginx-sentry.conf.j2 │ │ ├── sentry_yml.j2 │ │ └── sentry_conf.j2 │ ├── tasks │ │ ├── nginx.yml │ │ └── main.yml │ └── files │ │ └── supervisord.conf ├── common │ ├── handlers │ │ └── main.yml │ ├── templates │ │ ├── cis_conf.j2 │ │ └── motd_banner.j2 │ └── tasks │ │ └── main.yml ├── postgres │ ├── handlers │ │ └── main.yml │ ├── files │ │ └── pg_hba.conf │ └── tasks │ │ └── main.yml └── redis │ └── tasks │ └── main.yml ├── .gitignore ├── assets ├── launch-stack.png └── cloud-formation-designer.png ├── Releases.md ├── Guardfile ├── ansible.cfg ├── .vscode └── tasks.json ├── Vagrantfile ├── config └── lono.rb ├── site.yml ├── hosts ├── LICENCE ├── Gemfile.lock ├── deploy-example.sh ├── README.md ├── output ├── 1.0.0-internal-1az.yaml ├── master-internal-1az.yaml ├── 1.0.0-internet-facing-1az.yaml ├── master-internet-facing-1az.yaml └── 1.0.0-internal-2az.yaml └── templates └── sentry-formation.yaml.erb /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.4.1 -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem 'lono' -------------------------------------------------------------------------------- /roles/sentry/vars/main.yml: -------------------------------------------------------------------------------- 1 | nginx: 2 | main_dir: /etc/nginx 3 | -------------------------------------------------------------------------------- /roles/common/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Restart Sysctl 4 | command: sysctl -p 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Ansible ### 2 | *.retry 3 | .vagrant 4 | 5 | ### Build ### 6 | deploy.sh 7 | artifacts 8 | -------------------------------------------------------------------------------- /assets/launch-stack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/o2Labs/sentry-formation/HEAD/assets/launch-stack.png -------------------------------------------------------------------------------- /roles/postgres/handlers/main.yml: -------------------------------------------------------------------------------- 1 | - name: restart postgresql 2 | service: name=postgresql state=restarted enabled=yes -------------------------------------------------------------------------------- /assets/cloud-formation-designer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/o2Labs/sentry-formation/HEAD/assets/cloud-formation-designer.png -------------------------------------------------------------------------------- /Releases.md: -------------------------------------------------------------------------------- 1 | # 1.0.0 2 | 3 | - Add versioning. 4 | - Generate cloud formation template that is locked to a specific version of the repo. 5 | -------------------------------------------------------------------------------- /roles/common/templates/cis_conf.j2: -------------------------------------------------------------------------------- 1 | install dccp /bin/true 2 | install sctp /bin/true 3 | install rds /bin/true 4 | install tipc /bin/true 5 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | # More info at https://github.com/guard/guard#readme 2 | 3 | guard "lono" do 4 | watch(%r{^config/lono.rb$}) 5 | watch(%r{^templates/.*$}) 6 | end -------------------------------------------------------------------------------- /ansible.cfg: -------------------------------------------------------------------------------- 1 | [defaults] 2 | hostfile = hosts 3 | host_key_checking = False 4 | 5 | [ssh_connection] 6 | ssh_args = -o ForwardAgent=yes -o ControlMaster=auto -o ControlPersist=60 -------------------------------------------------------------------------------- /roles/sentry/handlers/main.yml: -------------------------------------------------------------------------------- 1 | - name: reload supervisor 2 | shell: supervisorctl reload 3 | 4 | - name: Restart nginx 5 | service: name=nginx 6 | state=restarted 7 | -------------------------------------------------------------------------------- /roles/redis/tasks/main.yml: -------------------------------------------------------------------------------- 1 | # Redis server 2 | --- 3 | 4 | - name: Install prerequisite packages that are necessary to compile applications and gems with native extensions 5 | apt: pkg=redis-server 6 | -------------------------------------------------------------------------------- /roles/postgres/files/pg_hba.conf: -------------------------------------------------------------------------------- 1 | # TYPE DATABASE USER CIDR-ADDRESS METHOD 2 | 3 | local all postgres trust 4 | 5 | local all all trust 6 | 7 | host all all 127.0.0.1/32 md5 8 | 9 | host all all ::1/128 md5 10 | 11 | host all all 0.0.0.0/0 md5 12 | -------------------------------------------------------------------------------- /roles/common/templates/motd_banner.j2: -------------------------------------------------------------------------------- 1 | 2 | ***************************************************************** 3 | 4 | WARNING: You have accessed a computer system managed by {{owner}}. 5 | You must be authorised by {{owner}} to use this system. 6 | Unauthorised access to or misuse of this system is prohibited and 7 | constitutes an offence under the Computer Misuse Act 1990. 8 | 9 | ***************************************************************** 10 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type":"shell", 8 | "taskName": "generate", 9 | "command": "lono generate", 10 | "group": { 11 | "kind": "build", 12 | "isDefault": true 13 | } 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /roles/sentry/defaults/main.yml: -------------------------------------------------------------------------------- 1 | other_python_pkgs: 2 | - build-essential 3 | - libxslt1-dev 4 | - libffi-dev 5 | - libjpeg-dev 6 | - libxml2-dev 7 | - libxslt-dev 8 | - libyaml-dev 9 | - libpq-dev 10 | - libreadline-gplv2-dev 11 | - libncursesw5-dev 12 | - libssl-dev 13 | - libsqlite3-dev 14 | - tk-dev 15 | - libgdbm-dev 16 | - libc6-dev 17 | - libbz2-dev 18 | - libblas-dev 19 | - liblapack-dev 20 | - libatlas-base-dev 21 | - python-passlib 22 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | # All Vagrant configuration is done below. The "2" in Vagrant.configure 5 | # configures the configuration version (we support older styles for 6 | # backwards compatibility). Please don't change it unless you know what 7 | # you're doing. 8 | Vagrant.configure("2") do |config| 9 | config.vm.box = "bento/ubuntu-16.04" 10 | config.vm.host_name = "sentry" 11 | config.vm.network "private_network", ip: "33.33.33.20" 12 | end 13 | -------------------------------------------------------------------------------- /config/lono.rb: -------------------------------------------------------------------------------- 1 | def generate(visibility, zones, version) 2 | template "#{version}-#{visibility}-#{zones}az.yaml" do 3 | source "sentry-formation.yaml.erb" 4 | variables( 5 | :Description => "Sentry.io #{visibility} setup in #{zones} availability zones", 6 | :visibility => visibility, 7 | :availability_zones => zones, 8 | :version => version 9 | ) 10 | end 11 | end 12 | 13 | def get_version() 14 | File.foreach('Releases.md').first.match(/[0-9.]+/)[0] 15 | end 16 | 17 | [get_version(), 'master'].each do |version| 18 | ["internal", "internet-facing"].each do |visibility| 19 | (1..3).each do |zones| 20 | generate(visibility, zones, version) 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /roles/sentry/templates/nginx_main_conf.j2: -------------------------------------------------------------------------------- 1 | user www-data; 2 | worker_processes 4; 3 | pid /run/nginx.pid; 4 | 5 | events { 6 | worker_connections 1024; 7 | # multi_accept on; 8 | } 9 | 10 | http { 11 | 12 | ## 13 | # Basic Settings 14 | ## 15 | 16 | sendfile on; 17 | tcp_nopush on; 18 | tcp_nodelay on; 19 | keepalive_timeout 65; 20 | types_hash_max_size 2048; 21 | 22 | include /etc/nginx/mime.types; 23 | default_type application/octet-stream; 24 | 25 | ## 26 | # Logging Settings 27 | ## 28 | 29 | access_log /var/log/nginx/access.log; 30 | error_log /var/log/nginx/error.log; 31 | 32 | ## 33 | # Virtual Host Configs 34 | ## 35 | 36 | include /etc/nginx/conf.d/*.conf; 37 | include /etc/nginx/sites-enabled/*; 38 | } 39 | -------------------------------------------------------------------------------- /site.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - hosts: aws 4 | connection: local 5 | roles: 6 | - common 7 | - sentry 8 | gather_facts: false 9 | # Because Ubuntu doesn't come with correct version of Python 10 | pre_tasks: 11 | - name: Update apt packages 12 | raw: apt-get update 13 | - name: Install python 14 | raw: apt-get install python-minimal aptitude -y 15 | - name: Gather facts 16 | action: setup 17 | become: yes 18 | 19 | - hosts: dev 20 | roles: 21 | - common 22 | - redis 23 | - postgres 24 | - sentry 25 | gather_facts: false 26 | # Because Ubuntu doesn't come with correct version of Python 27 | pre_tasks: 28 | - name: Update apt packages 29 | raw: apt-get update 30 | - name: Install python 31 | raw: apt-get install python-minimal aptitude -y 32 | - name: Gather facts 33 | action: setup 34 | become: yes # become sudo 35 | -------------------------------------------------------------------------------- /hosts: -------------------------------------------------------------------------------- 1 | [dev] 2 | 33.33.33.20 ansible_ssh_private_key_file=.vagrant/machines/default/virtualbox/private_key 3 | 4 | [dev:vars] 5 | user=vagrant 6 | owner="Example Company Name" 7 | sentry_admin_username="sentry@example.com" 8 | sentry_admin_password="CHANGE_ME" 9 | sentry_url="sentry.example.com" 10 | sentry_db_host="localhost" 11 | sentry_db_port="5432" 12 | sentry_db_name="sentry" 13 | sentry_db_user="sentrydbadmin" 14 | sentry_db_password="CHANGE_ME" 15 | sentry_redis_host="127.0.0.1" 16 | sentry_redis_port="6379" 17 | sentry_secret_key="CHANGE_ME" 18 | #sentry_public_dns_name="localhost" 19 | #sentry_mail_host="mail.example.com" 20 | #sentry_mail_port="25" 21 | #sentry_mail_username="sentrysmtpuser" 22 | #sentry_mail_password="CHANGE_ME" 23 | #sentry_mail_from="sentry@example.com" 24 | #sentry_github_app_id="YOUR_GITHUB_APP_ID" 25 | #sentry_github_api_secret="CHANGE_ME" 26 | #sentry_files_bucket_name="123456789-example-sentry-files" 27 | -------------------------------------------------------------------------------- /roles/postgres/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Postgres create postgres db 3 | 4 | - name: Install postgresql packages 5 | apt: pkg={{ item }} state=latest 6 | with_items: 7 | - postgresql 8 | - postgresql-contrib 9 | - postgresql-client 10 | - libpq-dev 11 | tags: postgres 12 | 13 | - name: Install postgresql python bindings 14 | pip: 15 | name: psycopg2 16 | tags: postgres 17 | 18 | - name: Create database 19 | become_user: postgres 20 | postgresql_db: 21 | name: "{{ sentry_db_name }}" 22 | encoding: 'UTF-8' 23 | lc_collate: 'en_US.UTF-8' 24 | lc_ctype: 'en_US.UTF-8' 25 | template: 'template0' 26 | state: present 27 | tags: postgres 28 | 29 | - name: Grant access for database to user 30 | become_user: postgres 31 | postgresql_user: 32 | name: "{{ sentry_db_user }}" 33 | password: "{{ sentry_db_password }}" 34 | role_attr_flags: "CREATEDB,SUPERUSER" 35 | tags: postgres 36 | -------------------------------------------------------------------------------- /roles/sentry/templates/supervisor-sentry.conf.j2: -------------------------------------------------------------------------------- 1 | [program:sentry] 2 | user=sentry 3 | group=sentry 4 | command=/www/sentry/ve/bin/sentry --config=/etc/sentry run web 5 | environment=PATH="/www/sentry/ve/bin",HOME="/home/sentry/",USER="sentry" 6 | autostart=true 7 | autorestart=true 8 | redirect_stderr=true 9 | 10 | # See https://docs.getsentry.com/on-premise/server/installation/#configure-supervisord 11 | [program:celery_beat] 12 | command=/www/sentry/ve/bin/sentry --config=/etc/sentry run cron --pidfile=/home/sentry/celerybeat.pid 13 | environment=PATH="/www/sentry/ve/bin",HOME="/home/sentry/",USER="sentry" 14 | autostart=true 15 | autorestart=true 16 | redirect_stderr=true 17 | 18 | [program:celery_worker] 19 | user=sentry 20 | group=sentry 21 | command=/www/sentry/ve/bin/sentry --config=/etc/sentry run worker 22 | environment=PATH="/www/sentry/ve/bin",HOME="/home/sentry/",USER="sentry" 23 | autostart=true 24 | autorestart=true 25 | redirect_stderr=true -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 The Lab 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /roles/sentry/templates/nginx-sentry.conf.j2: -------------------------------------------------------------------------------- 1 | ## 2 | # SSL Configs 3 | ## 4 | 5 | server { 6 | 7 | listen 443 ssl; 8 | ssl_certificate /etc/nginx/ssl/bundle.crt; 9 | ssl_certificate_key /etc/nginx/ssl/server.key; 10 | ssl_protocols TLSv1 TLSv1.1 TLSv1.2; 11 | ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:ECDHE-RSA-AES128-GCM-SHA256:AES256+EECDH:DHE-RSA-AES128-GCM-SHA256:AES256+EDH:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4"; 12 | 13 | server_name {{sentry_url}}; 14 | 15 | location / { 16 | proxy_pass http://localhost:9000; 17 | proxy_redirect off; 18 | 19 | proxy_set_header Host $host; 20 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 21 | proxy_set_header X-Forwarded-Proto $scheme; 22 | add_header Strict-Transport-Security "max-age=31536000"; 23 | } 24 | } -------------------------------------------------------------------------------- /roles/sentry/tasks/nginx.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # defaults file for nginx install 3 | - name: install nginx 4 | apt: name=nginx state=present update_cache=yes 5 | tags: nginx 6 | 7 | # Auto enable nginx 8 | - name: nginx auto start on boot 9 | service: name=nginx enabled=yes 10 | tags: nginx 11 | 12 | # Update main config for site 13 | - name: update the main nginx config 14 | template: src=nginx_main_conf.j2 dest={{nginx.main_dir}}/nginx.conf 15 | notify: 16 | - Restart nginx 17 | tags: nginx 18 | 19 | - name: create ssl directory 20 | file: path=/etc/nginx/ssl state=directory 21 | notify: 22 | - Restart nginx 23 | tags: nginx 24 | 25 | - name: create self-signed SSL cert 26 | command: 'openssl req -new -nodes -x509 -subj "/O={{owner}}/CN={{ansible_fqdn}}" -days 3650 -keyout /etc/nginx/ssl/server.key -out /etc/nginx/ssl/bundle.crt -extensions v3_ca creates=/etc/nginx/ssl/bundle.crt' 27 | notify: 28 | - Restart nginx 29 | tags: nginx 30 | 31 | - name: set SSL cert permissions 32 | file: 33 | path: /etc/nginx/ssl/{{ item }} 34 | owner: "{{ user }}" 35 | group: "{{ user }}" 36 | mode: 0755 37 | with_items: 38 | - "bundle.crt" 39 | - "server.key" 40 | notify: 41 | - Restart nginx 42 | tags: nginx 43 | -------------------------------------------------------------------------------- /roles/sentry/files/supervisord.conf: -------------------------------------------------------------------------------- 1 | ; supervisor config file 2 | 3 | [unix_http_server] 4 | file=/var/run//supervisor.sock ; (the path to the socket file) 5 | chmod=0700 ; sockef file mode (default 0700) 6 | 7 | [supervisord] 8 | logfile=/var/log/supervisor/supervisord.log ; (main log file;default $CWD/supervisord.log) 9 | pidfile=/var/run/supervisord.pid ; (supervisord pidfile;default supervisord.pid) 10 | childlogdir=/var/log/supervisor ; ('AUTO' child log dir, default $TEMP) 11 | 12 | ; the below section must remain in the config file for RPC 13 | ; (supervisorctl/web interface) to work, additional interfaces may be 14 | ; added by defining them in separate rpcinterface: sections 15 | [rpcinterface:supervisor] 16 | supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface 17 | 18 | [supervisorctl] 19 | serverurl=unix:///var/run//supervisor.sock ; use a unix:// URL for a unix socket 20 | 21 | ; The [include] section can just contain the "files" setting. This 22 | ; setting can list multiple files (separated by whitespace or 23 | ; newlines). It can also contain wildcards. The filenames are 24 | ; interpreted as relative to this file. Included files *cannot* 25 | ; include files themselves. 26 | 27 | [include] 28 | files = /etc/supervisor/conf.d/*.conf -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | aws-sdk (2.10.24) 5 | aws-sdk-resources (= 2.10.24) 6 | aws-sdk-core (2.10.24) 7 | aws-sigv4 (~> 1.0) 8 | jmespath (~> 1.0) 9 | aws-sdk-resources (2.10.24) 10 | aws-sdk-core (= 2.10.24) 11 | aws-sigv4 (1.0.1) 12 | coderay (1.1.1) 13 | colorize (0.8.1) 14 | ffi (1.9.18) 15 | formatador (0.2.5) 16 | guard (2.14.1) 17 | formatador (>= 0.2.4) 18 | listen (>= 2.7, < 4.0) 19 | lumberjack (~> 1.0) 20 | nenv (~> 0.1) 21 | notiffany (~> 0.0) 22 | pry (>= 0.9.12) 23 | shellany (~> 0.0) 24 | thor (>= 0.18.1) 25 | guard-cloudformation (0.0.3) 26 | colorize 27 | guard 28 | guard-compat (1.2.1) 29 | guard-lono (1.0.1) 30 | colorize 31 | guard 32 | guard-compat 33 | hashie (3.5.6) 34 | jmespath (1.3.1) 35 | json (2.1.0) 36 | listen (3.1.5) 37 | rb-fsevent (~> 0.9, >= 0.9.4) 38 | rb-inotify (~> 0.9, >= 0.9.7) 39 | ruby_dep (~> 1.2) 40 | lono (2.1.0) 41 | aws-sdk 42 | colorize 43 | guard 44 | guard-cloudformation 45 | guard-lono 46 | hashie 47 | json 48 | rb-fsevent 49 | thor 50 | lumberjack (1.0.12) 51 | method_source (0.8.2) 52 | nenv (0.3.0) 53 | notiffany (0.1.1) 54 | nenv (~> 0.1) 55 | shellany (~> 0.0) 56 | pry (0.10.4) 57 | coderay (~> 1.1.0) 58 | method_source (~> 0.8.1) 59 | slop (~> 3.4) 60 | rb-fsevent (0.10.2) 61 | rb-inotify (0.9.10) 62 | ffi (>= 0.5.0, < 2) 63 | ruby_dep (1.5.0) 64 | shellany (0.0.1) 65 | slop (3.6.0) 66 | thor (0.19.4) 67 | 68 | PLATFORMS 69 | ruby 70 | 71 | DEPENDENCIES 72 | lono 73 | 74 | BUNDLED WITH 75 | 1.15.3 76 | -------------------------------------------------------------------------------- /deploy-example.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 4 | TEMPLATE_PATH="file://$DIR/cloud-formation.yaml" 5 | STACK_NAME="sentry-test" 6 | OWNER='ParameterKey=Owner,ParameterValue="Example Company Name"' 7 | SENTRY_USER='ParameterKey=SentryAdminUser,ParameterValue="sentry@example.com"' 8 | SENTRY_PASSWORD='ParameterKey=SentryAdminPassword,ParameterValue="CHANGE_ME"' 9 | DNS_NAME='ParameterKey=SentryPublicDnsName,ParameterValue="sentry.example.com"' 10 | SSL_CERT_ARN='ParameterKey=SSLCertARN,ParameterValue="arn:aws:acm:eu-west-1:123456789:certificate/00000000-0000-0000-0000-000000000000"' 11 | SSH_KEY_NAME='ParameterKey=KeyName,ParameterValue=ec2-ssh-key' 12 | DB_USERNAME='ParameterKey=DBMasterUsername,ParameterValue="sentrydbadmin"' 13 | DB_PASSWORD='ParameterKey=DBMasterUserPassword,ParameterValue="CHANGE_ME"' 14 | SENTRY_SECRET_KEY='ParameterKey=SentrySecretKey,ParameterValue="CHANGE_ME"' 15 | GITHUB_APP_ID='ParameterKey=SentryGithubAppId,ParameterValue="YOUR_GITHUB_APP_ID"' 16 | GITHUB_API_SECRET='ParameterKey=SentryGithubApiSecret,ParameterValue="CHANGE_ME"' 17 | MAIL_USERNAME='ParameterKey=SentryMailUsername,ParameterValue="sentrysmtpuser"' 18 | MAIL_PASSWORD='ParameterKey=SentryMailPassword,ParameterValue="CHANGE_ME"' 19 | MAIL_FROM='ParameterKey=SentryMailFrom,ParameterValue="sentry@example.com"' 20 | 21 | aws cloudformation describe-stacks --stack-name "$STACK_NAME"&>/dev/null 22 | if [ $? -eq 0 ] 23 | then 24 | echo "Updating existing stack" 25 | aws cloudformation update-stack --stack-name "$STACK_NAME" --template-body "$TEMPLATE_PATH" --parameters "$OWNER" "$SENTRY_USER" "$SENTRY_PASSWORD" "$DNS_NAME" "$SSL_CERT_ARN" "$SSH_KEY_NAME" "$DB_USERNAME" "$DB_PASSWORD" "$SENTRY_SECRET_KEY" "$GITHUB_APP_ID" "$GITHUB_API_SECRET" "$MAIL_USERNAME" "$MAIL_PASSWORD" "$MAIL_FROM" --capabilities CAPABILITY_IAM 26 | else 27 | echo "Creating new stack" 28 | aws cloudformation create-stack --stack-name "$STACK_NAME" --template-body "$TEMPLATE_PATH" --parameters "$OWNER" "$SENTRY_USER" "$SENTRY_PASSWORD" "$DNS_NAME" "$SSL_CERT_ARN" "$SSH_KEY_NAME" "$DB_USERNAME" "$DB_PASSWORD" "$SENTRY_SECRET_KEY" "$GITHUB_APP_ID" "$GITHUB_API_SECRET" "$MAIL_USERNAME" "$MAIL_PASSWORD" "$MAIL_FROM" --capabilities CAPABILITY_IAM 29 | fi 30 | -------------------------------------------------------------------------------- /roles/sentry/templates/sentry_yml.j2: -------------------------------------------------------------------------------- 1 | # While a lot of configuration in Sentry can be changed via the UI, for all 2 | # new-style config (as of 8.0) you can also declare values here in this file 3 | # to enforce defaults or to ensure they cannot be changed via the UI. For more 4 | # information see the Sentry documentation. 5 | 6 | {% if sentry_public_dns_name is defined %} 7 | system.url-prefix: 'https://{{ sentry_public_dns_name }}' 8 | {% endif %} 9 | 10 | ############### 11 | # Mail Server # 12 | ############### 13 | 14 | {% if sentry_mail_host is defined %} 15 | mail.backend: 'smtp' # Use dummy if you want to disable email entirely 16 | mail.host: '{{ sentry_mail_host }}' 17 | mail.port: {{ sentry_mail_port }} 18 | mail.username: '{{ sentry_mail_username }}' 19 | mail.password: '{{ sentry_mail_password }}' 20 | mail.use-tls: true 21 | # The email address to send on behalf of 22 | mail.from: '{{ sentry_mail_from }}' 23 | 24 | # If you'd like to configure email replies, enable this. 25 | mail.enable-replies: false 26 | {% endif %} 27 | 28 | # When email-replies are enabled, this value is used in the Reply-To header 29 | # mail.reply-hostname: '' 30 | 31 | # If you're using mailgun for inbound mail, set your API key and configure a 32 | # route to forward to /api/hooks/mailgun/inbound/ 33 | # mail.mailgun-api-key: '' 34 | 35 | ################### 36 | # System Settings # 37 | ################### 38 | 39 | # If this file ever becomes compromised, it's important to regenerate your a new key 40 | # Changing this value will result in all current sessions being invalidated. 41 | # A new key can be generated with `$ sentry config generate-secret-key` 42 | system.secret-key: '{{ sentry_secret_key }}' 43 | 44 | # The ``redis.clusters`` setting is used, unsurprisingly, to configure Redis 45 | # clusters. These clusters can be then referred to by name when configuring 46 | # backends such as the cache, digests, or TSDB backend. 47 | redis.clusters: 48 | default: 49 | hosts: 50 | 0: 51 | host: {{ sentry_redis_host }} 52 | port: {{sentry_redis_port}} 53 | 54 | ################ 55 | # File storage # 56 | ################ 57 | 58 | # Uploaded media uses these `filestore` settings. The available 59 | # backends are either `filesystem` or `s3`. 60 | 61 | {% if sentry_files_bucket_name is defined %} 62 | filestore.backend: 's3' 63 | filestore.options: 64 | bucket_name: '{{ sentry_files_bucket_name }}' 65 | {% else %} 66 | filestore.backend: 'filesystem' 67 | filestore.options: 68 | location: '/tmp/sentry-files' 69 | {% endif %} 70 | -------------------------------------------------------------------------------- /roles/sentry/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - include: nginx.yml 3 | 4 | - name: Install python libs 5 | apt: name={{ other_python_pkgs }} state=present 6 | tags: 7 | - python 8 | 9 | - name: Install all relevant files for server 10 | pip: name={{item}} executable=pip 11 | with_items: 12 | - urllib3 13 | - pyopenssl 14 | - ndg-httpsclient 15 | - pyasn1 16 | - virtualenv 17 | tags: 18 | - python 19 | 20 | - name: install python mysql bindings for mysql commands 21 | apt: name=python-mysqldb state=installed 22 | tags: 23 | - python 24 | 25 | # Supervisor 26 | 27 | - name: Install supervisor 28 | apt: pkg=supervisor state=latest 29 | tags: supervisor 30 | 31 | - name: Create supervisor conf.d directory 32 | file: 33 | path: /etc/supervisor/conf.d 34 | state: directory 35 | tags: supervisor 36 | 37 | - name: Create supervisor log directory 38 | file: 39 | path: /var/log/supervisor 40 | state: directory 41 | tags: supervisor 42 | 43 | - name: Configure supervisor globals 44 | copy: src=supervisord.conf dest=/etc/supervisord.conf 45 | tags: supervisor 46 | 47 | # Sentry install 48 | 49 | - name: Create Sentry user 50 | user: 51 | home: /home/sentry/ 52 | name: sentry 53 | state: present 54 | tags: sentry 55 | 56 | - name: Create www folder 57 | file: 58 | path: /www 59 | state: directory 60 | tags: sentry 61 | 62 | - name: Create Sentry virtualenv location 63 | command: virtualenv /www/sentry/ 64 | tags: sentry 65 | 66 | - name: Install Sentry 67 | pip: 68 | name: sentry 69 | virtualenv: /www/sentry/ve 70 | tags: sentry 71 | 72 | - name: Install Sentry GitHub auth 73 | pip: 74 | name: https://github.com/getsentry/sentry-auth-github/archive/master.zip 75 | virtualenv: /www/sentry/ve 76 | tags: sentry 77 | 78 | - name: Create Sentry configuration folder 79 | file: path=/etc/sentry/ state=directory 80 | tags: sentry 81 | 82 | - name: Install celery with redis 83 | pip: 84 | name: celery[redis] 85 | virtualenv: /www/sentry/ve 86 | tags: sentry 87 | 88 | - name: Create Sentry python config 89 | template: 90 | src: sentry_conf.j2 91 | dest: /etc/sentry/sentry.conf.py 92 | tags: sentry 93 | 94 | - name: Create Sentry config yml 95 | template: 96 | src: sentry_yml.j2 97 | dest: /etc/sentry/config.yml 98 | tags: sentry 99 | 100 | - name: Create Sentry supervisor configuration 101 | template: 102 | src: supervisor-sentry.conf.j2 103 | dest: /etc/supervisor/conf.d/sentry.conf 104 | notify: 105 | - reload supervisor 106 | tags: sentry 107 | 108 | - name: Upgrade Sentry 109 | shell: /www/sentry/ve/bin/sentry --config=/etc/sentry/ upgrade --noinput 110 | become_user: sentry 111 | tags: sentry 112 | 113 | - name: Create Sentry superuser 114 | shell: /www/sentry/ve/bin/sentry --config=/etc/sentry createuser --email {{sentry_admin_username}} --password {{sentry_admin_password}} --superuser --no-input 115 | become_user: sentry 116 | ignore_errors: yes 117 | tags: sentry 118 | 119 | - name: Update Sentry nginx config 120 | template: 121 | src: nginx-sentry.conf.j2 122 | dest: '{{nginx.main_dir}}/sites-enabled/default' 123 | notify: 124 | - Restart nginx 125 | tags: sentry 126 | 127 | - name: Start Sentry 128 | service: 129 | name: supervisor 130 | state: restarted 131 | tags: sentry 132 | -------------------------------------------------------------------------------- /roles/sentry/templates/sentry_conf.j2: -------------------------------------------------------------------------------- 1 | # This file is just Python, with a touch of Django which means 2 | # you can inherit and tweak settings to your hearts content. 3 | from sentry.conf.server import * 4 | 5 | import os.path 6 | 7 | CONF_ROOT = os.path.dirname(__file__) 8 | 9 | DATABASES = { 10 | 'default': { 11 | 'ENGINE': 'sentry.db.postgres', 12 | 'NAME': '{{ sentry_db_name }}', 13 | 'USER': '{{ sentry_db_user }}', 14 | 'PASSWORD': '{{ sentry_db_password }}', 15 | 'HOST': '{{sentry_db_host}}', 16 | 'PORT': '{{sentry_db_port}}', 17 | 'AUTOCOMMIT': True, 18 | 'ATOMIC_REQUESTS': False, 19 | } 20 | } 21 | 22 | # You should not change this setting after your database has been created 23 | # unless you have altered all schemas first 24 | SENTRY_USE_BIG_INTS = True 25 | 26 | # If you're expecting any kind of real traffic on Sentry, we highly recommend 27 | # configuring the CACHES and Redis settings 28 | 29 | ########### 30 | # General # 31 | ########### 32 | 33 | # Instruct Sentry that this install intends to be run by a single organization 34 | # and thus various UI optimizations should be enabled. 35 | SENTRY_SINGLE_ORGANIZATION = True 36 | DEBUG = False 37 | 38 | ######### 39 | # Cache # 40 | ######### 41 | 42 | # Sentry currently utilizes two separate mechanisms. While CACHES is not a 43 | # requirement, it will optimize several high throughput patterns. 44 | 45 | # If you wish to use memcached, install the dependencies and adjust the config 46 | # as shown: 47 | # 48 | # pip install python-memcached 49 | # 50 | # CACHES = { 51 | # 'default': { 52 | # 'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache', 53 | # 'LOCATION': ['127.0.0.1:11211'], 54 | # } 55 | # } 56 | 57 | # A primary cache is required for things such as processing events 58 | SENTRY_CACHE = 'sentry.cache.redis.RedisCache' 59 | 60 | ######### 61 | # Queue # 62 | ######### 63 | 64 | # See https://docs.sentry.io/on-premise/server/queue/ for more 65 | # information on configuring your queue broker and workers. Sentry relies 66 | # on a Python framework called Celery to manage queues. 67 | 68 | BROKER_URL = 'redis://{{ sentry_redis_host }}:{{sentry_redis_port}}' 69 | 70 | ############### 71 | # Rate Limits # 72 | ############### 73 | 74 | # Rate limits apply to notification handlers and are enforced per-project 75 | # automatically. 76 | 77 | SENTRY_RATELIMITER = 'sentry.ratelimits.redis.RedisRateLimiter' 78 | 79 | ################## 80 | # Update Buffers # 81 | ################## 82 | 83 | # Buffers (combined with queueing) act as an intermediate layer between the 84 | # database and the storage API. They will greatly improve efficiency on large 85 | # numbers of the same events being sent to the API in a short amount of time. 86 | # (read: if you send any kind of real data to Sentry, you should enable buffers) 87 | 88 | SENTRY_BUFFER = 'sentry.buffer.redis.RedisBuffer' 89 | 90 | ########## 91 | # Quotas # 92 | ########## 93 | 94 | # Quotas allow you to rate limit individual projects or the Sentry install as 95 | # a whole. 96 | 97 | SENTRY_QUOTAS = 'sentry.quotas.redis.RedisQuota' 98 | 99 | ######## 100 | # TSDB # 101 | ######## 102 | 103 | # The TSDB is used for building charts as well as making things like per-rate 104 | # alerts possible. 105 | 106 | SENTRY_TSDB = 'sentry.tsdb.redis.RedisTSDB' 107 | 108 | ########### 109 | # Digests # 110 | ########### 111 | 112 | # The digest backend powers notification summaries. 113 | 114 | SENTRY_DIGESTS = 'sentry.digests.backends.redis.RedisBackend' 115 | 116 | ############## 117 | # Web Server # 118 | ############## 119 | 120 | # If you're using a reverse SSL proxy, you should enable the X-Forwarded-Proto 121 | # header and uncomment the following settings 122 | SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') 123 | SESSION_COOKIE_SECURE = True 124 | CSRF_COOKIE_SECURE = True 125 | 126 | # If you're not hosting at the root of your web server, 127 | # you need to uncomment and set it to the path where Sentry is hosted. 128 | # FORCE_SCRIPT_NAME = '/sentry' 129 | 130 | SENTRY_WEB_HOST = '0.0.0.0' 131 | SENTRY_WEB_PORT = 9000 132 | SENTRY_WEB_OPTIONS = { 133 | # 'workers': 3, # the number of web workers 134 | # 'protocol': 'uwsgi', # Enable uwsgi protocol instead of http 135 | } 136 | 137 | 138 | ################### 139 | # Configure Users # 140 | ################### 141 | 142 | SENTRY_FEATURES['auth:register'] = False 143 | {% if sentry_github_app_id is defined %} 144 | GITHUB_APP_ID = "{{ sentry_github_app_id }}" 145 | GITHUB_API_SECRET = "{{ sentry_github_api_secret }}" 146 | GITHUB_REQUIRE_VERIFIED_EMAIL = True 147 | {% endif %} 148 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # What is it 2 | 3 | An attempt to follow security best-practices to create a production-ready installation of Sentry.io on AWS. 4 | 5 | Features: 6 | - HTTPS end-to-end 7 | - Load-balanced, Multi-AZ setup 8 | - EC2 Auto-scaling group (also gives zero-downtime upgrades) 9 | - Creates own VPC 10 | - Encryption using integrated KMS key 11 | - Limit sentry access to your GitHub organisation 12 | 13 | ## Getting Started 14 | 15 | Choose from the templates below and either click "Launch Stack" or use ["Create Stack" in the CloudFormation AWS console](https://console.aws.amazon.com/cloudformation/home?#/stacks/new) and specify the relevant Amazon S3 template URL. 16 | 17 | | Name | Click to launch | S3 Link | 18 | |-----------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------| 19 | | Internet-facing setup in 1 availability zone | [![Launch Stack](assets/launch-stack.png)](https://console.aws.amazon.com/cloudformation/home#/stacks/new?stackName=sentry&templateURL=https:%2F%2Fs3-eu-west-1.amazonaws.com%2Fsentry-formation%2Ftemplates%2Fmaster-internet-facing-1az.yaml) | `https://s3-eu-west-1.amazonaws.com/sentry-formation/templates/master-internet-facing-1az.yaml` | 20 | | Internet-facing setup in 2 availability zones | [![Launch Stack](assets/launch-stack.png)](https://console.aws.amazon.com/cloudformation/home#/stacks/new?stackName=sentry&templateURL=https:%2F%2Fs3-eu-west-1.amazonaws.com%2Fsentry-formation%2Ftemplates%2Fmaster-internet-facing-2az.yaml) | `https://s3-eu-west-1.amazonaws.com/sentry-formation/templates/master-internet-facing-2az.yaml` | 21 | | Internet-facing setup in 3 availability zones | [![Launch Stack](assets/launch-stack.png)](https://console.aws.amazon.com/cloudformation/home#/stacks/new?stackName=sentry&templateURL=https:%2F%2Fs3-eu-west-1.amazonaws.com%2Fsentry-formation%2Ftemplates%2Fmaster-internet-facing-3az.yaml) | `https://s3-eu-west-1.amazonaws.com/sentry-formation/templates/master-internet-facing-3az.yaml` | 22 | | Internal setup in 1 availability zone | [![Launch Stack](assets/launch-stack.png)](https://console.aws.amazon.com/cloudformation/home#/stacks/new?stackName=sentry&templateURL=https:%2F%2Fs3-eu-west-1.amazonaws.com%2Fsentry-formation%2Ftemplates%2Fmaster-internal-1az.yaml) | `https://s3-eu-west-1.amazonaws.com/sentry-formation/templates/master-internal-1az.yaml` | 23 | | Internal setup in 2 availability zones | [![Launch Stack](assets/launch-stack.png)](https://console.aws.amazon.com/cloudformation/home#/stacks/new?stackName=sentry&templateURL=https:%2F%2Fs3-eu-west-1.amazonaws.com%2Fsentry-formation%2Ftemplates%2Fmaster-internal-2az.yaml) | `https://s3-eu-west-1.amazonaws.com/sentry-formation/templates/master-internal-2az.yaml` | 24 | | Internal setup in 3 availability zones | [![Launch Stack](assets/launch-stack.png)](https://console.aws.amazon.com/cloudformation/home#/stacks/new?stackName=sentry&templateURL=https:%2F%2Fs3-eu-west-1.amazonaws.com%2Fsentry-formation%2Ftemplates%2Fmaster-internal-3az.yaml) | `https://s3-eu-west-1.amazonaws.com/sentry-formation/templates/master-internal-3az.yaml` | 25 | 26 | If you don't want your new instances to automatically pull down new version of the setup scripts, then change the filename, switching `master` for the specific version you want to stick with e.g. `1.0.0`. 27 | 28 | ### What you need to provide 29 | 30 | - User accounts and secrets for Sentry, Redis and Postgres 31 | - DNS name: the hostname you're going to use to access your sentry installation. 32 | - SSL Certificate ARN: A certificate matching your DNS name that you've stored in KMS (see [Importing Certificates](http://docs.aws.amazon.com/acm/latest/userguide/import-certificate.html)) 33 | - SMTP email server for sending alerts (see [Using the Amazon SES SMTP Interface to Send Email](http://docs.aws.amazon.com/ses/latest/DeveloperGuide/send-email-smtp.html)) 34 | - GitHub App Id & API secret (if using GitHub to sign in). 35 | 36 | ### After first deployment 37 | 38 | Once the load balancer has been created, you can update your DNS entry. See [Routing Traffic to an ELB Load Balancer](http://docs.aws.amazon.com/Route53/latest/DeveloperGuide/routing-to-elb-load-balancer.html) if using Route 53. 39 | 40 | If you want to also encrypt your EC2 EBS volumes, you can make a copy of the original AMI, add encryption using the created `SentryEncryptionKey`. Then update your stack to the new encrypted AMI, which will provision new EC2 instances and remove the old instances. 41 | 42 | ### Automating Deployments 43 | 44 | This is a great option if you want to automatically deploy your stack from your CI server: 45 | 46 | 1. Take a copy of `deploy-example.sh`. 47 | 2. Fill in the parameters. 48 | 49 | ## Contributing 50 | 51 | ### Building templates 52 | 53 | Requires Ruby & Bundler to be installed locally. 54 | 55 | ``` 56 | bundle install 57 | lono generate 58 | ``` 59 | 60 | ### Running locally via vagrant 61 | 62 | Requires Vagrant and Ansible to be installed locally. 63 | 64 | ``` 65 | vagrant up 66 | ansible-playbook site.yml -u vagrant 67 | ``` 68 | 69 | Your site should then be available at https://33.33.33.20/ 70 | 71 | ## Credits 72 | 73 | Original version developed by Karl Turner (@otaiga), Bradley Allen (@ValkyrieUK) and Daniel Bradley (@danielrbradley). 74 | 75 | [Using AWS KMS to Encrypt Values in CloudFormation Stacks](https://ben.fogbutter.com/2016/02/22/using-kms-to-encrypt-cloud-formation-values.html) by Ben Jones (@RealSalmon) 76 | 77 | Starting point for CloudFormation setup: https://github.com/acervos/sentry 78 | 79 | ## CloudFormation Architecture Diagram 80 | 81 | ![CloudFormation designer export](assets/cloud-formation-designer.png) 82 | -------------------------------------------------------------------------------- /roles/common/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Upgrade apt packages 3 | apt: upgrade=dist 4 | update_cache=yes 5 | 6 | - name: Install NTP 7 | apt: pkg=ntp 8 | 9 | - name: Install prerequisite packages 10 | apt: pkg={{ item }} 11 | with_items: 12 | - autoconf 13 | - build-essential 14 | - python-setuptools 15 | - python-software-properties 16 | - python-dev 17 | - python-pip 18 | - libncurses-dev 19 | 20 | # CIS CAT secuirty configurations 21 | 22 | - name: crontab owner and group 23 | shell: "chown root:root /etc/cron* && sudo chmod og-rwx /etc/cron*" 24 | become: true 25 | tags: 26 | - cis_cat_security 27 | - crontab 28 | 29 | - name: Check that the at.allow exists 30 | stat: path=/etc/at.allow 31 | register: at_allow 32 | tags: 33 | - cis_cat_security 34 | - cron 35 | 36 | - name: restrict cron to users 37 | shell: "rm /etc/*.deny && touch /etc/cron.allow && touch /etc/at.allow && chmod og-rwx /etc/cron.allow && chmod og-rwx /etc/cron.allow && chmod og-rwx /etc/at.allow && sudo chown root:root /etc/cron.allow && sudo chown root:root /etc/at.allow" 38 | become: true 39 | when: at_allow.stat.exists == False 40 | tags: 41 | - cis_cat_security 42 | - cron 43 | 44 | - name: ssh config permissisons 45 | shell: "chown root:root /etc/ssh/sshd_config && sudo chmod 600 /etc/ssh/sshd_config" 46 | become: true 47 | tags: 48 | - cis_cat_security 49 | - ssh 50 | 51 | - name: ssh x11 forwarding 52 | lineinfile: dest=/etc/ssh/sshd_config regexp=^X11Forwarding line="X11Forwarding no" 53 | become: true 54 | tags: 55 | - cis_cat_security 56 | - ssh 57 | 58 | - name: ssh max auth tries 59 | lineinfile: dest=/etc/ssh/sshd_config regexp=^MaxAuthTries line="MaxAuthTries 4" 60 | become: true 61 | tags: 62 | - cis_cat_security 63 | - ssh 64 | 65 | - name: diable root login 66 | lineinfile: dest=/etc/ssh/sshd_config regexp=^PermitRootLogin line="PermitRootLogin no" 67 | become: true 68 | tags: 69 | - cis_cat_security 70 | - ssh 71 | 72 | - name: diable user environments 73 | lineinfile: dest=/etc/ssh/sshd_config regexp=^PermitUserEnvironment line="PermitUserEnvironment no" 74 | become: true 75 | tags: 76 | - cis_cat_security 77 | - ssh 78 | 79 | - name: limit ssh ciphers 80 | lineinfile: dest=/etc/ssh/sshd_config regexp=^Ciphers line="Ciphers aes128-ctr,aes192-ctr,aes256-ctr" 81 | become: true 82 | tags: 83 | - cis_cat_security 84 | - ssh 85 | 86 | - name: ssh ClientAliveCountMax times 87 | lineinfile: dest=/etc/ssh/sshd_config regexp=^ClientAliveCountMax line="ClientAliveCountMax 0" state=present 88 | become: true 89 | tags: 90 | - cis_cat_security 91 | - ssh 92 | 93 | - name: ssh ClientAliveInterval times 94 | lineinfile: dest=/etc/ssh/sshd_config regexp=^ClientAliveInterval line="ClientAliveInterval 300" state=present 95 | become: true 96 | tags: 97 | - cis_cat_security 98 | - ssh 99 | 100 | - name: update host names 101 | lineinfile: dest=/etc/ssh/sshd_config regexp=^AllowUsers line="AllowUsers {{user}}" state=present 102 | become: true 103 | tags: 104 | - cis_cat_security 105 | - ssh 106 | 107 | - name: IPV6 router ads all 108 | lineinfile: dest=/etc/sysctl.conf regexp=^net.ipv6.conf.all.accept_ra line="net.ipv6.conf.all.accept_ra=0" state=present 109 | become: true 110 | tags: 111 | - cis_cat_security 112 | - sysctl 113 | 114 | - name: IPV6 router ads default 115 | lineinfile: dest=/etc/sysctl.conf regexp=^net.ipv6.conf.default.accept_ra line="net.ipv6.conf.default.accept_ra = 0" state=present 116 | become: true 117 | tags: 118 | - cis_cat_security 119 | - sysctl 120 | 121 | - name: IPV6 router ads kernal 122 | shell: "sysctl -w net.ipv6.conf.all.accept_ra=0 && sysctl -w net.ipv6.conf.default.accept_ra=0 && sysctl -w net.ipv6.route.flush=1" 123 | become: true 124 | tags: 125 | - cis_cat_security 126 | - sysctl 127 | 128 | - name: IPV6 redirect acceptance 129 | lineinfile: dest=/etc/sysctl.conf regexp=^net.ipv6.conf.default.accept_redirects line="net.ipv6.conf.default.accept_redirects = 0" 130 | become: true 131 | tags: 132 | - cis_cat_security 133 | - sysctl 134 | 135 | - name: IPV6 redirect acceptance kernal 136 | shell: "sysctl -w net.ipv6.conf.all.accept_redirects=0 && sysctl -w net.ipv6.conf.default.accept_redirects=0 && sysctl -w net.ipv6.route.flush=1" 137 | become: true 138 | tags: 139 | - cis_cat_security 140 | - sysctl 141 | 142 | - name: IPV6 disable 143 | lineinfile: dest=/etc/sysctl.conf regexp=^net.ipv6.conf.lo.disable_ipv6 line="net.ipv6.conf.lo.disable_ipv6 = 1" 144 | become: true 145 | notify: Restart Sysctl 146 | tags: 147 | - cis_cat_security 148 | - sysctl 149 | 150 | - name: ICMP redirect acceptance all 151 | lineinfile: dest=/etc/sysctl.conf regexp=^net.ipv4.conf.all.secure_redirects line="net.ipv4.conf.all.secure_redirects = 0" 152 | become: true 153 | tags: 154 | - cis_cat_security 155 | - sysctl 156 | 157 | - name: ICMP redirect acceptance defaults 158 | lineinfile: dest=/etc/sysctl.conf regexp=^net.ipv4.conf.default.secure_redirects line="net.ipv4.conf.default.secure_redirects = 0" 159 | become: true 160 | tags: 161 | - cis_cat_security 162 | - sysctl 163 | 164 | - name: ICMP redirect acceptance kernal 165 | shell: "sysctl -w net.ipv4.conf.all.secure_redirects=0 && sysctl -w net.ipv4.conf.default.secure_redirects=0 && sysctl -w net.ipv4.route.flush=1" 166 | become: true 167 | tags: 168 | - cis_cat_security 169 | - sysctl 170 | 171 | - name: Log suspicious packets defaults 172 | lineinfile: dest=/etc/sysctl.conf regexp=^net.ipv4.conf.default.log_martians line="net.ipv4.conf.default.log_martians = 1" state=present 173 | become: true 174 | tags: 175 | - cis_cat_security 176 | - sysctl 177 | 178 | - name: Log suspicious packets all 179 | lineinfile: dest=/etc/sysctl.conf regexp=^net.ipv4.conf.all.log_martians line="net.ipv4.conf.all.log_martians = 1" state=present 180 | become: true 181 | tags: 182 | - cis_cat_security 183 | - sysctl 184 | 185 | - name: Log suspicious packets kernal 186 | shell: "sysctl -w net.ipv4.conf.all.log_martians=1 && sysctl -w net.ipv4.conf.default.log_martians=1 && sysctl -w net.ipv4.route.flush=1" 187 | become: true 188 | tags: 189 | - cis_cat_security 190 | - sysctl 191 | 192 | - name: Enable SYN cookies 193 | lineinfile: dest=/etc/sysctl.conf regexp=^net.ipv4.tcp_syncookies line="net.ipv4.tcp_syncookies = 1" state=present 194 | become: true 195 | tags: 196 | - cis_cat_security 197 | - sysctl 198 | 199 | - name: Enable SYN cookies kernal 200 | shell: "sysctl -w net.ipv4.tcp_syncookies=1 && sysctl -w net.ipv4.route.flush=1" 201 | become: true 202 | tags: 203 | - cis_cat_security 204 | - sysctl 205 | 206 | - name: Disable packets redirects defaults 207 | lineinfile: dest=/etc/sysctl.conf regexp=^net.ipv4.conf.default.send_redirects line="net.ipv4.conf.default.send_redirects = 0" state=present 208 | become: true 209 | tags: 210 | - cis_cat_security 211 | - sysctl 212 | 213 | - name: Disable packets redirects all 214 | lineinfile: dest=/etc/sysctl.conf regexp=^net.ipv4.conf.all.send_redirects line="net.ipv4.conf.all.send_redirects = 0" state=present 215 | become: true 216 | tags: 217 | - cis_cat_security 218 | - sysctl 219 | 220 | - name: Disable packets redirects kernal 221 | shell: "sysctl -w net.ipv4.conf.all.send_redirects=0 && sysctl -w net.ipv4.conf.default.send_redirects=0 && sysctl -w net.ipv4.route.flush=1" 222 | become: true 223 | tags: 224 | - cis_cat_security 225 | - sysctl 226 | 227 | 228 | - name: core dumps sysctl 229 | lineinfile: dest=/etc/sysctl.conf line="fs.suid_dumpable = 0" state=present 230 | tags: 231 | - cis_cat_security 232 | - sysctl 233 | 234 | - name: create hosts deny file 235 | file: path=/etc/hosts.deny state=touch owner=root group=root mode=644 236 | tags: 237 | - cis_cat_security 238 | - hosts 239 | 240 | - name: Create CIS conf file 241 | template: src=cis_conf.j2 dest=/etc/modprobe.d/CIS.conf 242 | become: true 243 | tags: 244 | - cis_cat_security 245 | - cis_conf 246 | 247 | - name: bootloader config permissisons 248 | shell: chmod og-rwx /boot/grub/grub.cfg 249 | become: true 250 | tags: 251 | - cis_cat_security 252 | - bootloader 253 | 254 | - name: install ubuntu Firewall 255 | apt: name=ufw state=present update_cache=yes 256 | tags: 257 | - cis_cat_security 258 | - ufw 259 | 260 | - name: enable ufw 261 | shell: yes | ufw enable 262 | tags: 263 | - cis_cat_security 264 | - ufw 265 | 266 | - name: enable ufw ports 267 | shell: "sudo ufw allow 22/tcp && sudo ufw allow 8080/tcp && sudo ufw allow 443/tcp" 268 | tags: 269 | - cis_cat_security 270 | - ufw 271 | 272 | - name: Create MOTD on issue.net 273 | template: src=motd_banner.j2 dest=/etc/issue.net owner=root group=root mode=644 274 | become: true 275 | tags: 276 | - cis_cat_security 277 | - motd 278 | 279 | - name: Create MOTD on motd 280 | template: src=motd_banner.j2 dest=/etc/motd owner=root group=root mode=644 281 | become: true 282 | tags: 283 | - cis_cat_security 284 | - motd 285 | 286 | - name: set ssh banner 287 | shell: "sed -i '/^#Banner /s/^#//' /etc/ssh/sshd_config" 288 | become: true 289 | tags: 290 | - cis_cat_security 291 | - motd 292 | 293 | - name: disable x window 294 | apt: 295 | name: xserver-xorg-core* 296 | state: absent 297 | tags: 298 | - cis_cat_security 299 | - x_window 300 | 301 | - name: disable apport 302 | lineinfile: dest=/etc/init/apport.conf regexp="^env enabled" line="env enabled=0" 303 | become: true 304 | tags: 305 | - cis_cat_security 306 | - apport 307 | 308 | - name: disable apport service 309 | service: name=apport enabled=no state=stopped 310 | become: true 311 | tags: 312 | - cis_cat_security 313 | - apport 314 | 315 | 316 | - name: disable rpc bind 317 | lineinfile: dest=/etc/init/rpcbind-boot.conf line="start on virtual-filesystems and net-device-up IFACE=lo" state=absent 318 | tags: 319 | - cis_cat_security 320 | - rpcbind-boot 321 | 322 | 323 | - name: restrict core dumps 324 | lineinfile: dest=/etc/security/limits.conf line="* hard core 0" state=present 325 | tags: 326 | - cis_cat_security 327 | - core_dumps 328 | 329 | 330 | - name: limit password reuse 331 | lineinfile: dest=/etc/pam.d/common-password line="password sufficient pam_unix.so remember=5" state=present 332 | tags: 333 | - cis_cat_security 334 | - password_limits 335 | 336 | - name: password complexity 337 | lineinfile: dest=/etc/pam.d/common-password line="password required pam_cracklib.so retry=3 minlen=14 dcredit=-1 ucredit=-1 ocredit=-1 lcredit=-1" state=present 338 | tags: 339 | - cis_cat_security 340 | - password_limits 341 | 342 | - name: password failed lockout 343 | lineinfile: dest=/etc/pam.d/login line="auth required pam_tally2.so onerr=fail audit silent deny=5 unlock_time=900" state=present 344 | tags: 345 | - cis_cat_security 346 | - password_limits 347 | 348 | - name: disable cramfs 349 | lineinfile: dest=/etc/modprobe.d/CIS.conf line="install cramfs /bin/true" state=present 350 | tags: 351 | - cis_cat_security 352 | - disable_file_systems 353 | 354 | - name: disable freevxfs 355 | lineinfile: dest=/etc/modprobe.d/CIS.conf line="install freevxfs /bin/true" state=present 356 | tags: 357 | - cis_cat_security 358 | - disable_file_systems 359 | 360 | - name: disable jffs2 361 | lineinfile: dest=/etc/modprobe.d/CIS.conf line="install jffs2 /bin/true" state=present 362 | tags: 363 | - cis_cat_security 364 | - disable_file_systems 365 | 366 | - name: disable hfs 367 | lineinfile: dest=/etc/modprobe.d/CIS.conf line="install hfs /bin/true" state=present 368 | tags: 369 | - cis_cat_security 370 | - disable_file_systems 371 | 372 | - name: disable hfsplus 373 | lineinfile: dest=/etc/modprobe.d/CIS.conf line="install hfsplus /bin/true" state=present 374 | tags: 375 | - cis_cat_security 376 | - disable_file_systems 377 | 378 | - name: disable squashfs 379 | lineinfile: dest=/etc/modprobe.d/CIS.conf line="install squashfs /bin/true" state=present 380 | tags: 381 | - cis_cat_security 382 | - disable_file_systems 383 | 384 | - name: disable udf 385 | lineinfile: dest=/etc/modprobe.d/CIS.conf line="install udf /bin/true" state=present 386 | tags: 387 | - cis_cat_security 388 | - disable_file_systems 389 | 390 | - name: disable FAT 391 | lineinfile: dest=/etc/modprobe.d/CIS.conf line="install vfat /bin/true" state=present 392 | tags: 393 | - cis_cat_security 394 | - disable_file_systems 395 | 396 | - name: uninstall telnet 397 | apt: name=telnet state=absent update_cache=yes 398 | tags: 399 | - cis_cat_security 400 | - disable_telnet -------------------------------------------------------------------------------- /output/1.0.0-internal-1az.yaml: -------------------------------------------------------------------------------- 1 | # This file was generated with lono. Do not edit directly, the changes will be lost. 2 | # More info: https://github.com/tongueroo/lono 3 | Description: Sentry.io internal setup in 1 availability zones 4 | Parameters: 5 | #### Required #### 6 | Owner: 7 | Type: String 8 | Description: Name of the owner of the service (normally your company name) 9 | DBMasterUsername: 10 | Type: String 11 | MinLength: '1' 12 | MaxLength: '16' 13 | AllowedPattern: "[a-zA-Z][a-zA-Z0-9]*" 14 | Description: Username for database access 15 | DBMasterUserPassword: 16 | NoEcho: true 17 | Type: String 18 | Description: Password for database access - minimum 8 characters 19 | MinLength: '8' 20 | MaxLength: '41' 21 | AllowedPattern: "[a-zA-Z0-9]*" 22 | ConstraintDescription: must contain 8 alphanumeric characters. 23 | SSLCertARN: 24 | Description: The ARN of the ACM cert 25 | Type: String 26 | KeyName: 27 | Description: Name of existing EC2 keypair to enable SSH access to the created 28 | instances 29 | Type: AWS::EC2::KeyPair::KeyName 30 | SentryAdminUser: 31 | Type: String 32 | MinLength: '1' 33 | MaxLength: '30' 34 | Description: Username for root sentry access 35 | SentryAdminPassword: 36 | NoEcho: true 37 | Type: String 38 | Description: Password for root sentry access - minimum 20 characters 39 | MinLength: '20' 40 | SentryPublicDnsName: 41 | Type: String 42 | Description: Host name that users will type to get to sentry. 43 | SentrySecretKey: 44 | Type: String 45 | Description: Private key for encrypting user sessions. 46 | MinLength: '50' 47 | NoEcho: true 48 | SentryGithubAppId: 49 | Description: GitHub API App ID for SSO 50 | Type: String 51 | SentryGithubApiSecret: 52 | Description: GitHub API secret key for SSO 53 | Type: String 54 | NoEcho: true 55 | SentryMailUsername: 56 | Description: SMTP username for sentry to use to send email 57 | Type: String 58 | SentryMailPassword: 59 | Description: SMTP password for sentry to use to send email 60 | Type: String 61 | NoEcho: true 62 | SentryMailFrom: 63 | Description: Sending email address for sentry to use to send email 64 | Type: String 65 | #### Optional #### 66 | VpcCidr: 67 | AllowedPattern: '((\d{1,3})\.){3}\d{1,3}/\d{1,2}' 68 | Description: VPC Cidr block 69 | Default: 10.0.0.0/22 70 | Type: String 71 | VpcAvailabilityZone1: 72 | Description: The AvailabilityZone to use for subnet 1 73 | Type: AWS::EC2::AvailabilityZone::Name 74 | Default: eu-west-1a 75 | VpcPublicSubnetCIDR1: 76 | AllowedPattern: '((\d{1,3})\.){3}\d{1,3}/\d{1,2}' 77 | Default: 10.0.0.0/26 78 | Description: VPC CIDR Block for Public Subnet 1 79 | Type: String 80 | VpcPrivateSubnetCIDR1: 81 | AllowedPattern: '((\d{1,3})\.){3}\d{1,3}/\d{1,2}' 82 | Default: 10.0.2.0/26 83 | Description: VPC CIDR Block for Private Subnet 1 84 | Type: String 85 | ImageId: 86 | Description: The AMI to use for this Sentry - YUM compliant required 87 | Type: String 88 | Default: ami-6d48500b 89 | InstanceType: 90 | Description: The size of the EC2 instances 91 | Default: t2.medium 92 | Type: String 93 | DBName: 94 | Type: String 95 | Default: sentry 96 | MinLength: '1' 97 | MaxLength: '64' 98 | AllowedPattern: "[a-zA-Z][a-zA-Z0-9]*" 99 | ConstraintDescription: must begin with a letter and contain only alphanumeric 100 | characters. 101 | DBAllocatedStorage: 102 | Type: String 103 | Default: '5' 104 | DBBackupRetentionPeriod: 105 | Type: String 106 | Default: '7' 107 | DBInstanceClass: 108 | Type: String 109 | Default: db.t2.small 110 | DBMultiAZ: 111 | Type: String 112 | Default: false 113 | Description: Provides enhanced availablily for RDS 114 | DBStorageType: 115 | Type: String 116 | Default: gp2 117 | RedisEngineVersion: 118 | Type: String 119 | Description: Version of Redis engine to use. 120 | Default: '3.2.4' 121 | RedisNodeType: 122 | Type: String 123 | Default: cache.t2.small 124 | RedisNumNodes: 125 | Type: String 126 | Default: '1' 127 | SentryMailHost: 128 | Description: SMTP host for sentry to use to send email 129 | Type: String 130 | Default: email-smtp.eu-west-1.amazonaws.com 131 | SentryMailPort: 132 | Description: SMTP port for sentry to use to send email 133 | Type: String 134 | Default: '25' 135 | ScalingMinNodes: 136 | Type: Number 137 | Description: Minium size of the auto scaling group 138 | Default: 1 139 | ScalingMaxNodes: 140 | Type: Number 141 | Description: Maximum size of the auto scaling group 142 | Default: 2 143 | AWSTemplateFormatVersion: '2010-09-09' 144 | Resources: 145 | ###### Network ###### 146 | VPC: 147 | Type: 'AWS::EC2::VPC' 148 | Properties: 149 | CidrBlock: 150 | Ref: VpcCidr 151 | EnableDnsHostnames: true 152 | Tags: 153 | - Key: Name 154 | Value: 155 | Ref: AWS::StackName 156 | PublicSubnet1: 157 | Type: AWS::EC2::Subnet 158 | Properties: 159 | AvailabilityZone: {Ref: VpcAvailabilityZone1} 160 | CidrBlock: {Ref: VpcPublicSubnetCIDR1} 161 | MapPublicIpOnLaunch: true 162 | Tags: 163 | - Key: Name 164 | Value: 165 | Fn::Join: 166 | - '-' 167 | - [{Ref: 'AWS::StackName'}, 'public', {Ref: VpcAvailabilityZone1}] 168 | VpcId: {Ref: VPC} 169 | PrivateSubnet1: 170 | Type: AWS::EC2::Subnet 171 | Properties: 172 | AvailabilityZone: {Ref: VpcAvailabilityZone1} 173 | CidrBlock: {Ref: VpcPrivateSubnetCIDR1} 174 | MapPublicIpOnLaunch: false 175 | Tags: 176 | - Key: Name 177 | Value: 178 | Fn::Join: 179 | - '-' 180 | - [{Ref: 'AWS::StackName'}, 'private', {Ref: VpcAvailabilityZone1}] 181 | VpcId: {Ref: VPC} 182 | InternetGateway: 183 | Type: AWS::EC2::InternetGateway 184 | Properties: 185 | Tags: 186 | - Key: Name 187 | Value: 188 | Ref: AWS::StackName 189 | GatewayAttachment: 190 | Type: AWS::EC2::VPCGatewayAttachment 191 | Properties: 192 | InternetGatewayId: 193 | Ref: InternetGateway 194 | VpcId: 195 | Ref: VPC 196 | ###### Public Routing ###### 197 | PublicRouteTable: 198 | Type: AWS::EC2::RouteTable 199 | Properties: 200 | Tags: 201 | - Key: Name 202 | Value: 203 | Fn::Join: 204 | - '-' 205 | - [{Ref: 'AWS::StackName'}, 'public'] 206 | VpcId: 207 | Ref: VPC 208 | PublicRoute: 209 | Type: AWS::EC2::Route 210 | Properties: 211 | DestinationCidrBlock: 0.0.0.0/0 212 | GatewayId: 213 | Ref: InternetGateway 214 | RouteTableId: 215 | Ref: PublicRouteTable 216 | PublicSubnetAssoc1: 217 | Type: AWS::EC2::SubnetRouteTableAssociation 218 | Properties: 219 | RouteTableId: 220 | Ref: PublicRouteTable 221 | SubnetId: 222 | Ref: PublicSubnet1 223 | ###### Private Routing ###### 224 | PrivateRouteTable1: 225 | Type: AWS::EC2::RouteTable 226 | Properties: 227 | Tags: 228 | - Key: Name 229 | Value: 230 | Fn::Join: 231 | - '-' 232 | - [{Ref: 'AWS::StackName'}, 'private-1'] 233 | VpcId: 234 | Ref: VPC 235 | PrivateSubnetAssoc1: 236 | Type: AWS::EC2::SubnetRouteTableAssociation 237 | Properties: 238 | RouteTableId: 239 | Ref: PrivateRouteTable1 240 | SubnetId: 241 | Ref: PrivateSubnet1 242 | EIP1: 243 | Type: AWS::EC2::EIP 244 | Properties: 245 | Domain: vpc 246 | NAT1: 247 | DependsOn: GatewayAttachment 248 | Type: AWS::EC2::NatGateway 249 | Properties: 250 | AllocationId: 251 | Fn::GetAtt: 252 | - EIP1 253 | - AllocationId 254 | SubnetId: 255 | Ref: PublicSubnet1 256 | NATRoute1: 257 | Type: AWS::EC2::Route 258 | Properties: 259 | RouteTableId: 260 | Ref: PrivateRouteTable1 261 | DestinationCidrBlock: 0.0.0.0/0 262 | NatGatewayId: 263 | Ref: NAT1 264 | ###### Roles ###### 265 | RootEncryptionRole: 266 | Type: AWS::IAM::Role 267 | Properties: 268 | AssumeRolePolicyDocument: 269 | Version: "2012-10-17" 270 | Statement: 271 | - Effect: "Allow" 272 | Principal: 273 | AWS: "*" 274 | Action: 275 | - "sts:AssumeRole" 276 | Path: "/" 277 | LambdaExecutionRole: 278 | Type: AWS::IAM::Role 279 | Properties: 280 | AssumeRolePolicyDocument: 281 | Version: '2012-10-17' 282 | Statement: 283 | - Effect: Allow 284 | Principal: 285 | Service: 286 | - lambda.amazonaws.com 287 | Action: 288 | - sts:AssumeRole 289 | Path: "/" 290 | Policies: 291 | - PolicyName: root 292 | PolicyDocument: 293 | Version: '2012-10-17' 294 | Statement: 295 | - Effect: Allow 296 | Action: 297 | - logs:* 298 | Resource: arn:aws:logs:*:*:* 299 | SentryRole: 300 | Type: AWS::IAM::Role 301 | Properties: 302 | AssumeRolePolicyDocument: 303 | Version: "2012-10-17" 304 | Statement: 305 | - Effect: Allow 306 | Principal: 307 | Service: 308 | - ec2.amazonaws.com 309 | Action: 310 | - sts:AssumeRole 311 | Path: "/" 312 | Policies: 313 | - PolicyName: root 314 | PolicyDocument: 315 | Version: "2012-10-17" 316 | Statement: 317 | - Effect: Allow 318 | Action: 319 | - s3:PutObject 320 | - s3:GetObject 321 | - s3:DeleteObject 322 | Resource: 323 | - Fn::Join: 324 | - '' 325 | - - !GetAtt SentryFilesS3Bucket.Arn 326 | - "/*" 327 | ###### Encryption ###### 328 | SentryEncryptionKey: 329 | Type: "AWS::KMS::Key" 330 | Properties: 331 | Description: Sentry environment root encryption key 332 | KeyPolicy: 333 | Version: "2012-10-17" 334 | Statement: 335 | - Sid: "Enable IAM User Permissions" 336 | Effect: "Allow" 337 | Principal: 338 | AWS: 339 | Fn::Join: 340 | - '' 341 | - ['arn:aws:iam::', {Ref: 'AWS::AccountId'}, ':root'] 342 | Action: "kms:*" 343 | Resource: "*" 344 | - Sid: "Allow administration of the key" 345 | Effect: "Allow" 346 | Principal: 347 | AWS: 348 | - !GetAtt RootEncryptionRole.Arn 349 | Action: 350 | - "kms:Create*" 351 | - "kms:Describe*" 352 | - "kms:Enable*" 353 | - "kms:List*" 354 | - "kms:Put*" 355 | - "kms:Update*" 356 | - "kms:Revoke*" 357 | - "kms:Disable*" 358 | - "kms:Get*" 359 | - "kms:Delete*" 360 | - "kms:TagResource" 361 | - "kms:UntagResource" 362 | - "kms:ScheduleKeyDeletion" 363 | - "kms:CancelKeyDeletion" 364 | Resource: "*" 365 | - Sid: "Allow use of the key" 366 | Effect: "Allow" 367 | Principal: 368 | AWS: 369 | - !GetAtt LambdaExecutionRole.Arn 370 | - !GetAtt SentryRole.Arn 371 | Action: 372 | - "kms:Encrypt" 373 | - "kms:Decrypt" 374 | - "kms:ReEncrypt*" 375 | - "kms:GenerateDataKey*" 376 | - "kms:DescribeKey" 377 | Resource: "*" 378 | SentryEncryptionKeyAlias: 379 | Type: AWS::KMS::Alias 380 | Properties: 381 | AliasName: 382 | Fn::Join: 383 | - '' 384 | - ['alias/', {Ref: 'AWS::StackName'}] 385 | TargetKeyId: 386 | Ref: SentryEncryptionKey 387 | EncryptionHelperFunction: 388 | Type: AWS::Lambda::Function 389 | Properties: 390 | Handler: index.lambda_handler 391 | Role: !GetAtt LambdaExecutionRole.Arn 392 | Code: 393 | ZipFile: !Sub | 394 | import base64 395 | import uuid 396 | import httplib 397 | import urlparse 398 | import json 399 | import boto3 400 | def send_response(request, response, status=None, reason=None): 401 | """ Send our response to the pre-signed URL supplied by CloudFormation 402 | If no ResponseURL is found in the request, there is no place to send a 403 | response. This may be the case if the supplied event was for testing. 404 | """ 405 | if status is not None: 406 | response['Status'] = status 407 | if reason is not None: 408 | response['Reason'] = reason 409 | if 'ResponseURL' in request and request['ResponseURL']: 410 | url = urlparse.urlparse(request['ResponseURL']) 411 | body = json.dumps(response) 412 | https = httplib.HTTPSConnection(url.hostname) 413 | https.request('PUT', url.path+'?'+url.query, body) 414 | return response 415 | def lambda_handler(event, context): 416 | response = { 417 | 'StackId': event['StackId'], 418 | 'RequestId': event['RequestId'], 419 | 'LogicalResourceId': event['LogicalResourceId'], 420 | 'Status': 'SUCCESS' 421 | } 422 | # PhysicalResourceId is meaningless here, but CloudFormation requires it 423 | if 'PhysicalResourceId' in event: 424 | response['PhysicalResourceId'] = event['PhysicalResourceId'] 425 | else: 426 | response['PhysicalResourceId'] = str(uuid.uuid4()) 427 | # There is nothing to do for a delete request 428 | if event['RequestType'] == 'Delete': 429 | return send_response(event, response) 430 | # Encrypt the value using AWS KMS and return the response 431 | try: 432 | for key in ['KeyId', 'PlainText']: 433 | if key not in event['ResourceProperties'] or not event['ResourceProperties'][key]: 434 | return send_response( 435 | event, response, status='FAILED', 436 | reason='The properties KeyId and PlainText must not be empty' 437 | ) 438 | client = boto3.client('kms') 439 | encrypted = client.encrypt( 440 | KeyId=event['ResourceProperties']['KeyId'], 441 | Plaintext=event['ResourceProperties']['PlainText'] 442 | ) 443 | response['Data'] = { 444 | 'CipherText': base64.b64encode(encrypted['CiphertextBlob']) 445 | } 446 | response['Reason'] = 'The value was successfully encrypted' 447 | except Exception as E: 448 | response['Status'] = 'FAILED' 449 | response['Reason'] = 'Encryption Failed - See CloudWatch logs for the Lamba function backing the custom resource for details' 450 | return send_response(event, response) 451 | Runtime: python2.7 452 | ###### Redis ###### 453 | RedisAccessSecurityGroup: 454 | Type: AWS::EC2::SecurityGroup 455 | Properties: 456 | VpcId: 457 | Ref: VPC 458 | GroupDescription: Allows access only to sentry redis cluster 459 | Tags: 460 | - Key: Name 461 | Value: 462 | Fn::Join: 463 | - '-' 464 | - [{Ref: 'AWS::StackName'}, 'RedisAccess'] 465 | RedisSecurityGroup: 466 | Type: AWS::EC2::SecurityGroup 467 | Properties: 468 | GroupDescription: Senty redis cluster source 469 | SecurityGroupIngress: 470 | - IpProtocol: tcp 471 | FromPort: '6379' 472 | ToPort: '6379' 473 | SourceSecurityGroupId: 474 | Ref: RedisAccessSecurityGroup 475 | VpcId: 476 | Ref: VPC 477 | Tags: 478 | - Key: Name 479 | Value: 480 | Fn::Join: 481 | - '-' 482 | - [{Ref: 'AWS::StackName'}, 'Redis'] 483 | RedisSubnetGroup: 484 | Type: AWS::ElastiCache::SubnetGroup 485 | Properties: 486 | Description: Sentry stack redis subnet group 487 | SubnetIds: 488 | - Ref: PrivateSubnet1 489 | RedisCacheCluster: 490 | Type: AWS::ElastiCache::CacheCluster 491 | Properties: 492 | CacheNodeType: 493 | Ref: RedisNodeType 494 | CacheSubnetGroupName: 495 | Ref: RedisSubnetGroup 496 | Engine: redis 497 | EngineVersion: 498 | Ref: RedisEngineVersion 499 | NumCacheNodes: 500 | Ref: RedisNumNodes 501 | VpcSecurityGroupIds: 502 | - Ref: RedisSecurityGroup 503 | ###### Postgres ###### 504 | PostgresAccessSecurityGroup: 505 | Type: AWS::EC2::SecurityGroup 506 | Properties: 507 | VpcId: 508 | Ref: VPC 509 | GroupDescription: Allows access only to sentry postgres instance 510 | Tags: 511 | - Key: Name 512 | Value: 513 | Fn::Join: 514 | - '-' 515 | - [{Ref: 'AWS::StackName'}, 'PostgresAccess'] 516 | PostgresSecurityGroup: 517 | Type: AWS::EC2::SecurityGroup 518 | Properties: 519 | SecurityGroupIngress: 520 | - ToPort: '5432' 521 | IpProtocol: tcp 522 | FromPort: '5432' 523 | SourceSecurityGroupId: 524 | Ref: PostgresAccessSecurityGroup 525 | VpcId: 526 | Ref: VPC 527 | GroupDescription: Senty postgres instance source 528 | Tags: 529 | - Key: Name 530 | Value: 531 | Fn::Join: 532 | - '-' 533 | - [{Ref: 'AWS::StackName'}, 'Postgres'] 534 | PostgresSubnetGroup: 535 | Type: AWS::RDS::DBSubnetGroup 536 | Properties: 537 | DBSubnetGroupDescription: Sentry stack postgres subnet group 538 | SubnetIds: 539 | - Ref: PrivateSubnet1 540 | PostgresInstance: 541 | Type: AWS::RDS::DBInstance 542 | Properties: 543 | AllocatedStorage: 544 | Ref: DBAllocatedStorage 545 | BackupRetentionPeriod: 546 | Ref: DBBackupRetentionPeriod 547 | DBInstanceClass: 548 | Ref: DBInstanceClass 549 | DBName: 550 | Ref: DBName 551 | Engine: postgres 552 | KmsKeyId: 553 | Ref: SentryEncryptionKey 554 | MasterUsername: 555 | Ref: DBMasterUsername 556 | MasterUserPassword: 557 | Ref: DBMasterUserPassword 558 | MultiAZ: 559 | Ref: DBMultiAZ 560 | Port: '5432' 561 | PubliclyAccessible: 'false' 562 | StorageEncrypted: 'true' 563 | StorageType: 564 | Ref: DBStorageType 565 | VPCSecurityGroups: 566 | - Ref: PostgresSecurityGroup 567 | DBSubnetGroupName: 568 | Ref: PostgresSubnetGroup 569 | ###### File Storage ###### 570 | SentryFilesS3Bucket: 571 | Type: AWS::S3::Bucket 572 | Properties: 573 | BucketName: 574 | Fn::Join: 575 | - '' 576 | - - Ref: AWS::AccountId 577 | - "-" 578 | - Ref: AWS::StackName 579 | - "-sentry-files" 580 | AccessControl: Private 581 | ###### App Server ###### 582 | LoadBalancerSecurityGroup: 583 | Type: AWS::EC2::SecurityGroup 584 | Properties: 585 | VpcId: 586 | Ref: VPC 587 | GroupDescription: An ELB group allowing access only to from the corresponding 588 | component 589 | Tags: 590 | - Key: Name 591 | Value: 592 | Fn::Join: 593 | - '-' 594 | - [{Ref: 'AWS::StackName'}, 'LoadBalancer'] 595 | SentryElasticLoadBalancer: 596 | Type: AWS::ElasticLoadBalancing::LoadBalancer 597 | Properties: 598 | Subnets: 599 | - Ref: PrivateSubnet1 600 | Scheme: internal 601 | Listeners: 602 | - InstancePort: '443' 603 | Protocol: HTTPS 604 | InstanceProtocol: HTTPS 605 | LoadBalancerPort: '443' 606 | SSLCertificateId: 607 | Ref: SSLCertARN 608 | PolicyNames: 609 | - SSLNegotiationPolicy 610 | Policies: 611 | - PolicyName : SSLNegotiationPolicy 612 | PolicyType: SSLNegotiationPolicyType 613 | Attributes: 614 | - Name: Protocol-TLSv1.2 615 | Value: 'true' 616 | - Name: Server-Defined-Cipher-Order 617 | Value: 'true' 618 | - Name: ECDHE-ECDSA-AES128-GCM-SHA256 619 | Value: 'true' 620 | - Name: ECDHE-RSA-AES128-GCM-SHA256 621 | Value: 'true' 622 | - Name: ECDHE-ECDSA-AES128-SHA256 623 | Value: 'true' 624 | - Name: ECDHE-RSA-AES128-SHA256 625 | Value: 'true' 626 | - Name: ECDHE-ECDSA-AES256-GCM-SHA384 627 | Value: 'true' 628 | - Name: ECDHE-RSA-AES256-GCM-SHA384 629 | Value: 'true' 630 | - Name: ECDHE-ECDSA-AES256-SHA384 631 | Value: 'true' 632 | - Name: ECDHE-RSA-AES256-SHA384 633 | Value: 'true' 634 | - Name: AES128-GCM-SHA256 635 | Value: 'true' 636 | - Name: AES128-SHA256 637 | Value: 'true' 638 | - Name: AES256-GCM-SHA384 639 | Value: 'true' 640 | - Name: AES256-SHA256 641 | Value: 'true' 642 | CrossZone: false 643 | SecurityGroups: 644 | - Ref: LoadBalancerSecurityGroup 645 | HealthCheck: 646 | HealthyThreshold: 3 647 | Interval: 10 648 | Timeout: 5 649 | UnhealthyThreshold: 10 650 | Target: HTTPS:443/_health/ 651 | SentryInstanceProfile: 652 | Type: AWS::IAM::InstanceProfile 653 | Properties: 654 | Path: "/" 655 | Roles: 656 | - Ref: SentryRole 657 | SentrySecurityGroup: 658 | Type: AWS::EC2::SecurityGroup 659 | Properties: 660 | SecurityGroupIngress: 661 | - ToPort: '443' 662 | IpProtocol: tcp 663 | FromPort: '443' 664 | SourceSecurityGroupId: 665 | Ref: LoadBalancerSecurityGroup 666 | VpcId: 667 | Ref: VPC 668 | GroupDescription: Sentry instance security group, gives access to from load balancer 669 | Tags: 670 | - Key: Name 671 | Value: 672 | Fn::Join: 673 | - '-' 674 | - [{Ref: 'AWS::StackName'}, 'Sentry'] 675 | EncryptedDeploymentHosts: 676 | Type: AWS::CloudFormation::CustomResource 677 | Version: "1.0" 678 | Properties: 679 | ServiceToken: !GetAtt EncryptionHelperFunction.Arn 680 | KeyId: 681 | Ref: SentryEncryptionKey 682 | PlainText: 683 | !Sub 684 | - | 685 | [aws] 686 | 127.0.0.1 687 | [aws:vars] 688 | user=ubuntu 689 | owner="${Owner}" 690 | sentry_admin_username="${SentryAdminUser}" 691 | sentry_admin_password="${SentryAdminPassword}" 692 | sentry_public_dns_name="${SentryPublicDnsName}" 693 | sentry_secret_key="${SentrySecretKey}" 694 | sentry_github_app_id="${SentryGithubAppId}" 695 | sentry_github_api_secret="${SentryGithubApiSecret}" 696 | sentry_url="${SentryUrl}" 697 | sentry_db_host="${DBHost}" 698 | sentry_db_port="5432" 699 | sentry_db_name="${DBName}" 700 | sentry_db_user="${DBMasterUsername}" 701 | sentry_db_password="${DBMasterUserPassword}" 702 | sentry_redis_host="${RedisHost}" 703 | sentry_redis_port="6379" 704 | sentry_mail_host="${SentryMailHost}" 705 | sentry_mail_port="${SentryMailPort}" 706 | sentry_mail_username="${SentryMailUsername}" 707 | sentry_mail_password="${SentryMailPassword}" 708 | sentry_mail_from="${SentryMailFrom}" 709 | sentry_files_bucket_name="${AWS::AccountId}-${AWS::StackName}-sentry-files" 710 | - SentryUrl: !GetAtt SentryElasticLoadBalancer.DNSName 711 | DBHost: !GetAtt PostgresInstance.Endpoint.Address 712 | RedisHost: !GetAtt RedisCacheCluster.RedisEndpoint.Address 713 | SentryLaunchConfiguration: 714 | Type: AWS::AutoScaling::LaunchConfiguration 715 | Properties: 716 | KeyName: 717 | Ref: KeyName 718 | ImageId: 719 | Ref: ImageId 720 | SecurityGroups: 721 | - Ref: SentrySecurityGroup 722 | - Ref: PostgresAccessSecurityGroup 723 | - Ref: RedisAccessSecurityGroup 724 | InstanceType: 725 | Ref: InstanceType 726 | IamInstanceProfile: 727 | Ref: SentryInstanceProfile 728 | UserData: 729 | Fn::Base64: 730 | !Sub 731 | - | 732 | #cloud-config 733 | runcmd: 734 | - apt-get update 735 | - apt-get install ansible awscli unzip -y 736 | - openssl req -new -nodes -x509 -subj "/C=GB/ST=London/L=London/O=Private/CN=${AWS::StackName}" -days 3650 -keyout /tmp/server.key -out /tmp/bundle.crt -extensions v3_ca 737 | - curl https://github.com/o2Labs/sentry-formation/archive/1.0.0.zip --output /tmp/repo.zip --location 738 | - unzip /tmp/repo.zip -d /tmp 739 | - echo "${DeploymentHosts}" > /tmp/hosts.base64 740 | - base64 -d /tmp/hosts.base64 > /tmp/hosts.encrypted 741 | - aws kms decrypt --region ${AWS::Region} --ciphertext-blob "fileb:///tmp/hosts.encrypted" --output text --query Plaintext > /tmp/hosts.decrypted 742 | - base64 -d /tmp/hosts.decrypted > /tmp/sentry-formation-1.0.0/hosts 743 | - ansible-playbook /tmp/sentry-formation-1.0.0/site.yml -i /tmp/sentry-formation-1.0.0/hosts 744 | - DeploymentHosts: !GetAtt EncryptedDeploymentHosts.CipherText 745 | SentryAutoScalingGroup: 746 | Type: AWS::AutoScaling::AutoScalingGroup 747 | UpdatePolicy: 748 | AutoScalingRollingUpdate: 749 | PauseTime: PT15M 750 | MaxBatchSize: 1 751 | MinInstancesInService: 1 752 | Properties: 753 | LoadBalancerNames: 754 | - Ref: SentryElasticLoadBalancer 755 | MinSize: 756 | Ref: ScalingMinNodes 757 | MaxSize: 758 | Ref: ScalingMaxNodes 759 | LaunchConfigurationName: 760 | Ref: SentryLaunchConfiguration 761 | Tags: 762 | - PropagateAtLaunch: true 763 | Key: Name 764 | Value: 765 | Ref: AWS::StackName 766 | VPCZoneIdentifier: 767 | - Ref: PrivateSubnet1 768 | SentryScalingPolicy: 769 | Type: AWS::AutoScaling::ScalingPolicy 770 | Properties: 771 | ScalingAdjustment: 1 772 | AutoScalingGroupName: 773 | Ref: SentryAutoScalingGroup 774 | AdjustmentType: ChangeInCapacity 775 | -------------------------------------------------------------------------------- /output/master-internal-1az.yaml: -------------------------------------------------------------------------------- 1 | # This file was generated with lono. Do not edit directly, the changes will be lost. 2 | # More info: https://github.com/tongueroo/lono 3 | Description: Sentry.io internal setup in 1 availability zones 4 | Parameters: 5 | #### Required #### 6 | Owner: 7 | Type: String 8 | Description: Name of the owner of the service (normally your company name) 9 | DBMasterUsername: 10 | Type: String 11 | MinLength: '1' 12 | MaxLength: '16' 13 | AllowedPattern: "[a-zA-Z][a-zA-Z0-9]*" 14 | Description: Username for database access 15 | DBMasterUserPassword: 16 | NoEcho: true 17 | Type: String 18 | Description: Password for database access - minimum 8 characters 19 | MinLength: '8' 20 | MaxLength: '41' 21 | AllowedPattern: "[a-zA-Z0-9]*" 22 | ConstraintDescription: must contain 8 alphanumeric characters. 23 | SSLCertARN: 24 | Description: The ARN of the ACM cert 25 | Type: String 26 | KeyName: 27 | Description: Name of existing EC2 keypair to enable SSH access to the created 28 | instances 29 | Type: AWS::EC2::KeyPair::KeyName 30 | SentryAdminUser: 31 | Type: String 32 | MinLength: '1' 33 | MaxLength: '30' 34 | Description: Username for root sentry access 35 | SentryAdminPassword: 36 | NoEcho: true 37 | Type: String 38 | Description: Password for root sentry access - minimum 20 characters 39 | MinLength: '20' 40 | SentryPublicDnsName: 41 | Type: String 42 | Description: Host name that users will type to get to sentry. 43 | SentrySecretKey: 44 | Type: String 45 | Description: Private key for encrypting user sessions. 46 | MinLength: '50' 47 | NoEcho: true 48 | SentryGithubAppId: 49 | Description: GitHub API App ID for SSO 50 | Type: String 51 | SentryGithubApiSecret: 52 | Description: GitHub API secret key for SSO 53 | Type: String 54 | NoEcho: true 55 | SentryMailUsername: 56 | Description: SMTP username for sentry to use to send email 57 | Type: String 58 | SentryMailPassword: 59 | Description: SMTP password for sentry to use to send email 60 | Type: String 61 | NoEcho: true 62 | SentryMailFrom: 63 | Description: Sending email address for sentry to use to send email 64 | Type: String 65 | #### Optional #### 66 | VpcCidr: 67 | AllowedPattern: '((\d{1,3})\.){3}\d{1,3}/\d{1,2}' 68 | Description: VPC Cidr block 69 | Default: 10.0.0.0/22 70 | Type: String 71 | VpcAvailabilityZone1: 72 | Description: The AvailabilityZone to use for subnet 1 73 | Type: AWS::EC2::AvailabilityZone::Name 74 | Default: eu-west-1a 75 | VpcPublicSubnetCIDR1: 76 | AllowedPattern: '((\d{1,3})\.){3}\d{1,3}/\d{1,2}' 77 | Default: 10.0.0.0/26 78 | Description: VPC CIDR Block for Public Subnet 1 79 | Type: String 80 | VpcPrivateSubnetCIDR1: 81 | AllowedPattern: '((\d{1,3})\.){3}\d{1,3}/\d{1,2}' 82 | Default: 10.0.2.0/26 83 | Description: VPC CIDR Block for Private Subnet 1 84 | Type: String 85 | ImageId: 86 | Description: The AMI to use for this Sentry - YUM compliant required 87 | Type: String 88 | Default: ami-6d48500b 89 | InstanceType: 90 | Description: The size of the EC2 instances 91 | Default: t2.medium 92 | Type: String 93 | DBName: 94 | Type: String 95 | Default: sentry 96 | MinLength: '1' 97 | MaxLength: '64' 98 | AllowedPattern: "[a-zA-Z][a-zA-Z0-9]*" 99 | ConstraintDescription: must begin with a letter and contain only alphanumeric 100 | characters. 101 | DBAllocatedStorage: 102 | Type: String 103 | Default: '5' 104 | DBBackupRetentionPeriod: 105 | Type: String 106 | Default: '7' 107 | DBInstanceClass: 108 | Type: String 109 | Default: db.t2.small 110 | DBMultiAZ: 111 | Type: String 112 | Default: false 113 | Description: Provides enhanced availablily for RDS 114 | DBStorageType: 115 | Type: String 116 | Default: gp2 117 | RedisEngineVersion: 118 | Type: String 119 | Description: Version of Redis engine to use. 120 | Default: '3.2.4' 121 | RedisNodeType: 122 | Type: String 123 | Default: cache.t2.small 124 | RedisNumNodes: 125 | Type: String 126 | Default: '1' 127 | SentryMailHost: 128 | Description: SMTP host for sentry to use to send email 129 | Type: String 130 | Default: email-smtp.eu-west-1.amazonaws.com 131 | SentryMailPort: 132 | Description: SMTP port for sentry to use to send email 133 | Type: String 134 | Default: '25' 135 | ScalingMinNodes: 136 | Type: Number 137 | Description: Minium size of the auto scaling group 138 | Default: 1 139 | ScalingMaxNodes: 140 | Type: Number 141 | Description: Maximum size of the auto scaling group 142 | Default: 2 143 | AWSTemplateFormatVersion: '2010-09-09' 144 | Resources: 145 | ###### Network ###### 146 | VPC: 147 | Type: 'AWS::EC2::VPC' 148 | Properties: 149 | CidrBlock: 150 | Ref: VpcCidr 151 | EnableDnsHostnames: true 152 | Tags: 153 | - Key: Name 154 | Value: 155 | Ref: AWS::StackName 156 | PublicSubnet1: 157 | Type: AWS::EC2::Subnet 158 | Properties: 159 | AvailabilityZone: {Ref: VpcAvailabilityZone1} 160 | CidrBlock: {Ref: VpcPublicSubnetCIDR1} 161 | MapPublicIpOnLaunch: true 162 | Tags: 163 | - Key: Name 164 | Value: 165 | Fn::Join: 166 | - '-' 167 | - [{Ref: 'AWS::StackName'}, 'public', {Ref: VpcAvailabilityZone1}] 168 | VpcId: {Ref: VPC} 169 | PrivateSubnet1: 170 | Type: AWS::EC2::Subnet 171 | Properties: 172 | AvailabilityZone: {Ref: VpcAvailabilityZone1} 173 | CidrBlock: {Ref: VpcPrivateSubnetCIDR1} 174 | MapPublicIpOnLaunch: false 175 | Tags: 176 | - Key: Name 177 | Value: 178 | Fn::Join: 179 | - '-' 180 | - [{Ref: 'AWS::StackName'}, 'private', {Ref: VpcAvailabilityZone1}] 181 | VpcId: {Ref: VPC} 182 | InternetGateway: 183 | Type: AWS::EC2::InternetGateway 184 | Properties: 185 | Tags: 186 | - Key: Name 187 | Value: 188 | Ref: AWS::StackName 189 | GatewayAttachment: 190 | Type: AWS::EC2::VPCGatewayAttachment 191 | Properties: 192 | InternetGatewayId: 193 | Ref: InternetGateway 194 | VpcId: 195 | Ref: VPC 196 | ###### Public Routing ###### 197 | PublicRouteTable: 198 | Type: AWS::EC2::RouteTable 199 | Properties: 200 | Tags: 201 | - Key: Name 202 | Value: 203 | Fn::Join: 204 | - '-' 205 | - [{Ref: 'AWS::StackName'}, 'public'] 206 | VpcId: 207 | Ref: VPC 208 | PublicRoute: 209 | Type: AWS::EC2::Route 210 | Properties: 211 | DestinationCidrBlock: 0.0.0.0/0 212 | GatewayId: 213 | Ref: InternetGateway 214 | RouteTableId: 215 | Ref: PublicRouteTable 216 | PublicSubnetAssoc1: 217 | Type: AWS::EC2::SubnetRouteTableAssociation 218 | Properties: 219 | RouteTableId: 220 | Ref: PublicRouteTable 221 | SubnetId: 222 | Ref: PublicSubnet1 223 | ###### Private Routing ###### 224 | PrivateRouteTable1: 225 | Type: AWS::EC2::RouteTable 226 | Properties: 227 | Tags: 228 | - Key: Name 229 | Value: 230 | Fn::Join: 231 | - '-' 232 | - [{Ref: 'AWS::StackName'}, 'private-1'] 233 | VpcId: 234 | Ref: VPC 235 | PrivateSubnetAssoc1: 236 | Type: AWS::EC2::SubnetRouteTableAssociation 237 | Properties: 238 | RouteTableId: 239 | Ref: PrivateRouteTable1 240 | SubnetId: 241 | Ref: PrivateSubnet1 242 | EIP1: 243 | Type: AWS::EC2::EIP 244 | Properties: 245 | Domain: vpc 246 | NAT1: 247 | DependsOn: GatewayAttachment 248 | Type: AWS::EC2::NatGateway 249 | Properties: 250 | AllocationId: 251 | Fn::GetAtt: 252 | - EIP1 253 | - AllocationId 254 | SubnetId: 255 | Ref: PublicSubnet1 256 | NATRoute1: 257 | Type: AWS::EC2::Route 258 | Properties: 259 | RouteTableId: 260 | Ref: PrivateRouteTable1 261 | DestinationCidrBlock: 0.0.0.0/0 262 | NatGatewayId: 263 | Ref: NAT1 264 | ###### Roles ###### 265 | RootEncryptionRole: 266 | Type: AWS::IAM::Role 267 | Properties: 268 | AssumeRolePolicyDocument: 269 | Version: "2012-10-17" 270 | Statement: 271 | - Effect: "Allow" 272 | Principal: 273 | AWS: "*" 274 | Action: 275 | - "sts:AssumeRole" 276 | Path: "/" 277 | LambdaExecutionRole: 278 | Type: AWS::IAM::Role 279 | Properties: 280 | AssumeRolePolicyDocument: 281 | Version: '2012-10-17' 282 | Statement: 283 | - Effect: Allow 284 | Principal: 285 | Service: 286 | - lambda.amazonaws.com 287 | Action: 288 | - sts:AssumeRole 289 | Path: "/" 290 | Policies: 291 | - PolicyName: root 292 | PolicyDocument: 293 | Version: '2012-10-17' 294 | Statement: 295 | - Effect: Allow 296 | Action: 297 | - logs:* 298 | Resource: arn:aws:logs:*:*:* 299 | SentryRole: 300 | Type: AWS::IAM::Role 301 | Properties: 302 | AssumeRolePolicyDocument: 303 | Version: "2012-10-17" 304 | Statement: 305 | - Effect: Allow 306 | Principal: 307 | Service: 308 | - ec2.amazonaws.com 309 | Action: 310 | - sts:AssumeRole 311 | Path: "/" 312 | Policies: 313 | - PolicyName: root 314 | PolicyDocument: 315 | Version: "2012-10-17" 316 | Statement: 317 | - Effect: Allow 318 | Action: 319 | - s3:PutObject 320 | - s3:GetObject 321 | - s3:DeleteObject 322 | Resource: 323 | - Fn::Join: 324 | - '' 325 | - - !GetAtt SentryFilesS3Bucket.Arn 326 | - "/*" 327 | ###### Encryption ###### 328 | SentryEncryptionKey: 329 | Type: "AWS::KMS::Key" 330 | Properties: 331 | Description: Sentry environment root encryption key 332 | KeyPolicy: 333 | Version: "2012-10-17" 334 | Statement: 335 | - Sid: "Enable IAM User Permissions" 336 | Effect: "Allow" 337 | Principal: 338 | AWS: 339 | Fn::Join: 340 | - '' 341 | - ['arn:aws:iam::', {Ref: 'AWS::AccountId'}, ':root'] 342 | Action: "kms:*" 343 | Resource: "*" 344 | - Sid: "Allow administration of the key" 345 | Effect: "Allow" 346 | Principal: 347 | AWS: 348 | - !GetAtt RootEncryptionRole.Arn 349 | Action: 350 | - "kms:Create*" 351 | - "kms:Describe*" 352 | - "kms:Enable*" 353 | - "kms:List*" 354 | - "kms:Put*" 355 | - "kms:Update*" 356 | - "kms:Revoke*" 357 | - "kms:Disable*" 358 | - "kms:Get*" 359 | - "kms:Delete*" 360 | - "kms:TagResource" 361 | - "kms:UntagResource" 362 | - "kms:ScheduleKeyDeletion" 363 | - "kms:CancelKeyDeletion" 364 | Resource: "*" 365 | - Sid: "Allow use of the key" 366 | Effect: "Allow" 367 | Principal: 368 | AWS: 369 | - !GetAtt LambdaExecutionRole.Arn 370 | - !GetAtt SentryRole.Arn 371 | Action: 372 | - "kms:Encrypt" 373 | - "kms:Decrypt" 374 | - "kms:ReEncrypt*" 375 | - "kms:GenerateDataKey*" 376 | - "kms:DescribeKey" 377 | Resource: "*" 378 | SentryEncryptionKeyAlias: 379 | Type: AWS::KMS::Alias 380 | Properties: 381 | AliasName: 382 | Fn::Join: 383 | - '' 384 | - ['alias/', {Ref: 'AWS::StackName'}] 385 | TargetKeyId: 386 | Ref: SentryEncryptionKey 387 | EncryptionHelperFunction: 388 | Type: AWS::Lambda::Function 389 | Properties: 390 | Handler: index.lambda_handler 391 | Role: !GetAtt LambdaExecutionRole.Arn 392 | Code: 393 | ZipFile: !Sub | 394 | import base64 395 | import uuid 396 | import httplib 397 | import urlparse 398 | import json 399 | import boto3 400 | def send_response(request, response, status=None, reason=None): 401 | """ Send our response to the pre-signed URL supplied by CloudFormation 402 | If no ResponseURL is found in the request, there is no place to send a 403 | response. This may be the case if the supplied event was for testing. 404 | """ 405 | if status is not None: 406 | response['Status'] = status 407 | if reason is not None: 408 | response['Reason'] = reason 409 | if 'ResponseURL' in request and request['ResponseURL']: 410 | url = urlparse.urlparse(request['ResponseURL']) 411 | body = json.dumps(response) 412 | https = httplib.HTTPSConnection(url.hostname) 413 | https.request('PUT', url.path+'?'+url.query, body) 414 | return response 415 | def lambda_handler(event, context): 416 | response = { 417 | 'StackId': event['StackId'], 418 | 'RequestId': event['RequestId'], 419 | 'LogicalResourceId': event['LogicalResourceId'], 420 | 'Status': 'SUCCESS' 421 | } 422 | # PhysicalResourceId is meaningless here, but CloudFormation requires it 423 | if 'PhysicalResourceId' in event: 424 | response['PhysicalResourceId'] = event['PhysicalResourceId'] 425 | else: 426 | response['PhysicalResourceId'] = str(uuid.uuid4()) 427 | # There is nothing to do for a delete request 428 | if event['RequestType'] == 'Delete': 429 | return send_response(event, response) 430 | # Encrypt the value using AWS KMS and return the response 431 | try: 432 | for key in ['KeyId', 'PlainText']: 433 | if key not in event['ResourceProperties'] or not event['ResourceProperties'][key]: 434 | return send_response( 435 | event, response, status='FAILED', 436 | reason='The properties KeyId and PlainText must not be empty' 437 | ) 438 | client = boto3.client('kms') 439 | encrypted = client.encrypt( 440 | KeyId=event['ResourceProperties']['KeyId'], 441 | Plaintext=event['ResourceProperties']['PlainText'] 442 | ) 443 | response['Data'] = { 444 | 'CipherText': base64.b64encode(encrypted['CiphertextBlob']) 445 | } 446 | response['Reason'] = 'The value was successfully encrypted' 447 | except Exception as E: 448 | response['Status'] = 'FAILED' 449 | response['Reason'] = 'Encryption Failed - See CloudWatch logs for the Lamba function backing the custom resource for details' 450 | return send_response(event, response) 451 | Runtime: python2.7 452 | ###### Redis ###### 453 | RedisAccessSecurityGroup: 454 | Type: AWS::EC2::SecurityGroup 455 | Properties: 456 | VpcId: 457 | Ref: VPC 458 | GroupDescription: Allows access only to sentry redis cluster 459 | Tags: 460 | - Key: Name 461 | Value: 462 | Fn::Join: 463 | - '-' 464 | - [{Ref: 'AWS::StackName'}, 'RedisAccess'] 465 | RedisSecurityGroup: 466 | Type: AWS::EC2::SecurityGroup 467 | Properties: 468 | GroupDescription: Senty redis cluster source 469 | SecurityGroupIngress: 470 | - IpProtocol: tcp 471 | FromPort: '6379' 472 | ToPort: '6379' 473 | SourceSecurityGroupId: 474 | Ref: RedisAccessSecurityGroup 475 | VpcId: 476 | Ref: VPC 477 | Tags: 478 | - Key: Name 479 | Value: 480 | Fn::Join: 481 | - '-' 482 | - [{Ref: 'AWS::StackName'}, 'Redis'] 483 | RedisSubnetGroup: 484 | Type: AWS::ElastiCache::SubnetGroup 485 | Properties: 486 | Description: Sentry stack redis subnet group 487 | SubnetIds: 488 | - Ref: PrivateSubnet1 489 | RedisCacheCluster: 490 | Type: AWS::ElastiCache::CacheCluster 491 | Properties: 492 | CacheNodeType: 493 | Ref: RedisNodeType 494 | CacheSubnetGroupName: 495 | Ref: RedisSubnetGroup 496 | Engine: redis 497 | EngineVersion: 498 | Ref: RedisEngineVersion 499 | NumCacheNodes: 500 | Ref: RedisNumNodes 501 | VpcSecurityGroupIds: 502 | - Ref: RedisSecurityGroup 503 | ###### Postgres ###### 504 | PostgresAccessSecurityGroup: 505 | Type: AWS::EC2::SecurityGroup 506 | Properties: 507 | VpcId: 508 | Ref: VPC 509 | GroupDescription: Allows access only to sentry postgres instance 510 | Tags: 511 | - Key: Name 512 | Value: 513 | Fn::Join: 514 | - '-' 515 | - [{Ref: 'AWS::StackName'}, 'PostgresAccess'] 516 | PostgresSecurityGroup: 517 | Type: AWS::EC2::SecurityGroup 518 | Properties: 519 | SecurityGroupIngress: 520 | - ToPort: '5432' 521 | IpProtocol: tcp 522 | FromPort: '5432' 523 | SourceSecurityGroupId: 524 | Ref: PostgresAccessSecurityGroup 525 | VpcId: 526 | Ref: VPC 527 | GroupDescription: Senty postgres instance source 528 | Tags: 529 | - Key: Name 530 | Value: 531 | Fn::Join: 532 | - '-' 533 | - [{Ref: 'AWS::StackName'}, 'Postgres'] 534 | PostgresSubnetGroup: 535 | Type: AWS::RDS::DBSubnetGroup 536 | Properties: 537 | DBSubnetGroupDescription: Sentry stack postgres subnet group 538 | SubnetIds: 539 | - Ref: PrivateSubnet1 540 | PostgresInstance: 541 | Type: AWS::RDS::DBInstance 542 | Properties: 543 | AllocatedStorage: 544 | Ref: DBAllocatedStorage 545 | BackupRetentionPeriod: 546 | Ref: DBBackupRetentionPeriod 547 | DBInstanceClass: 548 | Ref: DBInstanceClass 549 | DBName: 550 | Ref: DBName 551 | Engine: postgres 552 | KmsKeyId: 553 | Ref: SentryEncryptionKey 554 | MasterUsername: 555 | Ref: DBMasterUsername 556 | MasterUserPassword: 557 | Ref: DBMasterUserPassword 558 | MultiAZ: 559 | Ref: DBMultiAZ 560 | Port: '5432' 561 | PubliclyAccessible: 'false' 562 | StorageEncrypted: 'true' 563 | StorageType: 564 | Ref: DBStorageType 565 | VPCSecurityGroups: 566 | - Ref: PostgresSecurityGroup 567 | DBSubnetGroupName: 568 | Ref: PostgresSubnetGroup 569 | ###### File Storage ###### 570 | SentryFilesS3Bucket: 571 | Type: AWS::S3::Bucket 572 | Properties: 573 | BucketName: 574 | Fn::Join: 575 | - '' 576 | - - Ref: AWS::AccountId 577 | - "-" 578 | - Ref: AWS::StackName 579 | - "-sentry-files" 580 | AccessControl: Private 581 | ###### App Server ###### 582 | LoadBalancerSecurityGroup: 583 | Type: AWS::EC2::SecurityGroup 584 | Properties: 585 | VpcId: 586 | Ref: VPC 587 | GroupDescription: An ELB group allowing access only to from the corresponding 588 | component 589 | Tags: 590 | - Key: Name 591 | Value: 592 | Fn::Join: 593 | - '-' 594 | - [{Ref: 'AWS::StackName'}, 'LoadBalancer'] 595 | SentryElasticLoadBalancer: 596 | Type: AWS::ElasticLoadBalancing::LoadBalancer 597 | Properties: 598 | Subnets: 599 | - Ref: PrivateSubnet1 600 | Scheme: internal 601 | Listeners: 602 | - InstancePort: '443' 603 | Protocol: HTTPS 604 | InstanceProtocol: HTTPS 605 | LoadBalancerPort: '443' 606 | SSLCertificateId: 607 | Ref: SSLCertARN 608 | PolicyNames: 609 | - SSLNegotiationPolicy 610 | Policies: 611 | - PolicyName : SSLNegotiationPolicy 612 | PolicyType: SSLNegotiationPolicyType 613 | Attributes: 614 | - Name: Protocol-TLSv1.2 615 | Value: 'true' 616 | - Name: Server-Defined-Cipher-Order 617 | Value: 'true' 618 | - Name: ECDHE-ECDSA-AES128-GCM-SHA256 619 | Value: 'true' 620 | - Name: ECDHE-RSA-AES128-GCM-SHA256 621 | Value: 'true' 622 | - Name: ECDHE-ECDSA-AES128-SHA256 623 | Value: 'true' 624 | - Name: ECDHE-RSA-AES128-SHA256 625 | Value: 'true' 626 | - Name: ECDHE-ECDSA-AES256-GCM-SHA384 627 | Value: 'true' 628 | - Name: ECDHE-RSA-AES256-GCM-SHA384 629 | Value: 'true' 630 | - Name: ECDHE-ECDSA-AES256-SHA384 631 | Value: 'true' 632 | - Name: ECDHE-RSA-AES256-SHA384 633 | Value: 'true' 634 | - Name: AES128-GCM-SHA256 635 | Value: 'true' 636 | - Name: AES128-SHA256 637 | Value: 'true' 638 | - Name: AES256-GCM-SHA384 639 | Value: 'true' 640 | - Name: AES256-SHA256 641 | Value: 'true' 642 | CrossZone: false 643 | SecurityGroups: 644 | - Ref: LoadBalancerSecurityGroup 645 | HealthCheck: 646 | HealthyThreshold: 3 647 | Interval: 10 648 | Timeout: 5 649 | UnhealthyThreshold: 10 650 | Target: HTTPS:443/_health/ 651 | SentryInstanceProfile: 652 | Type: AWS::IAM::InstanceProfile 653 | Properties: 654 | Path: "/" 655 | Roles: 656 | - Ref: SentryRole 657 | SentrySecurityGroup: 658 | Type: AWS::EC2::SecurityGroup 659 | Properties: 660 | SecurityGroupIngress: 661 | - ToPort: '443' 662 | IpProtocol: tcp 663 | FromPort: '443' 664 | SourceSecurityGroupId: 665 | Ref: LoadBalancerSecurityGroup 666 | VpcId: 667 | Ref: VPC 668 | GroupDescription: Sentry instance security group, gives access to from load balancer 669 | Tags: 670 | - Key: Name 671 | Value: 672 | Fn::Join: 673 | - '-' 674 | - [{Ref: 'AWS::StackName'}, 'Sentry'] 675 | EncryptedDeploymentHosts: 676 | Type: AWS::CloudFormation::CustomResource 677 | Version: "1.0" 678 | Properties: 679 | ServiceToken: !GetAtt EncryptionHelperFunction.Arn 680 | KeyId: 681 | Ref: SentryEncryptionKey 682 | PlainText: 683 | !Sub 684 | - | 685 | [aws] 686 | 127.0.0.1 687 | [aws:vars] 688 | user=ubuntu 689 | owner="${Owner}" 690 | sentry_admin_username="${SentryAdminUser}" 691 | sentry_admin_password="${SentryAdminPassword}" 692 | sentry_public_dns_name="${SentryPublicDnsName}" 693 | sentry_secret_key="${SentrySecretKey}" 694 | sentry_github_app_id="${SentryGithubAppId}" 695 | sentry_github_api_secret="${SentryGithubApiSecret}" 696 | sentry_url="${SentryUrl}" 697 | sentry_db_host="${DBHost}" 698 | sentry_db_port="5432" 699 | sentry_db_name="${DBName}" 700 | sentry_db_user="${DBMasterUsername}" 701 | sentry_db_password="${DBMasterUserPassword}" 702 | sentry_redis_host="${RedisHost}" 703 | sentry_redis_port="6379" 704 | sentry_mail_host="${SentryMailHost}" 705 | sentry_mail_port="${SentryMailPort}" 706 | sentry_mail_username="${SentryMailUsername}" 707 | sentry_mail_password="${SentryMailPassword}" 708 | sentry_mail_from="${SentryMailFrom}" 709 | sentry_files_bucket_name="${AWS::AccountId}-${AWS::StackName}-sentry-files" 710 | - SentryUrl: !GetAtt SentryElasticLoadBalancer.DNSName 711 | DBHost: !GetAtt PostgresInstance.Endpoint.Address 712 | RedisHost: !GetAtt RedisCacheCluster.RedisEndpoint.Address 713 | SentryLaunchConfiguration: 714 | Type: AWS::AutoScaling::LaunchConfiguration 715 | Properties: 716 | KeyName: 717 | Ref: KeyName 718 | ImageId: 719 | Ref: ImageId 720 | SecurityGroups: 721 | - Ref: SentrySecurityGroup 722 | - Ref: PostgresAccessSecurityGroup 723 | - Ref: RedisAccessSecurityGroup 724 | InstanceType: 725 | Ref: InstanceType 726 | IamInstanceProfile: 727 | Ref: SentryInstanceProfile 728 | UserData: 729 | Fn::Base64: 730 | !Sub 731 | - | 732 | #cloud-config 733 | runcmd: 734 | - apt-get update 735 | - apt-get install ansible awscli unzip -y 736 | - openssl req -new -nodes -x509 -subj "/C=GB/ST=London/L=London/O=Private/CN=${AWS::StackName}" -days 3650 -keyout /tmp/server.key -out /tmp/bundle.crt -extensions v3_ca 737 | - curl https://github.com/o2Labs/sentry-formation/archive/master.zip --output /tmp/repo.zip --location 738 | - unzip /tmp/repo.zip -d /tmp 739 | - echo "${DeploymentHosts}" > /tmp/hosts.base64 740 | - base64 -d /tmp/hosts.base64 > /tmp/hosts.encrypted 741 | - aws kms decrypt --region ${AWS::Region} --ciphertext-blob "fileb:///tmp/hosts.encrypted" --output text --query Plaintext > /tmp/hosts.decrypted 742 | - base64 -d /tmp/hosts.decrypted > /tmp/sentry-formation-master/hosts 743 | - ansible-playbook /tmp/sentry-formation-master/site.yml -i /tmp/sentry-formation-master/hosts 744 | - DeploymentHosts: !GetAtt EncryptedDeploymentHosts.CipherText 745 | SentryAutoScalingGroup: 746 | Type: AWS::AutoScaling::AutoScalingGroup 747 | UpdatePolicy: 748 | AutoScalingRollingUpdate: 749 | PauseTime: PT15M 750 | MaxBatchSize: 1 751 | MinInstancesInService: 1 752 | Properties: 753 | LoadBalancerNames: 754 | - Ref: SentryElasticLoadBalancer 755 | MinSize: 756 | Ref: ScalingMinNodes 757 | MaxSize: 758 | Ref: ScalingMaxNodes 759 | LaunchConfigurationName: 760 | Ref: SentryLaunchConfiguration 761 | Tags: 762 | - PropagateAtLaunch: true 763 | Key: Name 764 | Value: 765 | Ref: AWS::StackName 766 | VPCZoneIdentifier: 767 | - Ref: PrivateSubnet1 768 | SentryScalingPolicy: 769 | Type: AWS::AutoScaling::ScalingPolicy 770 | Properties: 771 | ScalingAdjustment: 1 772 | AutoScalingGroupName: 773 | Ref: SentryAutoScalingGroup 774 | AdjustmentType: ChangeInCapacity 775 | -------------------------------------------------------------------------------- /output/1.0.0-internet-facing-1az.yaml: -------------------------------------------------------------------------------- 1 | # This file was generated with lono. Do not edit directly, the changes will be lost. 2 | # More info: https://github.com/tongueroo/lono 3 | Description: Sentry.io internet-facing setup in 1 availability zones 4 | Parameters: 5 | #### Required #### 6 | Owner: 7 | Type: String 8 | Description: Name of the owner of the service (normally your company name) 9 | DBMasterUsername: 10 | Type: String 11 | MinLength: '1' 12 | MaxLength: '16' 13 | AllowedPattern: "[a-zA-Z][a-zA-Z0-9]*" 14 | Description: Username for database access 15 | DBMasterUserPassword: 16 | NoEcho: true 17 | Type: String 18 | Description: Password for database access - minimum 8 characters 19 | MinLength: '8' 20 | MaxLength: '41' 21 | AllowedPattern: "[a-zA-Z0-9]*" 22 | ConstraintDescription: must contain 8 alphanumeric characters. 23 | SSLCertARN: 24 | Description: The ARN of the ACM cert 25 | Type: String 26 | KeyName: 27 | Description: Name of existing EC2 keypair to enable SSH access to the created 28 | instances 29 | Type: AWS::EC2::KeyPair::KeyName 30 | SentryAdminUser: 31 | Type: String 32 | MinLength: '1' 33 | MaxLength: '30' 34 | Description: Username for root sentry access 35 | SentryAdminPassword: 36 | NoEcho: true 37 | Type: String 38 | Description: Password for root sentry access - minimum 20 characters 39 | MinLength: '20' 40 | SentryPublicDnsName: 41 | Type: String 42 | Description: Host name that users will type to get to sentry. 43 | SentrySecretKey: 44 | Type: String 45 | Description: Private key for encrypting user sessions. 46 | MinLength: '50' 47 | NoEcho: true 48 | SentryGithubAppId: 49 | Description: GitHub API App ID for SSO 50 | Type: String 51 | SentryGithubApiSecret: 52 | Description: GitHub API secret key for SSO 53 | Type: String 54 | NoEcho: true 55 | SentryMailUsername: 56 | Description: SMTP username for sentry to use to send email 57 | Type: String 58 | SentryMailPassword: 59 | Description: SMTP password for sentry to use to send email 60 | Type: String 61 | NoEcho: true 62 | SentryMailFrom: 63 | Description: Sending email address for sentry to use to send email 64 | Type: String 65 | #### Optional #### 66 | VpcCidr: 67 | AllowedPattern: '((\d{1,3})\.){3}\d{1,3}/\d{1,2}' 68 | Description: VPC Cidr block 69 | Default: 10.0.0.0/22 70 | Type: String 71 | VpcAvailabilityZone1: 72 | Description: The AvailabilityZone to use for subnet 1 73 | Type: AWS::EC2::AvailabilityZone::Name 74 | Default: eu-west-1a 75 | VpcPublicSubnetCIDR1: 76 | AllowedPattern: '((\d{1,3})\.){3}\d{1,3}/\d{1,2}' 77 | Default: 10.0.0.0/26 78 | Description: VPC CIDR Block for Public Subnet 1 79 | Type: String 80 | VpcPrivateSubnetCIDR1: 81 | AllowedPattern: '((\d{1,3})\.){3}\d{1,3}/\d{1,2}' 82 | Default: 10.0.2.0/26 83 | Description: VPC CIDR Block for Private Subnet 1 84 | Type: String 85 | ImageId: 86 | Description: The AMI to use for this Sentry - YUM compliant required 87 | Type: String 88 | Default: ami-6d48500b 89 | InstanceType: 90 | Description: The size of the EC2 instances 91 | Default: t2.medium 92 | Type: String 93 | DBName: 94 | Type: String 95 | Default: sentry 96 | MinLength: '1' 97 | MaxLength: '64' 98 | AllowedPattern: "[a-zA-Z][a-zA-Z0-9]*" 99 | ConstraintDescription: must begin with a letter and contain only alphanumeric 100 | characters. 101 | DBAllocatedStorage: 102 | Type: String 103 | Default: '5' 104 | DBBackupRetentionPeriod: 105 | Type: String 106 | Default: '7' 107 | DBInstanceClass: 108 | Type: String 109 | Default: db.t2.small 110 | DBMultiAZ: 111 | Type: String 112 | Default: false 113 | Description: Provides enhanced availablily for RDS 114 | DBStorageType: 115 | Type: String 116 | Default: gp2 117 | RedisEngineVersion: 118 | Type: String 119 | Description: Version of Redis engine to use. 120 | Default: '3.2.4' 121 | RedisNodeType: 122 | Type: String 123 | Default: cache.t2.small 124 | RedisNumNodes: 125 | Type: String 126 | Default: '1' 127 | SentryMailHost: 128 | Description: SMTP host for sentry to use to send email 129 | Type: String 130 | Default: email-smtp.eu-west-1.amazonaws.com 131 | SentryMailPort: 132 | Description: SMTP port for sentry to use to send email 133 | Type: String 134 | Default: '25' 135 | ScalingMinNodes: 136 | Type: Number 137 | Description: Minium size of the auto scaling group 138 | Default: 1 139 | ScalingMaxNodes: 140 | Type: Number 141 | Description: Maximum size of the auto scaling group 142 | Default: 2 143 | AWSTemplateFormatVersion: '2010-09-09' 144 | Resources: 145 | ###### Network ###### 146 | VPC: 147 | Type: 'AWS::EC2::VPC' 148 | Properties: 149 | CidrBlock: 150 | Ref: VpcCidr 151 | EnableDnsHostnames: true 152 | Tags: 153 | - Key: Name 154 | Value: 155 | Ref: AWS::StackName 156 | PublicSubnet1: 157 | Type: AWS::EC2::Subnet 158 | Properties: 159 | AvailabilityZone: {Ref: VpcAvailabilityZone1} 160 | CidrBlock: {Ref: VpcPublicSubnetCIDR1} 161 | MapPublicIpOnLaunch: true 162 | Tags: 163 | - Key: Name 164 | Value: 165 | Fn::Join: 166 | - '-' 167 | - [{Ref: 'AWS::StackName'}, 'public', {Ref: VpcAvailabilityZone1}] 168 | VpcId: {Ref: VPC} 169 | PrivateSubnet1: 170 | Type: AWS::EC2::Subnet 171 | Properties: 172 | AvailabilityZone: {Ref: VpcAvailabilityZone1} 173 | CidrBlock: {Ref: VpcPrivateSubnetCIDR1} 174 | MapPublicIpOnLaunch: false 175 | Tags: 176 | - Key: Name 177 | Value: 178 | Fn::Join: 179 | - '-' 180 | - [{Ref: 'AWS::StackName'}, 'private', {Ref: VpcAvailabilityZone1}] 181 | VpcId: {Ref: VPC} 182 | InternetGateway: 183 | Type: AWS::EC2::InternetGateway 184 | Properties: 185 | Tags: 186 | - Key: Name 187 | Value: 188 | Ref: AWS::StackName 189 | GatewayAttachment: 190 | Type: AWS::EC2::VPCGatewayAttachment 191 | Properties: 192 | InternetGatewayId: 193 | Ref: InternetGateway 194 | VpcId: 195 | Ref: VPC 196 | ###### Public Routing ###### 197 | PublicRouteTable: 198 | Type: AWS::EC2::RouteTable 199 | Properties: 200 | Tags: 201 | - Key: Name 202 | Value: 203 | Fn::Join: 204 | - '-' 205 | - [{Ref: 'AWS::StackName'}, 'public'] 206 | VpcId: 207 | Ref: VPC 208 | PublicRoute: 209 | Type: AWS::EC2::Route 210 | Properties: 211 | DestinationCidrBlock: 0.0.0.0/0 212 | GatewayId: 213 | Ref: InternetGateway 214 | RouteTableId: 215 | Ref: PublicRouteTable 216 | PublicSubnetAssoc1: 217 | Type: AWS::EC2::SubnetRouteTableAssociation 218 | Properties: 219 | RouteTableId: 220 | Ref: PublicRouteTable 221 | SubnetId: 222 | Ref: PublicSubnet1 223 | ###### Private Routing ###### 224 | PrivateRouteTable1: 225 | Type: AWS::EC2::RouteTable 226 | Properties: 227 | Tags: 228 | - Key: Name 229 | Value: 230 | Fn::Join: 231 | - '-' 232 | - [{Ref: 'AWS::StackName'}, 'private-1'] 233 | VpcId: 234 | Ref: VPC 235 | PrivateSubnetAssoc1: 236 | Type: AWS::EC2::SubnetRouteTableAssociation 237 | Properties: 238 | RouteTableId: 239 | Ref: PrivateRouteTable1 240 | SubnetId: 241 | Ref: PrivateSubnet1 242 | EIP1: 243 | Type: AWS::EC2::EIP 244 | Properties: 245 | Domain: vpc 246 | NAT1: 247 | DependsOn: GatewayAttachment 248 | Type: AWS::EC2::NatGateway 249 | Properties: 250 | AllocationId: 251 | Fn::GetAtt: 252 | - EIP1 253 | - AllocationId 254 | SubnetId: 255 | Ref: PublicSubnet1 256 | NATRoute1: 257 | Type: AWS::EC2::Route 258 | Properties: 259 | RouteTableId: 260 | Ref: PrivateRouteTable1 261 | DestinationCidrBlock: 0.0.0.0/0 262 | NatGatewayId: 263 | Ref: NAT1 264 | ###### Roles ###### 265 | RootEncryptionRole: 266 | Type: AWS::IAM::Role 267 | Properties: 268 | AssumeRolePolicyDocument: 269 | Version: "2012-10-17" 270 | Statement: 271 | - Effect: "Allow" 272 | Principal: 273 | AWS: "*" 274 | Action: 275 | - "sts:AssumeRole" 276 | Path: "/" 277 | LambdaExecutionRole: 278 | Type: AWS::IAM::Role 279 | Properties: 280 | AssumeRolePolicyDocument: 281 | Version: '2012-10-17' 282 | Statement: 283 | - Effect: Allow 284 | Principal: 285 | Service: 286 | - lambda.amazonaws.com 287 | Action: 288 | - sts:AssumeRole 289 | Path: "/" 290 | Policies: 291 | - PolicyName: root 292 | PolicyDocument: 293 | Version: '2012-10-17' 294 | Statement: 295 | - Effect: Allow 296 | Action: 297 | - logs:* 298 | Resource: arn:aws:logs:*:*:* 299 | SentryRole: 300 | Type: AWS::IAM::Role 301 | Properties: 302 | AssumeRolePolicyDocument: 303 | Version: "2012-10-17" 304 | Statement: 305 | - Effect: Allow 306 | Principal: 307 | Service: 308 | - ec2.amazonaws.com 309 | Action: 310 | - sts:AssumeRole 311 | Path: "/" 312 | Policies: 313 | - PolicyName: root 314 | PolicyDocument: 315 | Version: "2012-10-17" 316 | Statement: 317 | - Effect: Allow 318 | Action: 319 | - s3:PutObject 320 | - s3:GetObject 321 | - s3:DeleteObject 322 | Resource: 323 | - Fn::Join: 324 | - '' 325 | - - !GetAtt SentryFilesS3Bucket.Arn 326 | - "/*" 327 | ###### Encryption ###### 328 | SentryEncryptionKey: 329 | Type: "AWS::KMS::Key" 330 | Properties: 331 | Description: Sentry environment root encryption key 332 | KeyPolicy: 333 | Version: "2012-10-17" 334 | Statement: 335 | - Sid: "Enable IAM User Permissions" 336 | Effect: "Allow" 337 | Principal: 338 | AWS: 339 | Fn::Join: 340 | - '' 341 | - ['arn:aws:iam::', {Ref: 'AWS::AccountId'}, ':root'] 342 | Action: "kms:*" 343 | Resource: "*" 344 | - Sid: "Allow administration of the key" 345 | Effect: "Allow" 346 | Principal: 347 | AWS: 348 | - !GetAtt RootEncryptionRole.Arn 349 | Action: 350 | - "kms:Create*" 351 | - "kms:Describe*" 352 | - "kms:Enable*" 353 | - "kms:List*" 354 | - "kms:Put*" 355 | - "kms:Update*" 356 | - "kms:Revoke*" 357 | - "kms:Disable*" 358 | - "kms:Get*" 359 | - "kms:Delete*" 360 | - "kms:TagResource" 361 | - "kms:UntagResource" 362 | - "kms:ScheduleKeyDeletion" 363 | - "kms:CancelKeyDeletion" 364 | Resource: "*" 365 | - Sid: "Allow use of the key" 366 | Effect: "Allow" 367 | Principal: 368 | AWS: 369 | - !GetAtt LambdaExecutionRole.Arn 370 | - !GetAtt SentryRole.Arn 371 | Action: 372 | - "kms:Encrypt" 373 | - "kms:Decrypt" 374 | - "kms:ReEncrypt*" 375 | - "kms:GenerateDataKey*" 376 | - "kms:DescribeKey" 377 | Resource: "*" 378 | SentryEncryptionKeyAlias: 379 | Type: AWS::KMS::Alias 380 | Properties: 381 | AliasName: 382 | Fn::Join: 383 | - '' 384 | - ['alias/', {Ref: 'AWS::StackName'}] 385 | TargetKeyId: 386 | Ref: SentryEncryptionKey 387 | EncryptionHelperFunction: 388 | Type: AWS::Lambda::Function 389 | Properties: 390 | Handler: index.lambda_handler 391 | Role: !GetAtt LambdaExecutionRole.Arn 392 | Code: 393 | ZipFile: !Sub | 394 | import base64 395 | import uuid 396 | import httplib 397 | import urlparse 398 | import json 399 | import boto3 400 | def send_response(request, response, status=None, reason=None): 401 | """ Send our response to the pre-signed URL supplied by CloudFormation 402 | If no ResponseURL is found in the request, there is no place to send a 403 | response. This may be the case if the supplied event was for testing. 404 | """ 405 | if status is not None: 406 | response['Status'] = status 407 | if reason is not None: 408 | response['Reason'] = reason 409 | if 'ResponseURL' in request and request['ResponseURL']: 410 | url = urlparse.urlparse(request['ResponseURL']) 411 | body = json.dumps(response) 412 | https = httplib.HTTPSConnection(url.hostname) 413 | https.request('PUT', url.path+'?'+url.query, body) 414 | return response 415 | def lambda_handler(event, context): 416 | response = { 417 | 'StackId': event['StackId'], 418 | 'RequestId': event['RequestId'], 419 | 'LogicalResourceId': event['LogicalResourceId'], 420 | 'Status': 'SUCCESS' 421 | } 422 | # PhysicalResourceId is meaningless here, but CloudFormation requires it 423 | if 'PhysicalResourceId' in event: 424 | response['PhysicalResourceId'] = event['PhysicalResourceId'] 425 | else: 426 | response['PhysicalResourceId'] = str(uuid.uuid4()) 427 | # There is nothing to do for a delete request 428 | if event['RequestType'] == 'Delete': 429 | return send_response(event, response) 430 | # Encrypt the value using AWS KMS and return the response 431 | try: 432 | for key in ['KeyId', 'PlainText']: 433 | if key not in event['ResourceProperties'] or not event['ResourceProperties'][key]: 434 | return send_response( 435 | event, response, status='FAILED', 436 | reason='The properties KeyId and PlainText must not be empty' 437 | ) 438 | client = boto3.client('kms') 439 | encrypted = client.encrypt( 440 | KeyId=event['ResourceProperties']['KeyId'], 441 | Plaintext=event['ResourceProperties']['PlainText'] 442 | ) 443 | response['Data'] = { 444 | 'CipherText': base64.b64encode(encrypted['CiphertextBlob']) 445 | } 446 | response['Reason'] = 'The value was successfully encrypted' 447 | except Exception as E: 448 | response['Status'] = 'FAILED' 449 | response['Reason'] = 'Encryption Failed - See CloudWatch logs for the Lamba function backing the custom resource for details' 450 | return send_response(event, response) 451 | Runtime: python2.7 452 | ###### Redis ###### 453 | RedisAccessSecurityGroup: 454 | Type: AWS::EC2::SecurityGroup 455 | Properties: 456 | VpcId: 457 | Ref: VPC 458 | GroupDescription: Allows access only to sentry redis cluster 459 | Tags: 460 | - Key: Name 461 | Value: 462 | Fn::Join: 463 | - '-' 464 | - [{Ref: 'AWS::StackName'}, 'RedisAccess'] 465 | RedisSecurityGroup: 466 | Type: AWS::EC2::SecurityGroup 467 | Properties: 468 | GroupDescription: Senty redis cluster source 469 | SecurityGroupIngress: 470 | - IpProtocol: tcp 471 | FromPort: '6379' 472 | ToPort: '6379' 473 | SourceSecurityGroupId: 474 | Ref: RedisAccessSecurityGroup 475 | VpcId: 476 | Ref: VPC 477 | Tags: 478 | - Key: Name 479 | Value: 480 | Fn::Join: 481 | - '-' 482 | - [{Ref: 'AWS::StackName'}, 'Redis'] 483 | RedisSubnetGroup: 484 | Type: AWS::ElastiCache::SubnetGroup 485 | Properties: 486 | Description: Sentry stack redis subnet group 487 | SubnetIds: 488 | - Ref: PrivateSubnet1 489 | RedisCacheCluster: 490 | Type: AWS::ElastiCache::CacheCluster 491 | Properties: 492 | CacheNodeType: 493 | Ref: RedisNodeType 494 | CacheSubnetGroupName: 495 | Ref: RedisSubnetGroup 496 | Engine: redis 497 | EngineVersion: 498 | Ref: RedisEngineVersion 499 | NumCacheNodes: 500 | Ref: RedisNumNodes 501 | VpcSecurityGroupIds: 502 | - Ref: RedisSecurityGroup 503 | ###### Postgres ###### 504 | PostgresAccessSecurityGroup: 505 | Type: AWS::EC2::SecurityGroup 506 | Properties: 507 | VpcId: 508 | Ref: VPC 509 | GroupDescription: Allows access only to sentry postgres instance 510 | Tags: 511 | - Key: Name 512 | Value: 513 | Fn::Join: 514 | - '-' 515 | - [{Ref: 'AWS::StackName'}, 'PostgresAccess'] 516 | PostgresSecurityGroup: 517 | Type: AWS::EC2::SecurityGroup 518 | Properties: 519 | SecurityGroupIngress: 520 | - ToPort: '5432' 521 | IpProtocol: tcp 522 | FromPort: '5432' 523 | SourceSecurityGroupId: 524 | Ref: PostgresAccessSecurityGroup 525 | VpcId: 526 | Ref: VPC 527 | GroupDescription: Senty postgres instance source 528 | Tags: 529 | - Key: Name 530 | Value: 531 | Fn::Join: 532 | - '-' 533 | - [{Ref: 'AWS::StackName'}, 'Postgres'] 534 | PostgresSubnetGroup: 535 | Type: AWS::RDS::DBSubnetGroup 536 | Properties: 537 | DBSubnetGroupDescription: Sentry stack postgres subnet group 538 | SubnetIds: 539 | - Ref: PrivateSubnet1 540 | PostgresInstance: 541 | Type: AWS::RDS::DBInstance 542 | Properties: 543 | AllocatedStorage: 544 | Ref: DBAllocatedStorage 545 | BackupRetentionPeriod: 546 | Ref: DBBackupRetentionPeriod 547 | DBInstanceClass: 548 | Ref: DBInstanceClass 549 | DBName: 550 | Ref: DBName 551 | Engine: postgres 552 | KmsKeyId: 553 | Ref: SentryEncryptionKey 554 | MasterUsername: 555 | Ref: DBMasterUsername 556 | MasterUserPassword: 557 | Ref: DBMasterUserPassword 558 | MultiAZ: 559 | Ref: DBMultiAZ 560 | Port: '5432' 561 | PubliclyAccessible: 'false' 562 | StorageEncrypted: 'true' 563 | StorageType: 564 | Ref: DBStorageType 565 | VPCSecurityGroups: 566 | - Ref: PostgresSecurityGroup 567 | DBSubnetGroupName: 568 | Ref: PostgresSubnetGroup 569 | ###### File Storage ###### 570 | SentryFilesS3Bucket: 571 | Type: AWS::S3::Bucket 572 | Properties: 573 | BucketName: 574 | Fn::Join: 575 | - '' 576 | - - Ref: AWS::AccountId 577 | - "-" 578 | - Ref: AWS::StackName 579 | - "-sentry-files" 580 | AccessControl: Private 581 | ###### App Server ###### 582 | LoadBalancerSecurityGroup: 583 | Type: AWS::EC2::SecurityGroup 584 | Properties: 585 | SecurityGroupIngress: 586 | - ToPort: '443' 587 | IpProtocol: tcp 588 | FromPort: '443' 589 | CidrIp: 0.0.0.0/0 590 | - ToPort: '443' 591 | IpProtocol: tcp 592 | FromPort: '443' 593 | CidrIpv6: ::/0 594 | VpcId: 595 | Ref: VPC 596 | GroupDescription: An ELB group allowing access only to from the corresponding 597 | component 598 | Tags: 599 | - Key: Name 600 | Value: 601 | Fn::Join: 602 | - '-' 603 | - [{Ref: 'AWS::StackName'}, 'LoadBalancer'] 604 | SentryElasticLoadBalancer: 605 | Type: AWS::ElasticLoadBalancing::LoadBalancer 606 | Properties: 607 | Subnets: 608 | - Ref: PublicSubnet1 609 | Scheme: internet-facing 610 | Listeners: 611 | - InstancePort: '443' 612 | Protocol: HTTPS 613 | InstanceProtocol: HTTPS 614 | LoadBalancerPort: '443' 615 | SSLCertificateId: 616 | Ref: SSLCertARN 617 | PolicyNames: 618 | - SSLNegotiationPolicy 619 | Policies: 620 | - PolicyName : SSLNegotiationPolicy 621 | PolicyType: SSLNegotiationPolicyType 622 | Attributes: 623 | - Name: Protocol-TLSv1.2 624 | Value: 'true' 625 | - Name: Server-Defined-Cipher-Order 626 | Value: 'true' 627 | - Name: ECDHE-ECDSA-AES128-GCM-SHA256 628 | Value: 'true' 629 | - Name: ECDHE-RSA-AES128-GCM-SHA256 630 | Value: 'true' 631 | - Name: ECDHE-ECDSA-AES128-SHA256 632 | Value: 'true' 633 | - Name: ECDHE-RSA-AES128-SHA256 634 | Value: 'true' 635 | - Name: ECDHE-ECDSA-AES256-GCM-SHA384 636 | Value: 'true' 637 | - Name: ECDHE-RSA-AES256-GCM-SHA384 638 | Value: 'true' 639 | - Name: ECDHE-ECDSA-AES256-SHA384 640 | Value: 'true' 641 | - Name: ECDHE-RSA-AES256-SHA384 642 | Value: 'true' 643 | - Name: AES128-GCM-SHA256 644 | Value: 'true' 645 | - Name: AES128-SHA256 646 | Value: 'true' 647 | - Name: AES256-GCM-SHA384 648 | Value: 'true' 649 | - Name: AES256-SHA256 650 | Value: 'true' 651 | CrossZone: false 652 | SecurityGroups: 653 | - Ref: LoadBalancerSecurityGroup 654 | HealthCheck: 655 | HealthyThreshold: 3 656 | Interval: 10 657 | Timeout: 5 658 | UnhealthyThreshold: 10 659 | Target: HTTPS:443/_health/ 660 | SentryInstanceProfile: 661 | Type: AWS::IAM::InstanceProfile 662 | Properties: 663 | Path: "/" 664 | Roles: 665 | - Ref: SentryRole 666 | SentrySecurityGroup: 667 | Type: AWS::EC2::SecurityGroup 668 | Properties: 669 | SecurityGroupIngress: 670 | - ToPort: '443' 671 | IpProtocol: tcp 672 | FromPort: '443' 673 | SourceSecurityGroupId: 674 | Ref: LoadBalancerSecurityGroup 675 | VpcId: 676 | Ref: VPC 677 | GroupDescription: Sentry instance security group, gives access to from load balancer 678 | Tags: 679 | - Key: Name 680 | Value: 681 | Fn::Join: 682 | - '-' 683 | - [{Ref: 'AWS::StackName'}, 'Sentry'] 684 | EncryptedDeploymentHosts: 685 | Type: AWS::CloudFormation::CustomResource 686 | Version: "1.0" 687 | Properties: 688 | ServiceToken: !GetAtt EncryptionHelperFunction.Arn 689 | KeyId: 690 | Ref: SentryEncryptionKey 691 | PlainText: 692 | !Sub 693 | - | 694 | [aws] 695 | 127.0.0.1 696 | [aws:vars] 697 | user=ubuntu 698 | owner="${Owner}" 699 | sentry_admin_username="${SentryAdminUser}" 700 | sentry_admin_password="${SentryAdminPassword}" 701 | sentry_public_dns_name="${SentryPublicDnsName}" 702 | sentry_secret_key="${SentrySecretKey}" 703 | sentry_github_app_id="${SentryGithubAppId}" 704 | sentry_github_api_secret="${SentryGithubApiSecret}" 705 | sentry_url="${SentryUrl}" 706 | sentry_db_host="${DBHost}" 707 | sentry_db_port="5432" 708 | sentry_db_name="${DBName}" 709 | sentry_db_user="${DBMasterUsername}" 710 | sentry_db_password="${DBMasterUserPassword}" 711 | sentry_redis_host="${RedisHost}" 712 | sentry_redis_port="6379" 713 | sentry_mail_host="${SentryMailHost}" 714 | sentry_mail_port="${SentryMailPort}" 715 | sentry_mail_username="${SentryMailUsername}" 716 | sentry_mail_password="${SentryMailPassword}" 717 | sentry_mail_from="${SentryMailFrom}" 718 | sentry_files_bucket_name="${AWS::AccountId}-${AWS::StackName}-sentry-files" 719 | - SentryUrl: !GetAtt SentryElasticLoadBalancer.DNSName 720 | DBHost: !GetAtt PostgresInstance.Endpoint.Address 721 | RedisHost: !GetAtt RedisCacheCluster.RedisEndpoint.Address 722 | SentryLaunchConfiguration: 723 | Type: AWS::AutoScaling::LaunchConfiguration 724 | Properties: 725 | KeyName: 726 | Ref: KeyName 727 | ImageId: 728 | Ref: ImageId 729 | SecurityGroups: 730 | - Ref: SentrySecurityGroup 731 | - Ref: PostgresAccessSecurityGroup 732 | - Ref: RedisAccessSecurityGroup 733 | InstanceType: 734 | Ref: InstanceType 735 | IamInstanceProfile: 736 | Ref: SentryInstanceProfile 737 | UserData: 738 | Fn::Base64: 739 | !Sub 740 | - | 741 | #cloud-config 742 | runcmd: 743 | - apt-get update 744 | - apt-get install ansible awscli unzip -y 745 | - openssl req -new -nodes -x509 -subj "/C=GB/ST=London/L=London/O=Private/CN=${AWS::StackName}" -days 3650 -keyout /tmp/server.key -out /tmp/bundle.crt -extensions v3_ca 746 | - curl https://github.com/o2Labs/sentry-formation/archive/1.0.0.zip --output /tmp/repo.zip --location 747 | - unzip /tmp/repo.zip -d /tmp 748 | - echo "${DeploymentHosts}" > /tmp/hosts.base64 749 | - base64 -d /tmp/hosts.base64 > /tmp/hosts.encrypted 750 | - aws kms decrypt --region ${AWS::Region} --ciphertext-blob "fileb:///tmp/hosts.encrypted" --output text --query Plaintext > /tmp/hosts.decrypted 751 | - base64 -d /tmp/hosts.decrypted > /tmp/sentry-formation-1.0.0/hosts 752 | - ansible-playbook /tmp/sentry-formation-1.0.0/site.yml -i /tmp/sentry-formation-1.0.0/hosts 753 | - DeploymentHosts: !GetAtt EncryptedDeploymentHosts.CipherText 754 | SentryAutoScalingGroup: 755 | Type: AWS::AutoScaling::AutoScalingGroup 756 | UpdatePolicy: 757 | AutoScalingRollingUpdate: 758 | PauseTime: PT15M 759 | MaxBatchSize: 1 760 | MinInstancesInService: 1 761 | Properties: 762 | LoadBalancerNames: 763 | - Ref: SentryElasticLoadBalancer 764 | MinSize: 765 | Ref: ScalingMinNodes 766 | MaxSize: 767 | Ref: ScalingMaxNodes 768 | LaunchConfigurationName: 769 | Ref: SentryLaunchConfiguration 770 | Tags: 771 | - PropagateAtLaunch: true 772 | Key: Name 773 | Value: 774 | Ref: AWS::StackName 775 | VPCZoneIdentifier: 776 | - Ref: PrivateSubnet1 777 | SentryScalingPolicy: 778 | Type: AWS::AutoScaling::ScalingPolicy 779 | Properties: 780 | ScalingAdjustment: 1 781 | AutoScalingGroupName: 782 | Ref: SentryAutoScalingGroup 783 | AdjustmentType: ChangeInCapacity 784 | -------------------------------------------------------------------------------- /output/master-internet-facing-1az.yaml: -------------------------------------------------------------------------------- 1 | # This file was generated with lono. Do not edit directly, the changes will be lost. 2 | # More info: https://github.com/tongueroo/lono 3 | Description: Sentry.io internet-facing setup in 1 availability zones 4 | Parameters: 5 | #### Required #### 6 | Owner: 7 | Type: String 8 | Description: Name of the owner of the service (normally your company name) 9 | DBMasterUsername: 10 | Type: String 11 | MinLength: '1' 12 | MaxLength: '16' 13 | AllowedPattern: "[a-zA-Z][a-zA-Z0-9]*" 14 | Description: Username for database access 15 | DBMasterUserPassword: 16 | NoEcho: true 17 | Type: String 18 | Description: Password for database access - minimum 8 characters 19 | MinLength: '8' 20 | MaxLength: '41' 21 | AllowedPattern: "[a-zA-Z0-9]*" 22 | ConstraintDescription: must contain 8 alphanumeric characters. 23 | SSLCertARN: 24 | Description: The ARN of the ACM cert 25 | Type: String 26 | KeyName: 27 | Description: Name of existing EC2 keypair to enable SSH access to the created 28 | instances 29 | Type: AWS::EC2::KeyPair::KeyName 30 | SentryAdminUser: 31 | Type: String 32 | MinLength: '1' 33 | MaxLength: '30' 34 | Description: Username for root sentry access 35 | SentryAdminPassword: 36 | NoEcho: true 37 | Type: String 38 | Description: Password for root sentry access - minimum 20 characters 39 | MinLength: '20' 40 | SentryPublicDnsName: 41 | Type: String 42 | Description: Host name that users will type to get to sentry. 43 | SentrySecretKey: 44 | Type: String 45 | Description: Private key for encrypting user sessions. 46 | MinLength: '50' 47 | NoEcho: true 48 | SentryGithubAppId: 49 | Description: GitHub API App ID for SSO 50 | Type: String 51 | SentryGithubApiSecret: 52 | Description: GitHub API secret key for SSO 53 | Type: String 54 | NoEcho: true 55 | SentryMailUsername: 56 | Description: SMTP username for sentry to use to send email 57 | Type: String 58 | SentryMailPassword: 59 | Description: SMTP password for sentry to use to send email 60 | Type: String 61 | NoEcho: true 62 | SentryMailFrom: 63 | Description: Sending email address for sentry to use to send email 64 | Type: String 65 | #### Optional #### 66 | VpcCidr: 67 | AllowedPattern: '((\d{1,3})\.){3}\d{1,3}/\d{1,2}' 68 | Description: VPC Cidr block 69 | Default: 10.0.0.0/22 70 | Type: String 71 | VpcAvailabilityZone1: 72 | Description: The AvailabilityZone to use for subnet 1 73 | Type: AWS::EC2::AvailabilityZone::Name 74 | Default: eu-west-1a 75 | VpcPublicSubnetCIDR1: 76 | AllowedPattern: '((\d{1,3})\.){3}\d{1,3}/\d{1,2}' 77 | Default: 10.0.0.0/26 78 | Description: VPC CIDR Block for Public Subnet 1 79 | Type: String 80 | VpcPrivateSubnetCIDR1: 81 | AllowedPattern: '((\d{1,3})\.){3}\d{1,3}/\d{1,2}' 82 | Default: 10.0.2.0/26 83 | Description: VPC CIDR Block for Private Subnet 1 84 | Type: String 85 | ImageId: 86 | Description: The AMI to use for this Sentry - YUM compliant required 87 | Type: String 88 | Default: ami-6d48500b 89 | InstanceType: 90 | Description: The size of the EC2 instances 91 | Default: t2.medium 92 | Type: String 93 | DBName: 94 | Type: String 95 | Default: sentry 96 | MinLength: '1' 97 | MaxLength: '64' 98 | AllowedPattern: "[a-zA-Z][a-zA-Z0-9]*" 99 | ConstraintDescription: must begin with a letter and contain only alphanumeric 100 | characters. 101 | DBAllocatedStorage: 102 | Type: String 103 | Default: '5' 104 | DBBackupRetentionPeriod: 105 | Type: String 106 | Default: '7' 107 | DBInstanceClass: 108 | Type: String 109 | Default: db.t2.small 110 | DBMultiAZ: 111 | Type: String 112 | Default: false 113 | Description: Provides enhanced availablily for RDS 114 | DBStorageType: 115 | Type: String 116 | Default: gp2 117 | RedisEngineVersion: 118 | Type: String 119 | Description: Version of Redis engine to use. 120 | Default: '3.2.4' 121 | RedisNodeType: 122 | Type: String 123 | Default: cache.t2.small 124 | RedisNumNodes: 125 | Type: String 126 | Default: '1' 127 | SentryMailHost: 128 | Description: SMTP host for sentry to use to send email 129 | Type: String 130 | Default: email-smtp.eu-west-1.amazonaws.com 131 | SentryMailPort: 132 | Description: SMTP port for sentry to use to send email 133 | Type: String 134 | Default: '25' 135 | ScalingMinNodes: 136 | Type: Number 137 | Description: Minium size of the auto scaling group 138 | Default: 1 139 | ScalingMaxNodes: 140 | Type: Number 141 | Description: Maximum size of the auto scaling group 142 | Default: 2 143 | AWSTemplateFormatVersion: '2010-09-09' 144 | Resources: 145 | ###### Network ###### 146 | VPC: 147 | Type: 'AWS::EC2::VPC' 148 | Properties: 149 | CidrBlock: 150 | Ref: VpcCidr 151 | EnableDnsHostnames: true 152 | Tags: 153 | - Key: Name 154 | Value: 155 | Ref: AWS::StackName 156 | PublicSubnet1: 157 | Type: AWS::EC2::Subnet 158 | Properties: 159 | AvailabilityZone: {Ref: VpcAvailabilityZone1} 160 | CidrBlock: {Ref: VpcPublicSubnetCIDR1} 161 | MapPublicIpOnLaunch: true 162 | Tags: 163 | - Key: Name 164 | Value: 165 | Fn::Join: 166 | - '-' 167 | - [{Ref: 'AWS::StackName'}, 'public', {Ref: VpcAvailabilityZone1}] 168 | VpcId: {Ref: VPC} 169 | PrivateSubnet1: 170 | Type: AWS::EC2::Subnet 171 | Properties: 172 | AvailabilityZone: {Ref: VpcAvailabilityZone1} 173 | CidrBlock: {Ref: VpcPrivateSubnetCIDR1} 174 | MapPublicIpOnLaunch: false 175 | Tags: 176 | - Key: Name 177 | Value: 178 | Fn::Join: 179 | - '-' 180 | - [{Ref: 'AWS::StackName'}, 'private', {Ref: VpcAvailabilityZone1}] 181 | VpcId: {Ref: VPC} 182 | InternetGateway: 183 | Type: AWS::EC2::InternetGateway 184 | Properties: 185 | Tags: 186 | - Key: Name 187 | Value: 188 | Ref: AWS::StackName 189 | GatewayAttachment: 190 | Type: AWS::EC2::VPCGatewayAttachment 191 | Properties: 192 | InternetGatewayId: 193 | Ref: InternetGateway 194 | VpcId: 195 | Ref: VPC 196 | ###### Public Routing ###### 197 | PublicRouteTable: 198 | Type: AWS::EC2::RouteTable 199 | Properties: 200 | Tags: 201 | - Key: Name 202 | Value: 203 | Fn::Join: 204 | - '-' 205 | - [{Ref: 'AWS::StackName'}, 'public'] 206 | VpcId: 207 | Ref: VPC 208 | PublicRoute: 209 | Type: AWS::EC2::Route 210 | Properties: 211 | DestinationCidrBlock: 0.0.0.0/0 212 | GatewayId: 213 | Ref: InternetGateway 214 | RouteTableId: 215 | Ref: PublicRouteTable 216 | PublicSubnetAssoc1: 217 | Type: AWS::EC2::SubnetRouteTableAssociation 218 | Properties: 219 | RouteTableId: 220 | Ref: PublicRouteTable 221 | SubnetId: 222 | Ref: PublicSubnet1 223 | ###### Private Routing ###### 224 | PrivateRouteTable1: 225 | Type: AWS::EC2::RouteTable 226 | Properties: 227 | Tags: 228 | - Key: Name 229 | Value: 230 | Fn::Join: 231 | - '-' 232 | - [{Ref: 'AWS::StackName'}, 'private-1'] 233 | VpcId: 234 | Ref: VPC 235 | PrivateSubnetAssoc1: 236 | Type: AWS::EC2::SubnetRouteTableAssociation 237 | Properties: 238 | RouteTableId: 239 | Ref: PrivateRouteTable1 240 | SubnetId: 241 | Ref: PrivateSubnet1 242 | EIP1: 243 | Type: AWS::EC2::EIP 244 | Properties: 245 | Domain: vpc 246 | NAT1: 247 | DependsOn: GatewayAttachment 248 | Type: AWS::EC2::NatGateway 249 | Properties: 250 | AllocationId: 251 | Fn::GetAtt: 252 | - EIP1 253 | - AllocationId 254 | SubnetId: 255 | Ref: PublicSubnet1 256 | NATRoute1: 257 | Type: AWS::EC2::Route 258 | Properties: 259 | RouteTableId: 260 | Ref: PrivateRouteTable1 261 | DestinationCidrBlock: 0.0.0.0/0 262 | NatGatewayId: 263 | Ref: NAT1 264 | ###### Roles ###### 265 | RootEncryptionRole: 266 | Type: AWS::IAM::Role 267 | Properties: 268 | AssumeRolePolicyDocument: 269 | Version: "2012-10-17" 270 | Statement: 271 | - Effect: "Allow" 272 | Principal: 273 | AWS: "*" 274 | Action: 275 | - "sts:AssumeRole" 276 | Path: "/" 277 | LambdaExecutionRole: 278 | Type: AWS::IAM::Role 279 | Properties: 280 | AssumeRolePolicyDocument: 281 | Version: '2012-10-17' 282 | Statement: 283 | - Effect: Allow 284 | Principal: 285 | Service: 286 | - lambda.amazonaws.com 287 | Action: 288 | - sts:AssumeRole 289 | Path: "/" 290 | Policies: 291 | - PolicyName: root 292 | PolicyDocument: 293 | Version: '2012-10-17' 294 | Statement: 295 | - Effect: Allow 296 | Action: 297 | - logs:* 298 | Resource: arn:aws:logs:*:*:* 299 | SentryRole: 300 | Type: AWS::IAM::Role 301 | Properties: 302 | AssumeRolePolicyDocument: 303 | Version: "2012-10-17" 304 | Statement: 305 | - Effect: Allow 306 | Principal: 307 | Service: 308 | - ec2.amazonaws.com 309 | Action: 310 | - sts:AssumeRole 311 | Path: "/" 312 | Policies: 313 | - PolicyName: root 314 | PolicyDocument: 315 | Version: "2012-10-17" 316 | Statement: 317 | - Effect: Allow 318 | Action: 319 | - s3:PutObject 320 | - s3:GetObject 321 | - s3:DeleteObject 322 | Resource: 323 | - Fn::Join: 324 | - '' 325 | - - !GetAtt SentryFilesS3Bucket.Arn 326 | - "/*" 327 | ###### Encryption ###### 328 | SentryEncryptionKey: 329 | Type: "AWS::KMS::Key" 330 | Properties: 331 | Description: Sentry environment root encryption key 332 | KeyPolicy: 333 | Version: "2012-10-17" 334 | Statement: 335 | - Sid: "Enable IAM User Permissions" 336 | Effect: "Allow" 337 | Principal: 338 | AWS: 339 | Fn::Join: 340 | - '' 341 | - ['arn:aws:iam::', {Ref: 'AWS::AccountId'}, ':root'] 342 | Action: "kms:*" 343 | Resource: "*" 344 | - Sid: "Allow administration of the key" 345 | Effect: "Allow" 346 | Principal: 347 | AWS: 348 | - !GetAtt RootEncryptionRole.Arn 349 | Action: 350 | - "kms:Create*" 351 | - "kms:Describe*" 352 | - "kms:Enable*" 353 | - "kms:List*" 354 | - "kms:Put*" 355 | - "kms:Update*" 356 | - "kms:Revoke*" 357 | - "kms:Disable*" 358 | - "kms:Get*" 359 | - "kms:Delete*" 360 | - "kms:TagResource" 361 | - "kms:UntagResource" 362 | - "kms:ScheduleKeyDeletion" 363 | - "kms:CancelKeyDeletion" 364 | Resource: "*" 365 | - Sid: "Allow use of the key" 366 | Effect: "Allow" 367 | Principal: 368 | AWS: 369 | - !GetAtt LambdaExecutionRole.Arn 370 | - !GetAtt SentryRole.Arn 371 | Action: 372 | - "kms:Encrypt" 373 | - "kms:Decrypt" 374 | - "kms:ReEncrypt*" 375 | - "kms:GenerateDataKey*" 376 | - "kms:DescribeKey" 377 | Resource: "*" 378 | SentryEncryptionKeyAlias: 379 | Type: AWS::KMS::Alias 380 | Properties: 381 | AliasName: 382 | Fn::Join: 383 | - '' 384 | - ['alias/', {Ref: 'AWS::StackName'}] 385 | TargetKeyId: 386 | Ref: SentryEncryptionKey 387 | EncryptionHelperFunction: 388 | Type: AWS::Lambda::Function 389 | Properties: 390 | Handler: index.lambda_handler 391 | Role: !GetAtt LambdaExecutionRole.Arn 392 | Code: 393 | ZipFile: !Sub | 394 | import base64 395 | import uuid 396 | import httplib 397 | import urlparse 398 | import json 399 | import boto3 400 | def send_response(request, response, status=None, reason=None): 401 | """ Send our response to the pre-signed URL supplied by CloudFormation 402 | If no ResponseURL is found in the request, there is no place to send a 403 | response. This may be the case if the supplied event was for testing. 404 | """ 405 | if status is not None: 406 | response['Status'] = status 407 | if reason is not None: 408 | response['Reason'] = reason 409 | if 'ResponseURL' in request and request['ResponseURL']: 410 | url = urlparse.urlparse(request['ResponseURL']) 411 | body = json.dumps(response) 412 | https = httplib.HTTPSConnection(url.hostname) 413 | https.request('PUT', url.path+'?'+url.query, body) 414 | return response 415 | def lambda_handler(event, context): 416 | response = { 417 | 'StackId': event['StackId'], 418 | 'RequestId': event['RequestId'], 419 | 'LogicalResourceId': event['LogicalResourceId'], 420 | 'Status': 'SUCCESS' 421 | } 422 | # PhysicalResourceId is meaningless here, but CloudFormation requires it 423 | if 'PhysicalResourceId' in event: 424 | response['PhysicalResourceId'] = event['PhysicalResourceId'] 425 | else: 426 | response['PhysicalResourceId'] = str(uuid.uuid4()) 427 | # There is nothing to do for a delete request 428 | if event['RequestType'] == 'Delete': 429 | return send_response(event, response) 430 | # Encrypt the value using AWS KMS and return the response 431 | try: 432 | for key in ['KeyId', 'PlainText']: 433 | if key not in event['ResourceProperties'] or not event['ResourceProperties'][key]: 434 | return send_response( 435 | event, response, status='FAILED', 436 | reason='The properties KeyId and PlainText must not be empty' 437 | ) 438 | client = boto3.client('kms') 439 | encrypted = client.encrypt( 440 | KeyId=event['ResourceProperties']['KeyId'], 441 | Plaintext=event['ResourceProperties']['PlainText'] 442 | ) 443 | response['Data'] = { 444 | 'CipherText': base64.b64encode(encrypted['CiphertextBlob']) 445 | } 446 | response['Reason'] = 'The value was successfully encrypted' 447 | except Exception as E: 448 | response['Status'] = 'FAILED' 449 | response['Reason'] = 'Encryption Failed - See CloudWatch logs for the Lamba function backing the custom resource for details' 450 | return send_response(event, response) 451 | Runtime: python2.7 452 | ###### Redis ###### 453 | RedisAccessSecurityGroup: 454 | Type: AWS::EC2::SecurityGroup 455 | Properties: 456 | VpcId: 457 | Ref: VPC 458 | GroupDescription: Allows access only to sentry redis cluster 459 | Tags: 460 | - Key: Name 461 | Value: 462 | Fn::Join: 463 | - '-' 464 | - [{Ref: 'AWS::StackName'}, 'RedisAccess'] 465 | RedisSecurityGroup: 466 | Type: AWS::EC2::SecurityGroup 467 | Properties: 468 | GroupDescription: Senty redis cluster source 469 | SecurityGroupIngress: 470 | - IpProtocol: tcp 471 | FromPort: '6379' 472 | ToPort: '6379' 473 | SourceSecurityGroupId: 474 | Ref: RedisAccessSecurityGroup 475 | VpcId: 476 | Ref: VPC 477 | Tags: 478 | - Key: Name 479 | Value: 480 | Fn::Join: 481 | - '-' 482 | - [{Ref: 'AWS::StackName'}, 'Redis'] 483 | RedisSubnetGroup: 484 | Type: AWS::ElastiCache::SubnetGroup 485 | Properties: 486 | Description: Sentry stack redis subnet group 487 | SubnetIds: 488 | - Ref: PrivateSubnet1 489 | RedisCacheCluster: 490 | Type: AWS::ElastiCache::CacheCluster 491 | Properties: 492 | CacheNodeType: 493 | Ref: RedisNodeType 494 | CacheSubnetGroupName: 495 | Ref: RedisSubnetGroup 496 | Engine: redis 497 | EngineVersion: 498 | Ref: RedisEngineVersion 499 | NumCacheNodes: 500 | Ref: RedisNumNodes 501 | VpcSecurityGroupIds: 502 | - Ref: RedisSecurityGroup 503 | ###### Postgres ###### 504 | PostgresAccessSecurityGroup: 505 | Type: AWS::EC2::SecurityGroup 506 | Properties: 507 | VpcId: 508 | Ref: VPC 509 | GroupDescription: Allows access only to sentry postgres instance 510 | Tags: 511 | - Key: Name 512 | Value: 513 | Fn::Join: 514 | - '-' 515 | - [{Ref: 'AWS::StackName'}, 'PostgresAccess'] 516 | PostgresSecurityGroup: 517 | Type: AWS::EC2::SecurityGroup 518 | Properties: 519 | SecurityGroupIngress: 520 | - ToPort: '5432' 521 | IpProtocol: tcp 522 | FromPort: '5432' 523 | SourceSecurityGroupId: 524 | Ref: PostgresAccessSecurityGroup 525 | VpcId: 526 | Ref: VPC 527 | GroupDescription: Senty postgres instance source 528 | Tags: 529 | - Key: Name 530 | Value: 531 | Fn::Join: 532 | - '-' 533 | - [{Ref: 'AWS::StackName'}, 'Postgres'] 534 | PostgresSubnetGroup: 535 | Type: AWS::RDS::DBSubnetGroup 536 | Properties: 537 | DBSubnetGroupDescription: Sentry stack postgres subnet group 538 | SubnetIds: 539 | - Ref: PrivateSubnet1 540 | PostgresInstance: 541 | Type: AWS::RDS::DBInstance 542 | Properties: 543 | AllocatedStorage: 544 | Ref: DBAllocatedStorage 545 | BackupRetentionPeriod: 546 | Ref: DBBackupRetentionPeriod 547 | DBInstanceClass: 548 | Ref: DBInstanceClass 549 | DBName: 550 | Ref: DBName 551 | Engine: postgres 552 | KmsKeyId: 553 | Ref: SentryEncryptionKey 554 | MasterUsername: 555 | Ref: DBMasterUsername 556 | MasterUserPassword: 557 | Ref: DBMasterUserPassword 558 | MultiAZ: 559 | Ref: DBMultiAZ 560 | Port: '5432' 561 | PubliclyAccessible: 'false' 562 | StorageEncrypted: 'true' 563 | StorageType: 564 | Ref: DBStorageType 565 | VPCSecurityGroups: 566 | - Ref: PostgresSecurityGroup 567 | DBSubnetGroupName: 568 | Ref: PostgresSubnetGroup 569 | ###### File Storage ###### 570 | SentryFilesS3Bucket: 571 | Type: AWS::S3::Bucket 572 | Properties: 573 | BucketName: 574 | Fn::Join: 575 | - '' 576 | - - Ref: AWS::AccountId 577 | - "-" 578 | - Ref: AWS::StackName 579 | - "-sentry-files" 580 | AccessControl: Private 581 | ###### App Server ###### 582 | LoadBalancerSecurityGroup: 583 | Type: AWS::EC2::SecurityGroup 584 | Properties: 585 | SecurityGroupIngress: 586 | - ToPort: '443' 587 | IpProtocol: tcp 588 | FromPort: '443' 589 | CidrIp: 0.0.0.0/0 590 | - ToPort: '443' 591 | IpProtocol: tcp 592 | FromPort: '443' 593 | CidrIpv6: ::/0 594 | VpcId: 595 | Ref: VPC 596 | GroupDescription: An ELB group allowing access only to from the corresponding 597 | component 598 | Tags: 599 | - Key: Name 600 | Value: 601 | Fn::Join: 602 | - '-' 603 | - [{Ref: 'AWS::StackName'}, 'LoadBalancer'] 604 | SentryElasticLoadBalancer: 605 | Type: AWS::ElasticLoadBalancing::LoadBalancer 606 | Properties: 607 | Subnets: 608 | - Ref: PublicSubnet1 609 | Scheme: internet-facing 610 | Listeners: 611 | - InstancePort: '443' 612 | Protocol: HTTPS 613 | InstanceProtocol: HTTPS 614 | LoadBalancerPort: '443' 615 | SSLCertificateId: 616 | Ref: SSLCertARN 617 | PolicyNames: 618 | - SSLNegotiationPolicy 619 | Policies: 620 | - PolicyName : SSLNegotiationPolicy 621 | PolicyType: SSLNegotiationPolicyType 622 | Attributes: 623 | - Name: Protocol-TLSv1.2 624 | Value: 'true' 625 | - Name: Server-Defined-Cipher-Order 626 | Value: 'true' 627 | - Name: ECDHE-ECDSA-AES128-GCM-SHA256 628 | Value: 'true' 629 | - Name: ECDHE-RSA-AES128-GCM-SHA256 630 | Value: 'true' 631 | - Name: ECDHE-ECDSA-AES128-SHA256 632 | Value: 'true' 633 | - Name: ECDHE-RSA-AES128-SHA256 634 | Value: 'true' 635 | - Name: ECDHE-ECDSA-AES256-GCM-SHA384 636 | Value: 'true' 637 | - Name: ECDHE-RSA-AES256-GCM-SHA384 638 | Value: 'true' 639 | - Name: ECDHE-ECDSA-AES256-SHA384 640 | Value: 'true' 641 | - Name: ECDHE-RSA-AES256-SHA384 642 | Value: 'true' 643 | - Name: AES128-GCM-SHA256 644 | Value: 'true' 645 | - Name: AES128-SHA256 646 | Value: 'true' 647 | - Name: AES256-GCM-SHA384 648 | Value: 'true' 649 | - Name: AES256-SHA256 650 | Value: 'true' 651 | CrossZone: false 652 | SecurityGroups: 653 | - Ref: LoadBalancerSecurityGroup 654 | HealthCheck: 655 | HealthyThreshold: 3 656 | Interval: 10 657 | Timeout: 5 658 | UnhealthyThreshold: 10 659 | Target: HTTPS:443/_health/ 660 | SentryInstanceProfile: 661 | Type: AWS::IAM::InstanceProfile 662 | Properties: 663 | Path: "/" 664 | Roles: 665 | - Ref: SentryRole 666 | SentrySecurityGroup: 667 | Type: AWS::EC2::SecurityGroup 668 | Properties: 669 | SecurityGroupIngress: 670 | - ToPort: '443' 671 | IpProtocol: tcp 672 | FromPort: '443' 673 | SourceSecurityGroupId: 674 | Ref: LoadBalancerSecurityGroup 675 | VpcId: 676 | Ref: VPC 677 | GroupDescription: Sentry instance security group, gives access to from load balancer 678 | Tags: 679 | - Key: Name 680 | Value: 681 | Fn::Join: 682 | - '-' 683 | - [{Ref: 'AWS::StackName'}, 'Sentry'] 684 | EncryptedDeploymentHosts: 685 | Type: AWS::CloudFormation::CustomResource 686 | Version: "1.0" 687 | Properties: 688 | ServiceToken: !GetAtt EncryptionHelperFunction.Arn 689 | KeyId: 690 | Ref: SentryEncryptionKey 691 | PlainText: 692 | !Sub 693 | - | 694 | [aws] 695 | 127.0.0.1 696 | [aws:vars] 697 | user=ubuntu 698 | owner="${Owner}" 699 | sentry_admin_username="${SentryAdminUser}" 700 | sentry_admin_password="${SentryAdminPassword}" 701 | sentry_public_dns_name="${SentryPublicDnsName}" 702 | sentry_secret_key="${SentrySecretKey}" 703 | sentry_github_app_id="${SentryGithubAppId}" 704 | sentry_github_api_secret="${SentryGithubApiSecret}" 705 | sentry_url="${SentryUrl}" 706 | sentry_db_host="${DBHost}" 707 | sentry_db_port="5432" 708 | sentry_db_name="${DBName}" 709 | sentry_db_user="${DBMasterUsername}" 710 | sentry_db_password="${DBMasterUserPassword}" 711 | sentry_redis_host="${RedisHost}" 712 | sentry_redis_port="6379" 713 | sentry_mail_host="${SentryMailHost}" 714 | sentry_mail_port="${SentryMailPort}" 715 | sentry_mail_username="${SentryMailUsername}" 716 | sentry_mail_password="${SentryMailPassword}" 717 | sentry_mail_from="${SentryMailFrom}" 718 | sentry_files_bucket_name="${AWS::AccountId}-${AWS::StackName}-sentry-files" 719 | - SentryUrl: !GetAtt SentryElasticLoadBalancer.DNSName 720 | DBHost: !GetAtt PostgresInstance.Endpoint.Address 721 | RedisHost: !GetAtt RedisCacheCluster.RedisEndpoint.Address 722 | SentryLaunchConfiguration: 723 | Type: AWS::AutoScaling::LaunchConfiguration 724 | Properties: 725 | KeyName: 726 | Ref: KeyName 727 | ImageId: 728 | Ref: ImageId 729 | SecurityGroups: 730 | - Ref: SentrySecurityGroup 731 | - Ref: PostgresAccessSecurityGroup 732 | - Ref: RedisAccessSecurityGroup 733 | InstanceType: 734 | Ref: InstanceType 735 | IamInstanceProfile: 736 | Ref: SentryInstanceProfile 737 | UserData: 738 | Fn::Base64: 739 | !Sub 740 | - | 741 | #cloud-config 742 | runcmd: 743 | - apt-get update 744 | - apt-get install ansible awscli unzip -y 745 | - openssl req -new -nodes -x509 -subj "/C=GB/ST=London/L=London/O=Private/CN=${AWS::StackName}" -days 3650 -keyout /tmp/server.key -out /tmp/bundle.crt -extensions v3_ca 746 | - curl https://github.com/o2Labs/sentry-formation/archive/master.zip --output /tmp/repo.zip --location 747 | - unzip /tmp/repo.zip -d /tmp 748 | - echo "${DeploymentHosts}" > /tmp/hosts.base64 749 | - base64 -d /tmp/hosts.base64 > /tmp/hosts.encrypted 750 | - aws kms decrypt --region ${AWS::Region} --ciphertext-blob "fileb:///tmp/hosts.encrypted" --output text --query Plaintext > /tmp/hosts.decrypted 751 | - base64 -d /tmp/hosts.decrypted > /tmp/sentry-formation-master/hosts 752 | - ansible-playbook /tmp/sentry-formation-master/site.yml -i /tmp/sentry-formation-master/hosts 753 | - DeploymentHosts: !GetAtt EncryptedDeploymentHosts.CipherText 754 | SentryAutoScalingGroup: 755 | Type: AWS::AutoScaling::AutoScalingGroup 756 | UpdatePolicy: 757 | AutoScalingRollingUpdate: 758 | PauseTime: PT15M 759 | MaxBatchSize: 1 760 | MinInstancesInService: 1 761 | Properties: 762 | LoadBalancerNames: 763 | - Ref: SentryElasticLoadBalancer 764 | MinSize: 765 | Ref: ScalingMinNodes 766 | MaxSize: 767 | Ref: ScalingMaxNodes 768 | LaunchConfigurationName: 769 | Ref: SentryLaunchConfiguration 770 | Tags: 771 | - PropagateAtLaunch: true 772 | Key: Name 773 | Value: 774 | Ref: AWS::StackName 775 | VPCZoneIdentifier: 776 | - Ref: PrivateSubnet1 777 | SentryScalingPolicy: 778 | Type: AWS::AutoScaling::ScalingPolicy 779 | Properties: 780 | ScalingAdjustment: 1 781 | AutoScalingGroupName: 782 | Ref: SentryAutoScalingGroup 783 | AdjustmentType: ChangeInCapacity 784 | -------------------------------------------------------------------------------- /templates/sentry-formation.yaml.erb: -------------------------------------------------------------------------------- 1 | Description: <%= @Description %> 2 | Parameters: 3 | 4 | #### Required #### 5 | Owner: 6 | Type: String 7 | Description: Name of the owner of the service (normally your company name) 8 | DBMasterUsername: 9 | Type: String 10 | MinLength: '1' 11 | MaxLength: '16' 12 | AllowedPattern: "[a-zA-Z][a-zA-Z0-9]*" 13 | Description: Username for database access 14 | DBMasterUserPassword: 15 | NoEcho: true 16 | Type: String 17 | Description: Password for database access - minimum 8 characters 18 | MinLength: '8' 19 | MaxLength: '41' 20 | AllowedPattern: "[a-zA-Z0-9]*" 21 | ConstraintDescription: must contain 8 alphanumeric characters. 22 | SSLCertARN: 23 | Description: The ARN of the ACM cert 24 | Type: String 25 | KeyName: 26 | Description: Name of existing EC2 keypair to enable SSH access to the created 27 | instances 28 | Type: AWS::EC2::KeyPair::KeyName 29 | SentryAdminUser: 30 | Type: String 31 | MinLength: '1' 32 | MaxLength: '30' 33 | Description: Username for root sentry access 34 | SentryAdminPassword: 35 | NoEcho: true 36 | Type: String 37 | Description: Password for root sentry access - minimum 20 characters 38 | MinLength: '20' 39 | SentryPublicDnsName: 40 | Type: String 41 | Description: Host name that users will type to get to sentry. 42 | SentrySecretKey: 43 | Type: String 44 | Description: Private key for encrypting user sessions. 45 | MinLength: '50' 46 | NoEcho: true 47 | SentryGithubAppId: 48 | Description: GitHub API App ID for SSO 49 | Type: String 50 | SentryGithubApiSecret: 51 | Description: GitHub API secret key for SSO 52 | Type: String 53 | NoEcho: true 54 | SentryMailUsername: 55 | Description: SMTP username for sentry to use to send email 56 | Type: String 57 | SentryMailPassword: 58 | Description: SMTP password for sentry to use to send email 59 | Type: String 60 | NoEcho: true 61 | SentryMailFrom: 62 | Description: Sending email address for sentry to use to send email 63 | Type: String 64 | 65 | #### Optional #### 66 | VpcCidr: 67 | AllowedPattern: '((\d{1,3})\.){3}\d{1,3}/\d{1,2}' 68 | Description: VPC Cidr block 69 | Default: 10.0.0.0/22 70 | Type: String 71 | <% for @num in 1..@availability_zones -%> 72 | VpcAvailabilityZone<%= @num %>: 73 | Description: The AvailabilityZone to use for subnet <%= @num %> 74 | Type: AWS::EC2::AvailabilityZone::Name 75 | Default: eu-west-1<%= (@num + 96).chr %> 76 | <% end -%> 77 | <% for @num in 1..@availability_zones -%> 78 | VpcPublicSubnetCIDR<%= @num %>: 79 | AllowedPattern: '((\d{1,3})\.){3}\d{1,3}/\d{1,2}' 80 | Default: 10.0.<%= ((@num - 1) / 2).floor %>.<%= (@num - 1) * 64 % 128 %>/26 81 | Description: VPC CIDR Block for Public Subnet <%= @num %> 82 | Type: String 83 | <% end -%> 84 | <% for @num in 1..@availability_zones -%> 85 | VpcPrivateSubnetCIDR<%= @num %>: 86 | AllowedPattern: '((\d{1,3})\.){3}\d{1,3}/\d{1,2}' 87 | Default: 10.0.<%= ((@num + 3) / 2).floor %>.<%= (@num - 1) * 64 % 128 %>/26 88 | Description: VPC CIDR Block for Private Subnet <%= @num %> 89 | Type: String 90 | <% end -%> 91 | ImageId: 92 | Description: The AMI to use for this Sentry - YUM compliant required 93 | Type: String 94 | Default: ami-6d48500b 95 | InstanceType: 96 | Description: The size of the EC2 instances 97 | Default: t2.medium 98 | Type: String 99 | DBName: 100 | Type: String 101 | Default: sentry 102 | MinLength: '1' 103 | MaxLength: '64' 104 | AllowedPattern: "[a-zA-Z][a-zA-Z0-9]*" 105 | ConstraintDescription: must begin with a letter and contain only alphanumeric 106 | characters. 107 | DBAllocatedStorage: 108 | Type: String 109 | Default: '5' 110 | DBBackupRetentionPeriod: 111 | Type: String 112 | Default: '7' 113 | DBInstanceClass: 114 | Type: String 115 | Default: db.t2.small 116 | DBMultiAZ: 117 | Type: String 118 | Default: <%= @availability_zones > 1 %> 119 | Description: Provides enhanced availablily for RDS 120 | DBStorageType: 121 | Type: String 122 | Default: gp2 123 | RedisEngineVersion: 124 | Type: String 125 | Description: Version of Redis engine to use. 126 | Default: '3.2.4' 127 | RedisNodeType: 128 | Type: String 129 | Default: cache.t2.small 130 | RedisNumNodes: 131 | Type: String 132 | Default: '1' 133 | SentryMailHost: 134 | Description: SMTP host for sentry to use to send email 135 | Type: String 136 | Default: email-smtp.eu-west-1.amazonaws.com 137 | SentryMailPort: 138 | Description: SMTP port for sentry to use to send email 139 | Type: String 140 | Default: '25' 141 | ScalingMinNodes: 142 | Type: Number 143 | Description: Minium size of the auto scaling group 144 | Default: 1 145 | ScalingMaxNodes: 146 | Type: Number 147 | Description: Maximum size of the auto scaling group 148 | Default: 2 149 | 150 | AWSTemplateFormatVersion: '2010-09-09' 151 | Resources: 152 | ###### Network ###### 153 | VPC: 154 | Type: 'AWS::EC2::VPC' 155 | Properties: 156 | CidrBlock: 157 | Ref: VpcCidr 158 | EnableDnsHostnames: true 159 | Tags: 160 | - Key: Name 161 | Value: 162 | Ref: AWS::StackName 163 | 164 | <% for @num in 1..@availability_zones -%> 165 | PublicSubnet<%= @num %>: 166 | Type: AWS::EC2::Subnet 167 | Properties: 168 | AvailabilityZone: {Ref: VpcAvailabilityZone<%= @num %>} 169 | CidrBlock: {Ref: VpcPublicSubnetCIDR<%= @num %>} 170 | MapPublicIpOnLaunch: true 171 | Tags: 172 | - Key: Name 173 | Value: 174 | Fn::Join: 175 | - '-' 176 | - [{Ref: 'AWS::StackName'}, 'public', {Ref: VpcAvailabilityZone<%= @num %>}] 177 | VpcId: {Ref: VPC} 178 | <% end -%> 179 | 180 | <% for @num in 1..@availability_zones -%> 181 | PrivateSubnet<%= @num %>: 182 | Type: AWS::EC2::Subnet 183 | Properties: 184 | AvailabilityZone: {Ref: VpcAvailabilityZone<%= @num %>} 185 | CidrBlock: {Ref: VpcPrivateSubnetCIDR<%= @num %>} 186 | MapPublicIpOnLaunch: false 187 | Tags: 188 | - Key: Name 189 | Value: 190 | Fn::Join: 191 | - '-' 192 | - [{Ref: 'AWS::StackName'}, 'private', {Ref: VpcAvailabilityZone<%= @num %>}] 193 | VpcId: {Ref: VPC} 194 | <% end -%> 195 | 196 | InternetGateway: 197 | Type: AWS::EC2::InternetGateway 198 | Properties: 199 | Tags: 200 | - Key: Name 201 | Value: 202 | Ref: AWS::StackName 203 | 204 | GatewayAttachment: 205 | Type: AWS::EC2::VPCGatewayAttachment 206 | Properties: 207 | InternetGatewayId: 208 | Ref: InternetGateway 209 | VpcId: 210 | Ref: VPC 211 | 212 | ###### Public Routing ###### 213 | PublicRouteTable: 214 | Type: AWS::EC2::RouteTable 215 | Properties: 216 | Tags: 217 | - Key: Name 218 | Value: 219 | Fn::Join: 220 | - '-' 221 | - [{Ref: 'AWS::StackName'}, 'public'] 222 | VpcId: 223 | Ref: VPC 224 | 225 | PublicRoute: 226 | Type: AWS::EC2::Route 227 | Properties: 228 | DestinationCidrBlock: 0.0.0.0/0 229 | GatewayId: 230 | Ref: InternetGateway 231 | RouteTableId: 232 | Ref: PublicRouteTable 233 | 234 | <% for @num in 1..@availability_zones -%> 235 | PublicSubnetAssoc<%= @num %>: 236 | Type: AWS::EC2::SubnetRouteTableAssociation 237 | Properties: 238 | RouteTableId: 239 | Ref: PublicRouteTable 240 | SubnetId: 241 | Ref: PublicSubnet<%= @num %> 242 | <% end -%> 243 | 244 | ###### Private Routing ###### 245 | <% for @num in 1..@availability_zones -%> 246 | PrivateRouteTable<%= @num %>: 247 | Type: AWS::EC2::RouteTable 248 | Properties: 249 | Tags: 250 | - Key: Name 251 | Value: 252 | Fn::Join: 253 | - '-' 254 | - [{Ref: 'AWS::StackName'}, 'private-<%= @num %>'] 255 | VpcId: 256 | Ref: VPC 257 | 258 | PrivateSubnetAssoc<%= @num %>: 259 | Type: AWS::EC2::SubnetRouteTableAssociation 260 | Properties: 261 | RouteTableId: 262 | Ref: PrivateRouteTable<%= @num %> 263 | SubnetId: 264 | Ref: PrivateSubnet<%= @num %> 265 | 266 | EIP<%= @num %>: 267 | Type: AWS::EC2::EIP 268 | Properties: 269 | Domain: vpc 270 | 271 | NAT<%= @num %>: 272 | DependsOn: GatewayAttachment 273 | Type: AWS::EC2::NatGateway 274 | Properties: 275 | AllocationId: 276 | Fn::GetAtt: 277 | - EIP<%= @num %> 278 | - AllocationId 279 | SubnetId: 280 | Ref: PublicSubnet<%= @num %> 281 | 282 | NATRoute<%= @num %>: 283 | Type: AWS::EC2::Route 284 | Properties: 285 | RouteTableId: 286 | Ref: PrivateRouteTable<%= @num %> 287 | DestinationCidrBlock: 0.0.0.0/0 288 | NatGatewayId: 289 | Ref: NAT<%= @num %> 290 | <% end -%> 291 | 292 | ###### Roles ###### 293 | 294 | RootEncryptionRole: 295 | Type: AWS::IAM::Role 296 | Properties: 297 | AssumeRolePolicyDocument: 298 | Version: "2012-10-17" 299 | Statement: 300 | - Effect: "Allow" 301 | Principal: 302 | AWS: "*" 303 | Action: 304 | - "sts:AssumeRole" 305 | Path: "/" 306 | 307 | LambdaExecutionRole: 308 | Type: AWS::IAM::Role 309 | Properties: 310 | AssumeRolePolicyDocument: 311 | Version: '2012-10-17' 312 | Statement: 313 | - Effect: Allow 314 | Principal: 315 | Service: 316 | - lambda.amazonaws.com 317 | Action: 318 | - sts:AssumeRole 319 | Path: "/" 320 | Policies: 321 | - PolicyName: root 322 | PolicyDocument: 323 | Version: '2012-10-17' 324 | Statement: 325 | - Effect: Allow 326 | Action: 327 | - logs:* 328 | Resource: arn:aws:logs:*:*:* 329 | 330 | SentryRole: 331 | Type: AWS::IAM::Role 332 | Properties: 333 | AssumeRolePolicyDocument: 334 | Version: "2012-10-17" 335 | Statement: 336 | - Effect: Allow 337 | Principal: 338 | Service: 339 | - ec2.amazonaws.com 340 | Action: 341 | - sts:AssumeRole 342 | Path: "/" 343 | Policies: 344 | - PolicyName: root 345 | PolicyDocument: 346 | Version: "2012-10-17" 347 | Statement: 348 | - Effect: Allow 349 | Action: 350 | - s3:PutObject 351 | - s3:GetObject 352 | - s3:DeleteObject 353 | Resource: 354 | - Fn::Join: 355 | - '' 356 | - - !GetAtt SentryFilesS3Bucket.Arn 357 | - "/*" 358 | 359 | ###### Encryption ###### 360 | 361 | SentryEncryptionKey: 362 | Type: "AWS::KMS::Key" 363 | Properties: 364 | Description: Sentry environment root encryption key 365 | KeyPolicy: 366 | Version: "2012-10-17" 367 | Statement: 368 | - Sid: "Enable IAM User Permissions" 369 | Effect: "Allow" 370 | Principal: 371 | AWS: 372 | Fn::Join: 373 | - '' 374 | - ['arn:aws:iam::', {Ref: 'AWS::AccountId'}, ':root'] 375 | Action: "kms:*" 376 | Resource: "*" 377 | - Sid: "Allow administration of the key" 378 | Effect: "Allow" 379 | Principal: 380 | AWS: 381 | - !GetAtt RootEncryptionRole.Arn 382 | Action: 383 | - "kms:Create*" 384 | - "kms:Describe*" 385 | - "kms:Enable*" 386 | - "kms:List*" 387 | - "kms:Put*" 388 | - "kms:Update*" 389 | - "kms:Revoke*" 390 | - "kms:Disable*" 391 | - "kms:Get*" 392 | - "kms:Delete*" 393 | - "kms:TagResource" 394 | - "kms:UntagResource" 395 | - "kms:ScheduleKeyDeletion" 396 | - "kms:CancelKeyDeletion" 397 | Resource: "*" 398 | - Sid: "Allow use of the key" 399 | Effect: "Allow" 400 | Principal: 401 | AWS: 402 | - !GetAtt LambdaExecutionRole.Arn 403 | - !GetAtt SentryRole.Arn 404 | Action: 405 | - "kms:Encrypt" 406 | - "kms:Decrypt" 407 | - "kms:ReEncrypt*" 408 | - "kms:GenerateDataKey*" 409 | - "kms:DescribeKey" 410 | Resource: "*" 411 | 412 | SentryEncryptionKeyAlias: 413 | Type: AWS::KMS::Alias 414 | Properties: 415 | AliasName: 416 | Fn::Join: 417 | - '' 418 | - ['alias/', {Ref: 'AWS::StackName'}] 419 | TargetKeyId: 420 | Ref: SentryEncryptionKey 421 | 422 | EncryptionHelperFunction: 423 | Type: AWS::Lambda::Function 424 | Properties: 425 | Handler: index.lambda_handler 426 | Role: !GetAtt LambdaExecutionRole.Arn 427 | Code: 428 | ZipFile: !Sub | 429 | import base64 430 | import uuid 431 | import httplib 432 | import urlparse 433 | import json 434 | import boto3 435 | 436 | def send_response(request, response, status=None, reason=None): 437 | """ Send our response to the pre-signed URL supplied by CloudFormation 438 | 439 | If no ResponseURL is found in the request, there is no place to send a 440 | response. This may be the case if the supplied event was for testing. 441 | """ 442 | 443 | if status is not None: 444 | response['Status'] = status 445 | 446 | if reason is not None: 447 | response['Reason'] = reason 448 | 449 | if 'ResponseURL' in request and request['ResponseURL']: 450 | url = urlparse.urlparse(request['ResponseURL']) 451 | body = json.dumps(response) 452 | https = httplib.HTTPSConnection(url.hostname) 453 | https.request('PUT', url.path+'?'+url.query, body) 454 | 455 | return response 456 | 457 | def lambda_handler(event, context): 458 | 459 | response = { 460 | 'StackId': event['StackId'], 461 | 'RequestId': event['RequestId'], 462 | 'LogicalResourceId': event['LogicalResourceId'], 463 | 'Status': 'SUCCESS' 464 | } 465 | 466 | # PhysicalResourceId is meaningless here, but CloudFormation requires it 467 | if 'PhysicalResourceId' in event: 468 | response['PhysicalResourceId'] = event['PhysicalResourceId'] 469 | else: 470 | response['PhysicalResourceId'] = str(uuid.uuid4()) 471 | 472 | # There is nothing to do for a delete request 473 | if event['RequestType'] == 'Delete': 474 | return send_response(event, response) 475 | 476 | # Encrypt the value using AWS KMS and return the response 477 | try: 478 | 479 | for key in ['KeyId', 'PlainText']: 480 | if key not in event['ResourceProperties'] or not event['ResourceProperties'][key]: 481 | return send_response( 482 | event, response, status='FAILED', 483 | reason='The properties KeyId and PlainText must not be empty' 484 | ) 485 | 486 | client = boto3.client('kms') 487 | encrypted = client.encrypt( 488 | KeyId=event['ResourceProperties']['KeyId'], 489 | Plaintext=event['ResourceProperties']['PlainText'] 490 | ) 491 | 492 | response['Data'] = { 493 | 'CipherText': base64.b64encode(encrypted['CiphertextBlob']) 494 | } 495 | response['Reason'] = 'The value was successfully encrypted' 496 | 497 | except Exception as E: 498 | response['Status'] = 'FAILED' 499 | response['Reason'] = 'Encryption Failed - See CloudWatch logs for the Lamba function backing the custom resource for details' 500 | 501 | return send_response(event, response) 502 | Runtime: python2.7 503 | 504 | ###### Redis ###### 505 | RedisAccessSecurityGroup: 506 | Type: AWS::EC2::SecurityGroup 507 | Properties: 508 | VpcId: 509 | Ref: VPC 510 | GroupDescription: Allows access only to sentry redis cluster 511 | Tags: 512 | - Key: Name 513 | Value: 514 | Fn::Join: 515 | - '-' 516 | - [{Ref: 'AWS::StackName'}, 'RedisAccess'] 517 | 518 | RedisSecurityGroup: 519 | Type: AWS::EC2::SecurityGroup 520 | Properties: 521 | GroupDescription: Senty redis cluster source 522 | SecurityGroupIngress: 523 | - IpProtocol: tcp 524 | FromPort: '6379' 525 | ToPort: '6379' 526 | SourceSecurityGroupId: 527 | Ref: RedisAccessSecurityGroup 528 | VpcId: 529 | Ref: VPC 530 | Tags: 531 | - Key: Name 532 | Value: 533 | Fn::Join: 534 | - '-' 535 | - [{Ref: 'AWS::StackName'}, 'Redis'] 536 | 537 | RedisSubnetGroup: 538 | Type: AWS::ElastiCache::SubnetGroup 539 | Properties: 540 | Description: Sentry stack redis subnet group 541 | SubnetIds: 542 | <% for @num in 1..@availability_zones -%> 543 | - Ref: PrivateSubnet<%= @num %> 544 | <% end -%> 545 | 546 | RedisCacheCluster: 547 | Type: AWS::ElastiCache::CacheCluster 548 | Properties: 549 | CacheNodeType: 550 | Ref: RedisNodeType 551 | CacheSubnetGroupName: 552 | Ref: RedisSubnetGroup 553 | Engine: redis 554 | EngineVersion: 555 | Ref: RedisEngineVersion 556 | NumCacheNodes: 557 | Ref: RedisNumNodes 558 | VpcSecurityGroupIds: 559 | - Ref: RedisSecurityGroup 560 | 561 | ###### Postgres ###### 562 | PostgresAccessSecurityGroup: 563 | Type: AWS::EC2::SecurityGroup 564 | Properties: 565 | VpcId: 566 | Ref: VPC 567 | GroupDescription: Allows access only to sentry postgres instance 568 | Tags: 569 | - Key: Name 570 | Value: 571 | Fn::Join: 572 | - '-' 573 | - [{Ref: 'AWS::StackName'}, 'PostgresAccess'] 574 | 575 | PostgresSecurityGroup: 576 | Type: AWS::EC2::SecurityGroup 577 | Properties: 578 | SecurityGroupIngress: 579 | - ToPort: '5432' 580 | IpProtocol: tcp 581 | FromPort: '5432' 582 | SourceSecurityGroupId: 583 | Ref: PostgresAccessSecurityGroup 584 | VpcId: 585 | Ref: VPC 586 | GroupDescription: Senty postgres instance source 587 | Tags: 588 | - Key: Name 589 | Value: 590 | Fn::Join: 591 | - '-' 592 | - [{Ref: 'AWS::StackName'}, 'Postgres'] 593 | 594 | PostgresSubnetGroup: 595 | Type: AWS::RDS::DBSubnetGroup 596 | Properties: 597 | DBSubnetGroupDescription: Sentry stack postgres subnet group 598 | SubnetIds: 599 | <% for @num in 1..@availability_zones -%> 600 | - Ref: PrivateSubnet<%= @num %> 601 | <% end -%> 602 | 603 | PostgresInstance: 604 | Type: AWS::RDS::DBInstance 605 | Properties: 606 | AllocatedStorage: 607 | Ref: DBAllocatedStorage 608 | BackupRetentionPeriod: 609 | Ref: DBBackupRetentionPeriod 610 | DBInstanceClass: 611 | Ref: DBInstanceClass 612 | DBName: 613 | Ref: DBName 614 | Engine: postgres 615 | KmsKeyId: 616 | Ref: SentryEncryptionKey 617 | MasterUsername: 618 | Ref: DBMasterUsername 619 | MasterUserPassword: 620 | Ref: DBMasterUserPassword 621 | MultiAZ: 622 | Ref: DBMultiAZ 623 | Port: '5432' 624 | PubliclyAccessible: 'false' 625 | StorageEncrypted: 'true' 626 | StorageType: 627 | Ref: DBStorageType 628 | VPCSecurityGroups: 629 | - Ref: PostgresSecurityGroup 630 | DBSubnetGroupName: 631 | Ref: PostgresSubnetGroup 632 | 633 | ###### File Storage ###### 634 | 635 | SentryFilesS3Bucket: 636 | Type: AWS::S3::Bucket 637 | Properties: 638 | BucketName: 639 | Fn::Join: 640 | - '' 641 | - - Ref: AWS::AccountId 642 | - "-" 643 | - Ref: AWS::StackName 644 | - "-sentry-files" 645 | AccessControl: Private 646 | 647 | ###### App Server ###### 648 | 649 | LoadBalancerSecurityGroup: 650 | Type: AWS::EC2::SecurityGroup 651 | Properties: 652 | <% if @visibility === "internet-facing" -%> 653 | SecurityGroupIngress: 654 | - ToPort: '443' 655 | IpProtocol: tcp 656 | FromPort: '443' 657 | CidrIp: 0.0.0.0/0 658 | - ToPort: '443' 659 | IpProtocol: tcp 660 | FromPort: '443' 661 | CidrIpv6: ::/0 662 | <% end -%> 663 | VpcId: 664 | Ref: VPC 665 | GroupDescription: An ELB group allowing access only to from the corresponding 666 | component 667 | Tags: 668 | - Key: Name 669 | Value: 670 | Fn::Join: 671 | - '-' 672 | - [{Ref: 'AWS::StackName'}, 'LoadBalancer'] 673 | 674 | SentryElasticLoadBalancer: 675 | Type: AWS::ElasticLoadBalancing::LoadBalancer 676 | Properties: 677 | Subnets: 678 | <% for @num in 1..@availability_zones -%> 679 | <% if @visibility === "internet-facing" -%> 680 | - Ref: PublicSubnet<%= @num %> 681 | <% else -%> 682 | - Ref: PrivateSubnet<%= @num %> 683 | <% end -%> 684 | <% end -%> 685 | Scheme: <%= @visibility %> 686 | Listeners: 687 | - InstancePort: '443' 688 | Protocol: HTTPS 689 | InstanceProtocol: HTTPS 690 | LoadBalancerPort: '443' 691 | SSLCertificateId: 692 | Ref: SSLCertARN 693 | PolicyNames: 694 | - SSLNegotiationPolicy 695 | Policies: 696 | - PolicyName : SSLNegotiationPolicy 697 | PolicyType: SSLNegotiationPolicyType 698 | Attributes: 699 | - Name: Protocol-TLSv1.2 700 | Value: 'true' 701 | - Name: Server-Defined-Cipher-Order 702 | Value: 'true' 703 | - Name: ECDHE-ECDSA-AES128-GCM-SHA256 704 | Value: 'true' 705 | - Name: ECDHE-RSA-AES128-GCM-SHA256 706 | Value: 'true' 707 | - Name: ECDHE-ECDSA-AES128-SHA256 708 | Value: 'true' 709 | - Name: ECDHE-RSA-AES128-SHA256 710 | Value: 'true' 711 | - Name: ECDHE-ECDSA-AES256-GCM-SHA384 712 | Value: 'true' 713 | - Name: ECDHE-RSA-AES256-GCM-SHA384 714 | Value: 'true' 715 | - Name: ECDHE-ECDSA-AES256-SHA384 716 | Value: 'true' 717 | - Name: ECDHE-RSA-AES256-SHA384 718 | Value: 'true' 719 | - Name: AES128-GCM-SHA256 720 | Value: 'true' 721 | - Name: AES128-SHA256 722 | Value: 'true' 723 | - Name: AES256-GCM-SHA384 724 | Value: 'true' 725 | - Name: AES256-SHA256 726 | Value: 'true' 727 | CrossZone: false 728 | SecurityGroups: 729 | - Ref: LoadBalancerSecurityGroup 730 | HealthCheck: 731 | HealthyThreshold: 3 732 | Interval: 10 733 | Timeout: 5 734 | UnhealthyThreshold: 10 735 | Target: HTTPS:443/_health/ 736 | 737 | SentryInstanceProfile: 738 | Type: AWS::IAM::InstanceProfile 739 | Properties: 740 | Path: "/" 741 | Roles: 742 | - Ref: SentryRole 743 | 744 | SentrySecurityGroup: 745 | Type: AWS::EC2::SecurityGroup 746 | Properties: 747 | SecurityGroupIngress: 748 | - ToPort: '443' 749 | IpProtocol: tcp 750 | FromPort: '443' 751 | SourceSecurityGroupId: 752 | Ref: LoadBalancerSecurityGroup 753 | VpcId: 754 | Ref: VPC 755 | GroupDescription: Sentry instance security group, gives access to from load balancer 756 | Tags: 757 | - Key: Name 758 | Value: 759 | Fn::Join: 760 | - '-' 761 | - [{Ref: 'AWS::StackName'}, 'Sentry'] 762 | 763 | EncryptedDeploymentHosts: 764 | Type: AWS::CloudFormation::CustomResource 765 | Version: "1.0" 766 | Properties: 767 | ServiceToken: !GetAtt EncryptionHelperFunction.Arn 768 | KeyId: 769 | Ref: SentryEncryptionKey 770 | PlainText: 771 | !Sub 772 | - | 773 | [aws] 774 | 127.0.0.1 775 | 776 | [aws:vars] 777 | user=ubuntu 778 | owner="${Owner}" 779 | sentry_admin_username="${SentryAdminUser}" 780 | sentry_admin_password="${SentryAdminPassword}" 781 | sentry_public_dns_name="${SentryPublicDnsName}" 782 | sentry_secret_key="${SentrySecretKey}" 783 | sentry_github_app_id="${SentryGithubAppId}" 784 | sentry_github_api_secret="${SentryGithubApiSecret}" 785 | sentry_url="${SentryUrl}" 786 | sentry_db_host="${DBHost}" 787 | sentry_db_port="5432" 788 | sentry_db_name="${DBName}" 789 | sentry_db_user="${DBMasterUsername}" 790 | sentry_db_password="${DBMasterUserPassword}" 791 | sentry_redis_host="${RedisHost}" 792 | sentry_redis_port="6379" 793 | sentry_mail_host="${SentryMailHost}" 794 | sentry_mail_port="${SentryMailPort}" 795 | sentry_mail_username="${SentryMailUsername}" 796 | sentry_mail_password="${SentryMailPassword}" 797 | sentry_mail_from="${SentryMailFrom}" 798 | sentry_files_bucket_name="${AWS::AccountId}-${AWS::StackName}-sentry-files" 799 | - SentryUrl: !GetAtt SentryElasticLoadBalancer.DNSName 800 | DBHost: !GetAtt PostgresInstance.Endpoint.Address 801 | RedisHost: !GetAtt RedisCacheCluster.RedisEndpoint.Address 802 | 803 | SentryLaunchConfiguration: 804 | Type: AWS::AutoScaling::LaunchConfiguration 805 | Properties: 806 | KeyName: 807 | Ref: KeyName 808 | ImageId: 809 | Ref: ImageId 810 | SecurityGroups: 811 | - Ref: SentrySecurityGroup 812 | - Ref: PostgresAccessSecurityGroup 813 | - Ref: RedisAccessSecurityGroup 814 | InstanceType: 815 | Ref: InstanceType 816 | IamInstanceProfile: 817 | Ref: SentryInstanceProfile 818 | UserData: 819 | Fn::Base64: 820 | !Sub 821 | - | 822 | #cloud-config 823 | runcmd: 824 | - apt-get update 825 | - apt-get install ansible awscli unzip -y 826 | - openssl req -new -nodes -x509 -subj "/C=GB/ST=London/L=London/O=Private/CN=${AWS::StackName}" -days 3650 -keyout /tmp/server.key -out /tmp/bundle.crt -extensions v3_ca 827 | - curl https://github.com/o2Labs/sentry-formation/archive/<%= @version %>.zip --output /tmp/repo.zip --location 828 | - unzip /tmp/repo.zip -d /tmp 829 | - echo "${DeploymentHosts}" > /tmp/hosts.base64 830 | - base64 -d /tmp/hosts.base64 > /tmp/hosts.encrypted 831 | - aws kms decrypt --region ${AWS::Region} --ciphertext-blob "fileb:///tmp/hosts.encrypted" --output text --query Plaintext > /tmp/hosts.decrypted 832 | - base64 -d /tmp/hosts.decrypted > /tmp/sentry-formation-<%= @version %>/hosts 833 | - ansible-playbook /tmp/sentry-formation-<%= @version %>/site.yml -i /tmp/sentry-formation-<%= @version %>/hosts 834 | - DeploymentHosts: !GetAtt EncryptedDeploymentHosts.CipherText 835 | 836 | SentryAutoScalingGroup: 837 | Type: AWS::AutoScaling::AutoScalingGroup 838 | UpdatePolicy: 839 | AutoScalingRollingUpdate: 840 | PauseTime: PT15M 841 | MaxBatchSize: 1 842 | MinInstancesInService: 1 843 | Properties: 844 | LoadBalancerNames: 845 | - Ref: SentryElasticLoadBalancer 846 | MinSize: 847 | Ref: ScalingMinNodes 848 | MaxSize: 849 | Ref: ScalingMaxNodes 850 | LaunchConfigurationName: 851 | Ref: SentryLaunchConfiguration 852 | Tags: 853 | - PropagateAtLaunch: true 854 | Key: Name 855 | Value: 856 | Ref: AWS::StackName 857 | VPCZoneIdentifier: 858 | <% for @num in 1..@availability_zones -%> 859 | - Ref: PrivateSubnet<%= @num %> 860 | <% end -%> 861 | 862 | SentryScalingPolicy: 863 | Type: AWS::AutoScaling::ScalingPolicy 864 | Properties: 865 | ScalingAdjustment: 1 866 | AutoScalingGroupName: 867 | Ref: SentryAutoScalingGroup 868 | AdjustmentType: ChangeInCapacity 869 | -------------------------------------------------------------------------------- /output/1.0.0-internal-2az.yaml: -------------------------------------------------------------------------------- 1 | # This file was generated with lono. Do not edit directly, the changes will be lost. 2 | # More info: https://github.com/tongueroo/lono 3 | Description: Sentry.io internal setup in 2 availability zones 4 | Parameters: 5 | #### Required #### 6 | Owner: 7 | Type: String 8 | Description: Name of the owner of the service (normally your company name) 9 | DBMasterUsername: 10 | Type: String 11 | MinLength: '1' 12 | MaxLength: '16' 13 | AllowedPattern: "[a-zA-Z][a-zA-Z0-9]*" 14 | Description: Username for database access 15 | DBMasterUserPassword: 16 | NoEcho: true 17 | Type: String 18 | Description: Password for database access - minimum 8 characters 19 | MinLength: '8' 20 | MaxLength: '41' 21 | AllowedPattern: "[a-zA-Z0-9]*" 22 | ConstraintDescription: must contain 8 alphanumeric characters. 23 | SSLCertARN: 24 | Description: The ARN of the ACM cert 25 | Type: String 26 | KeyName: 27 | Description: Name of existing EC2 keypair to enable SSH access to the created 28 | instances 29 | Type: AWS::EC2::KeyPair::KeyName 30 | SentryAdminUser: 31 | Type: String 32 | MinLength: '1' 33 | MaxLength: '30' 34 | Description: Username for root sentry access 35 | SentryAdminPassword: 36 | NoEcho: true 37 | Type: String 38 | Description: Password for root sentry access - minimum 20 characters 39 | MinLength: '20' 40 | SentryPublicDnsName: 41 | Type: String 42 | Description: Host name that users will type to get to sentry. 43 | SentrySecretKey: 44 | Type: String 45 | Description: Private key for encrypting user sessions. 46 | MinLength: '50' 47 | NoEcho: true 48 | SentryGithubAppId: 49 | Description: GitHub API App ID for SSO 50 | Type: String 51 | SentryGithubApiSecret: 52 | Description: GitHub API secret key for SSO 53 | Type: String 54 | NoEcho: true 55 | SentryMailUsername: 56 | Description: SMTP username for sentry to use to send email 57 | Type: String 58 | SentryMailPassword: 59 | Description: SMTP password for sentry to use to send email 60 | Type: String 61 | NoEcho: true 62 | SentryMailFrom: 63 | Description: Sending email address for sentry to use to send email 64 | Type: String 65 | #### Optional #### 66 | VpcCidr: 67 | AllowedPattern: '((\d{1,3})\.){3}\d{1,3}/\d{1,2}' 68 | Description: VPC Cidr block 69 | Default: 10.0.0.0/22 70 | Type: String 71 | VpcAvailabilityZone1: 72 | Description: The AvailabilityZone to use for subnet 1 73 | Type: AWS::EC2::AvailabilityZone::Name 74 | Default: eu-west-1a 75 | VpcAvailabilityZone2: 76 | Description: The AvailabilityZone to use for subnet 2 77 | Type: AWS::EC2::AvailabilityZone::Name 78 | Default: eu-west-1b 79 | VpcPublicSubnetCIDR1: 80 | AllowedPattern: '((\d{1,3})\.){3}\d{1,3}/\d{1,2}' 81 | Default: 10.0.0.0/26 82 | Description: VPC CIDR Block for Public Subnet 1 83 | Type: String 84 | VpcPublicSubnetCIDR2: 85 | AllowedPattern: '((\d{1,3})\.){3}\d{1,3}/\d{1,2}' 86 | Default: 10.0.0.64/26 87 | Description: VPC CIDR Block for Public Subnet 2 88 | Type: String 89 | VpcPrivateSubnetCIDR1: 90 | AllowedPattern: '((\d{1,3})\.){3}\d{1,3}/\d{1,2}' 91 | Default: 10.0.2.0/26 92 | Description: VPC CIDR Block for Private Subnet 1 93 | Type: String 94 | VpcPrivateSubnetCIDR2: 95 | AllowedPattern: '((\d{1,3})\.){3}\d{1,3}/\d{1,2}' 96 | Default: 10.0.2.64/26 97 | Description: VPC CIDR Block for Private Subnet 2 98 | Type: String 99 | ImageId: 100 | Description: The AMI to use for this Sentry - YUM compliant required 101 | Type: String 102 | Default: ami-6d48500b 103 | InstanceType: 104 | Description: The size of the EC2 instances 105 | Default: t2.medium 106 | Type: String 107 | DBName: 108 | Type: String 109 | Default: sentry 110 | MinLength: '1' 111 | MaxLength: '64' 112 | AllowedPattern: "[a-zA-Z][a-zA-Z0-9]*" 113 | ConstraintDescription: must begin with a letter and contain only alphanumeric 114 | characters. 115 | DBAllocatedStorage: 116 | Type: String 117 | Default: '5' 118 | DBBackupRetentionPeriod: 119 | Type: String 120 | Default: '7' 121 | DBInstanceClass: 122 | Type: String 123 | Default: db.t2.small 124 | DBMultiAZ: 125 | Type: String 126 | Default: true 127 | Description: Provides enhanced availablily for RDS 128 | DBStorageType: 129 | Type: String 130 | Default: gp2 131 | RedisEngineVersion: 132 | Type: String 133 | Description: Version of Redis engine to use. 134 | Default: '3.2.4' 135 | RedisNodeType: 136 | Type: String 137 | Default: cache.t2.small 138 | RedisNumNodes: 139 | Type: String 140 | Default: '1' 141 | SentryMailHost: 142 | Description: SMTP host for sentry to use to send email 143 | Type: String 144 | Default: email-smtp.eu-west-1.amazonaws.com 145 | SentryMailPort: 146 | Description: SMTP port for sentry to use to send email 147 | Type: String 148 | Default: '25' 149 | ScalingMinNodes: 150 | Type: Number 151 | Description: Minium size of the auto scaling group 152 | Default: 1 153 | ScalingMaxNodes: 154 | Type: Number 155 | Description: Maximum size of the auto scaling group 156 | Default: 2 157 | AWSTemplateFormatVersion: '2010-09-09' 158 | Resources: 159 | ###### Network ###### 160 | VPC: 161 | Type: 'AWS::EC2::VPC' 162 | Properties: 163 | CidrBlock: 164 | Ref: VpcCidr 165 | EnableDnsHostnames: true 166 | Tags: 167 | - Key: Name 168 | Value: 169 | Ref: AWS::StackName 170 | PublicSubnet1: 171 | Type: AWS::EC2::Subnet 172 | Properties: 173 | AvailabilityZone: {Ref: VpcAvailabilityZone1} 174 | CidrBlock: {Ref: VpcPublicSubnetCIDR1} 175 | MapPublicIpOnLaunch: true 176 | Tags: 177 | - Key: Name 178 | Value: 179 | Fn::Join: 180 | - '-' 181 | - [{Ref: 'AWS::StackName'}, 'public', {Ref: VpcAvailabilityZone1}] 182 | VpcId: {Ref: VPC} 183 | PublicSubnet2: 184 | Type: AWS::EC2::Subnet 185 | Properties: 186 | AvailabilityZone: {Ref: VpcAvailabilityZone2} 187 | CidrBlock: {Ref: VpcPublicSubnetCIDR2} 188 | MapPublicIpOnLaunch: true 189 | Tags: 190 | - Key: Name 191 | Value: 192 | Fn::Join: 193 | - '-' 194 | - [{Ref: 'AWS::StackName'}, 'public', {Ref: VpcAvailabilityZone2}] 195 | VpcId: {Ref: VPC} 196 | PrivateSubnet1: 197 | Type: AWS::EC2::Subnet 198 | Properties: 199 | AvailabilityZone: {Ref: VpcAvailabilityZone1} 200 | CidrBlock: {Ref: VpcPrivateSubnetCIDR1} 201 | MapPublicIpOnLaunch: false 202 | Tags: 203 | - Key: Name 204 | Value: 205 | Fn::Join: 206 | - '-' 207 | - [{Ref: 'AWS::StackName'}, 'private', {Ref: VpcAvailabilityZone1}] 208 | VpcId: {Ref: VPC} 209 | PrivateSubnet2: 210 | Type: AWS::EC2::Subnet 211 | Properties: 212 | AvailabilityZone: {Ref: VpcAvailabilityZone2} 213 | CidrBlock: {Ref: VpcPrivateSubnetCIDR2} 214 | MapPublicIpOnLaunch: false 215 | Tags: 216 | - Key: Name 217 | Value: 218 | Fn::Join: 219 | - '-' 220 | - [{Ref: 'AWS::StackName'}, 'private', {Ref: VpcAvailabilityZone2}] 221 | VpcId: {Ref: VPC} 222 | InternetGateway: 223 | Type: AWS::EC2::InternetGateway 224 | Properties: 225 | Tags: 226 | - Key: Name 227 | Value: 228 | Ref: AWS::StackName 229 | GatewayAttachment: 230 | Type: AWS::EC2::VPCGatewayAttachment 231 | Properties: 232 | InternetGatewayId: 233 | Ref: InternetGateway 234 | VpcId: 235 | Ref: VPC 236 | ###### Public Routing ###### 237 | PublicRouteTable: 238 | Type: AWS::EC2::RouteTable 239 | Properties: 240 | Tags: 241 | - Key: Name 242 | Value: 243 | Fn::Join: 244 | - '-' 245 | - [{Ref: 'AWS::StackName'}, 'public'] 246 | VpcId: 247 | Ref: VPC 248 | PublicRoute: 249 | Type: AWS::EC2::Route 250 | Properties: 251 | DestinationCidrBlock: 0.0.0.0/0 252 | GatewayId: 253 | Ref: InternetGateway 254 | RouteTableId: 255 | Ref: PublicRouteTable 256 | PublicSubnetAssoc1: 257 | Type: AWS::EC2::SubnetRouteTableAssociation 258 | Properties: 259 | RouteTableId: 260 | Ref: PublicRouteTable 261 | SubnetId: 262 | Ref: PublicSubnet1 263 | PublicSubnetAssoc2: 264 | Type: AWS::EC2::SubnetRouteTableAssociation 265 | Properties: 266 | RouteTableId: 267 | Ref: PublicRouteTable 268 | SubnetId: 269 | Ref: PublicSubnet2 270 | ###### Private Routing ###### 271 | PrivateRouteTable1: 272 | Type: AWS::EC2::RouteTable 273 | Properties: 274 | Tags: 275 | - Key: Name 276 | Value: 277 | Fn::Join: 278 | - '-' 279 | - [{Ref: 'AWS::StackName'}, 'private-1'] 280 | VpcId: 281 | Ref: VPC 282 | PrivateSubnetAssoc1: 283 | Type: AWS::EC2::SubnetRouteTableAssociation 284 | Properties: 285 | RouteTableId: 286 | Ref: PrivateRouteTable1 287 | SubnetId: 288 | Ref: PrivateSubnet1 289 | EIP1: 290 | Type: AWS::EC2::EIP 291 | Properties: 292 | Domain: vpc 293 | NAT1: 294 | DependsOn: GatewayAttachment 295 | Type: AWS::EC2::NatGateway 296 | Properties: 297 | AllocationId: 298 | Fn::GetAtt: 299 | - EIP1 300 | - AllocationId 301 | SubnetId: 302 | Ref: PublicSubnet1 303 | NATRoute1: 304 | Type: AWS::EC2::Route 305 | Properties: 306 | RouteTableId: 307 | Ref: PrivateRouteTable1 308 | DestinationCidrBlock: 0.0.0.0/0 309 | NatGatewayId: 310 | Ref: NAT1 311 | PrivateRouteTable2: 312 | Type: AWS::EC2::RouteTable 313 | Properties: 314 | Tags: 315 | - Key: Name 316 | Value: 317 | Fn::Join: 318 | - '-' 319 | - [{Ref: 'AWS::StackName'}, 'private-2'] 320 | VpcId: 321 | Ref: VPC 322 | PrivateSubnetAssoc2: 323 | Type: AWS::EC2::SubnetRouteTableAssociation 324 | Properties: 325 | RouteTableId: 326 | Ref: PrivateRouteTable2 327 | SubnetId: 328 | Ref: PrivateSubnet2 329 | EIP2: 330 | Type: AWS::EC2::EIP 331 | Properties: 332 | Domain: vpc 333 | NAT2: 334 | DependsOn: GatewayAttachment 335 | Type: AWS::EC2::NatGateway 336 | Properties: 337 | AllocationId: 338 | Fn::GetAtt: 339 | - EIP2 340 | - AllocationId 341 | SubnetId: 342 | Ref: PublicSubnet2 343 | NATRoute2: 344 | Type: AWS::EC2::Route 345 | Properties: 346 | RouteTableId: 347 | Ref: PrivateRouteTable2 348 | DestinationCidrBlock: 0.0.0.0/0 349 | NatGatewayId: 350 | Ref: NAT2 351 | ###### Roles ###### 352 | RootEncryptionRole: 353 | Type: AWS::IAM::Role 354 | Properties: 355 | AssumeRolePolicyDocument: 356 | Version: "2012-10-17" 357 | Statement: 358 | - Effect: "Allow" 359 | Principal: 360 | AWS: "*" 361 | Action: 362 | - "sts:AssumeRole" 363 | Path: "/" 364 | LambdaExecutionRole: 365 | Type: AWS::IAM::Role 366 | Properties: 367 | AssumeRolePolicyDocument: 368 | Version: '2012-10-17' 369 | Statement: 370 | - Effect: Allow 371 | Principal: 372 | Service: 373 | - lambda.amazonaws.com 374 | Action: 375 | - sts:AssumeRole 376 | Path: "/" 377 | Policies: 378 | - PolicyName: root 379 | PolicyDocument: 380 | Version: '2012-10-17' 381 | Statement: 382 | - Effect: Allow 383 | Action: 384 | - logs:* 385 | Resource: arn:aws:logs:*:*:* 386 | SentryRole: 387 | Type: AWS::IAM::Role 388 | Properties: 389 | AssumeRolePolicyDocument: 390 | Version: "2012-10-17" 391 | Statement: 392 | - Effect: Allow 393 | Principal: 394 | Service: 395 | - ec2.amazonaws.com 396 | Action: 397 | - sts:AssumeRole 398 | Path: "/" 399 | Policies: 400 | - PolicyName: root 401 | PolicyDocument: 402 | Version: "2012-10-17" 403 | Statement: 404 | - Effect: Allow 405 | Action: 406 | - s3:PutObject 407 | - s3:GetObject 408 | - s3:DeleteObject 409 | Resource: 410 | - Fn::Join: 411 | - '' 412 | - - !GetAtt SentryFilesS3Bucket.Arn 413 | - "/*" 414 | ###### Encryption ###### 415 | SentryEncryptionKey: 416 | Type: "AWS::KMS::Key" 417 | Properties: 418 | Description: Sentry environment root encryption key 419 | KeyPolicy: 420 | Version: "2012-10-17" 421 | Statement: 422 | - Sid: "Enable IAM User Permissions" 423 | Effect: "Allow" 424 | Principal: 425 | AWS: 426 | Fn::Join: 427 | - '' 428 | - ['arn:aws:iam::', {Ref: 'AWS::AccountId'}, ':root'] 429 | Action: "kms:*" 430 | Resource: "*" 431 | - Sid: "Allow administration of the key" 432 | Effect: "Allow" 433 | Principal: 434 | AWS: 435 | - !GetAtt RootEncryptionRole.Arn 436 | Action: 437 | - "kms:Create*" 438 | - "kms:Describe*" 439 | - "kms:Enable*" 440 | - "kms:List*" 441 | - "kms:Put*" 442 | - "kms:Update*" 443 | - "kms:Revoke*" 444 | - "kms:Disable*" 445 | - "kms:Get*" 446 | - "kms:Delete*" 447 | - "kms:TagResource" 448 | - "kms:UntagResource" 449 | - "kms:ScheduleKeyDeletion" 450 | - "kms:CancelKeyDeletion" 451 | Resource: "*" 452 | - Sid: "Allow use of the key" 453 | Effect: "Allow" 454 | Principal: 455 | AWS: 456 | - !GetAtt LambdaExecutionRole.Arn 457 | - !GetAtt SentryRole.Arn 458 | Action: 459 | - "kms:Encrypt" 460 | - "kms:Decrypt" 461 | - "kms:ReEncrypt*" 462 | - "kms:GenerateDataKey*" 463 | - "kms:DescribeKey" 464 | Resource: "*" 465 | SentryEncryptionKeyAlias: 466 | Type: AWS::KMS::Alias 467 | Properties: 468 | AliasName: 469 | Fn::Join: 470 | - '' 471 | - ['alias/', {Ref: 'AWS::StackName'}] 472 | TargetKeyId: 473 | Ref: SentryEncryptionKey 474 | EncryptionHelperFunction: 475 | Type: AWS::Lambda::Function 476 | Properties: 477 | Handler: index.lambda_handler 478 | Role: !GetAtt LambdaExecutionRole.Arn 479 | Code: 480 | ZipFile: !Sub | 481 | import base64 482 | import uuid 483 | import httplib 484 | import urlparse 485 | import json 486 | import boto3 487 | def send_response(request, response, status=None, reason=None): 488 | """ Send our response to the pre-signed URL supplied by CloudFormation 489 | If no ResponseURL is found in the request, there is no place to send a 490 | response. This may be the case if the supplied event was for testing. 491 | """ 492 | if status is not None: 493 | response['Status'] = status 494 | if reason is not None: 495 | response['Reason'] = reason 496 | if 'ResponseURL' in request and request['ResponseURL']: 497 | url = urlparse.urlparse(request['ResponseURL']) 498 | body = json.dumps(response) 499 | https = httplib.HTTPSConnection(url.hostname) 500 | https.request('PUT', url.path+'?'+url.query, body) 501 | return response 502 | def lambda_handler(event, context): 503 | response = { 504 | 'StackId': event['StackId'], 505 | 'RequestId': event['RequestId'], 506 | 'LogicalResourceId': event['LogicalResourceId'], 507 | 'Status': 'SUCCESS' 508 | } 509 | # PhysicalResourceId is meaningless here, but CloudFormation requires it 510 | if 'PhysicalResourceId' in event: 511 | response['PhysicalResourceId'] = event['PhysicalResourceId'] 512 | else: 513 | response['PhysicalResourceId'] = str(uuid.uuid4()) 514 | # There is nothing to do for a delete request 515 | if event['RequestType'] == 'Delete': 516 | return send_response(event, response) 517 | # Encrypt the value using AWS KMS and return the response 518 | try: 519 | for key in ['KeyId', 'PlainText']: 520 | if key not in event['ResourceProperties'] or not event['ResourceProperties'][key]: 521 | return send_response( 522 | event, response, status='FAILED', 523 | reason='The properties KeyId and PlainText must not be empty' 524 | ) 525 | client = boto3.client('kms') 526 | encrypted = client.encrypt( 527 | KeyId=event['ResourceProperties']['KeyId'], 528 | Plaintext=event['ResourceProperties']['PlainText'] 529 | ) 530 | response['Data'] = { 531 | 'CipherText': base64.b64encode(encrypted['CiphertextBlob']) 532 | } 533 | response['Reason'] = 'The value was successfully encrypted' 534 | except Exception as E: 535 | response['Status'] = 'FAILED' 536 | response['Reason'] = 'Encryption Failed - See CloudWatch logs for the Lamba function backing the custom resource for details' 537 | return send_response(event, response) 538 | Runtime: python2.7 539 | ###### Redis ###### 540 | RedisAccessSecurityGroup: 541 | Type: AWS::EC2::SecurityGroup 542 | Properties: 543 | VpcId: 544 | Ref: VPC 545 | GroupDescription: Allows access only to sentry redis cluster 546 | Tags: 547 | - Key: Name 548 | Value: 549 | Fn::Join: 550 | - '-' 551 | - [{Ref: 'AWS::StackName'}, 'RedisAccess'] 552 | RedisSecurityGroup: 553 | Type: AWS::EC2::SecurityGroup 554 | Properties: 555 | GroupDescription: Senty redis cluster source 556 | SecurityGroupIngress: 557 | - IpProtocol: tcp 558 | FromPort: '6379' 559 | ToPort: '6379' 560 | SourceSecurityGroupId: 561 | Ref: RedisAccessSecurityGroup 562 | VpcId: 563 | Ref: VPC 564 | Tags: 565 | - Key: Name 566 | Value: 567 | Fn::Join: 568 | - '-' 569 | - [{Ref: 'AWS::StackName'}, 'Redis'] 570 | RedisSubnetGroup: 571 | Type: AWS::ElastiCache::SubnetGroup 572 | Properties: 573 | Description: Sentry stack redis subnet group 574 | SubnetIds: 575 | - Ref: PrivateSubnet1 576 | - Ref: PrivateSubnet2 577 | RedisCacheCluster: 578 | Type: AWS::ElastiCache::CacheCluster 579 | Properties: 580 | CacheNodeType: 581 | Ref: RedisNodeType 582 | CacheSubnetGroupName: 583 | Ref: RedisSubnetGroup 584 | Engine: redis 585 | EngineVersion: 586 | Ref: RedisEngineVersion 587 | NumCacheNodes: 588 | Ref: RedisNumNodes 589 | VpcSecurityGroupIds: 590 | - Ref: RedisSecurityGroup 591 | ###### Postgres ###### 592 | PostgresAccessSecurityGroup: 593 | Type: AWS::EC2::SecurityGroup 594 | Properties: 595 | VpcId: 596 | Ref: VPC 597 | GroupDescription: Allows access only to sentry postgres instance 598 | Tags: 599 | - Key: Name 600 | Value: 601 | Fn::Join: 602 | - '-' 603 | - [{Ref: 'AWS::StackName'}, 'PostgresAccess'] 604 | PostgresSecurityGroup: 605 | Type: AWS::EC2::SecurityGroup 606 | Properties: 607 | SecurityGroupIngress: 608 | - ToPort: '5432' 609 | IpProtocol: tcp 610 | FromPort: '5432' 611 | SourceSecurityGroupId: 612 | Ref: PostgresAccessSecurityGroup 613 | VpcId: 614 | Ref: VPC 615 | GroupDescription: Senty postgres instance source 616 | Tags: 617 | - Key: Name 618 | Value: 619 | Fn::Join: 620 | - '-' 621 | - [{Ref: 'AWS::StackName'}, 'Postgres'] 622 | PostgresSubnetGroup: 623 | Type: AWS::RDS::DBSubnetGroup 624 | Properties: 625 | DBSubnetGroupDescription: Sentry stack postgres subnet group 626 | SubnetIds: 627 | - Ref: PrivateSubnet1 628 | - Ref: PrivateSubnet2 629 | PostgresInstance: 630 | Type: AWS::RDS::DBInstance 631 | Properties: 632 | AllocatedStorage: 633 | Ref: DBAllocatedStorage 634 | BackupRetentionPeriod: 635 | Ref: DBBackupRetentionPeriod 636 | DBInstanceClass: 637 | Ref: DBInstanceClass 638 | DBName: 639 | Ref: DBName 640 | Engine: postgres 641 | KmsKeyId: 642 | Ref: SentryEncryptionKey 643 | MasterUsername: 644 | Ref: DBMasterUsername 645 | MasterUserPassword: 646 | Ref: DBMasterUserPassword 647 | MultiAZ: 648 | Ref: DBMultiAZ 649 | Port: '5432' 650 | PubliclyAccessible: 'false' 651 | StorageEncrypted: 'true' 652 | StorageType: 653 | Ref: DBStorageType 654 | VPCSecurityGroups: 655 | - Ref: PostgresSecurityGroup 656 | DBSubnetGroupName: 657 | Ref: PostgresSubnetGroup 658 | ###### File Storage ###### 659 | SentryFilesS3Bucket: 660 | Type: AWS::S3::Bucket 661 | Properties: 662 | BucketName: 663 | Fn::Join: 664 | - '' 665 | - - Ref: AWS::AccountId 666 | - "-" 667 | - Ref: AWS::StackName 668 | - "-sentry-files" 669 | AccessControl: Private 670 | ###### App Server ###### 671 | LoadBalancerSecurityGroup: 672 | Type: AWS::EC2::SecurityGroup 673 | Properties: 674 | VpcId: 675 | Ref: VPC 676 | GroupDescription: An ELB group allowing access only to from the corresponding 677 | component 678 | Tags: 679 | - Key: Name 680 | Value: 681 | Fn::Join: 682 | - '-' 683 | - [{Ref: 'AWS::StackName'}, 'LoadBalancer'] 684 | SentryElasticLoadBalancer: 685 | Type: AWS::ElasticLoadBalancing::LoadBalancer 686 | Properties: 687 | Subnets: 688 | - Ref: PrivateSubnet1 689 | - Ref: PrivateSubnet2 690 | Scheme: internal 691 | Listeners: 692 | - InstancePort: '443' 693 | Protocol: HTTPS 694 | InstanceProtocol: HTTPS 695 | LoadBalancerPort: '443' 696 | SSLCertificateId: 697 | Ref: SSLCertARN 698 | PolicyNames: 699 | - SSLNegotiationPolicy 700 | Policies: 701 | - PolicyName : SSLNegotiationPolicy 702 | PolicyType: SSLNegotiationPolicyType 703 | Attributes: 704 | - Name: Protocol-TLSv1.2 705 | Value: 'true' 706 | - Name: Server-Defined-Cipher-Order 707 | Value: 'true' 708 | - Name: ECDHE-ECDSA-AES128-GCM-SHA256 709 | Value: 'true' 710 | - Name: ECDHE-RSA-AES128-GCM-SHA256 711 | Value: 'true' 712 | - Name: ECDHE-ECDSA-AES128-SHA256 713 | Value: 'true' 714 | - Name: ECDHE-RSA-AES128-SHA256 715 | Value: 'true' 716 | - Name: ECDHE-ECDSA-AES256-GCM-SHA384 717 | Value: 'true' 718 | - Name: ECDHE-RSA-AES256-GCM-SHA384 719 | Value: 'true' 720 | - Name: ECDHE-ECDSA-AES256-SHA384 721 | Value: 'true' 722 | - Name: ECDHE-RSA-AES256-SHA384 723 | Value: 'true' 724 | - Name: AES128-GCM-SHA256 725 | Value: 'true' 726 | - Name: AES128-SHA256 727 | Value: 'true' 728 | - Name: AES256-GCM-SHA384 729 | Value: 'true' 730 | - Name: AES256-SHA256 731 | Value: 'true' 732 | CrossZone: false 733 | SecurityGroups: 734 | - Ref: LoadBalancerSecurityGroup 735 | HealthCheck: 736 | HealthyThreshold: 3 737 | Interval: 10 738 | Timeout: 5 739 | UnhealthyThreshold: 10 740 | Target: HTTPS:443/_health/ 741 | SentryInstanceProfile: 742 | Type: AWS::IAM::InstanceProfile 743 | Properties: 744 | Path: "/" 745 | Roles: 746 | - Ref: SentryRole 747 | SentrySecurityGroup: 748 | Type: AWS::EC2::SecurityGroup 749 | Properties: 750 | SecurityGroupIngress: 751 | - ToPort: '443' 752 | IpProtocol: tcp 753 | FromPort: '443' 754 | SourceSecurityGroupId: 755 | Ref: LoadBalancerSecurityGroup 756 | VpcId: 757 | Ref: VPC 758 | GroupDescription: Sentry instance security group, gives access to from load balancer 759 | Tags: 760 | - Key: Name 761 | Value: 762 | Fn::Join: 763 | - '-' 764 | - [{Ref: 'AWS::StackName'}, 'Sentry'] 765 | EncryptedDeploymentHosts: 766 | Type: AWS::CloudFormation::CustomResource 767 | Version: "1.0" 768 | Properties: 769 | ServiceToken: !GetAtt EncryptionHelperFunction.Arn 770 | KeyId: 771 | Ref: SentryEncryptionKey 772 | PlainText: 773 | !Sub 774 | - | 775 | [aws] 776 | 127.0.0.1 777 | [aws:vars] 778 | user=ubuntu 779 | owner="${Owner}" 780 | sentry_admin_username="${SentryAdminUser}" 781 | sentry_admin_password="${SentryAdminPassword}" 782 | sentry_public_dns_name="${SentryPublicDnsName}" 783 | sentry_secret_key="${SentrySecretKey}" 784 | sentry_github_app_id="${SentryGithubAppId}" 785 | sentry_github_api_secret="${SentryGithubApiSecret}" 786 | sentry_url="${SentryUrl}" 787 | sentry_db_host="${DBHost}" 788 | sentry_db_port="5432" 789 | sentry_db_name="${DBName}" 790 | sentry_db_user="${DBMasterUsername}" 791 | sentry_db_password="${DBMasterUserPassword}" 792 | sentry_redis_host="${RedisHost}" 793 | sentry_redis_port="6379" 794 | sentry_mail_host="${SentryMailHost}" 795 | sentry_mail_port="${SentryMailPort}" 796 | sentry_mail_username="${SentryMailUsername}" 797 | sentry_mail_password="${SentryMailPassword}" 798 | sentry_mail_from="${SentryMailFrom}" 799 | sentry_files_bucket_name="${AWS::AccountId}-${AWS::StackName}-sentry-files" 800 | - SentryUrl: !GetAtt SentryElasticLoadBalancer.DNSName 801 | DBHost: !GetAtt PostgresInstance.Endpoint.Address 802 | RedisHost: !GetAtt RedisCacheCluster.RedisEndpoint.Address 803 | SentryLaunchConfiguration: 804 | Type: AWS::AutoScaling::LaunchConfiguration 805 | Properties: 806 | KeyName: 807 | Ref: KeyName 808 | ImageId: 809 | Ref: ImageId 810 | SecurityGroups: 811 | - Ref: SentrySecurityGroup 812 | - Ref: PostgresAccessSecurityGroup 813 | - Ref: RedisAccessSecurityGroup 814 | InstanceType: 815 | Ref: InstanceType 816 | IamInstanceProfile: 817 | Ref: SentryInstanceProfile 818 | UserData: 819 | Fn::Base64: 820 | !Sub 821 | - | 822 | #cloud-config 823 | runcmd: 824 | - apt-get update 825 | - apt-get install ansible awscli unzip -y 826 | - openssl req -new -nodes -x509 -subj "/C=GB/ST=London/L=London/O=Private/CN=${AWS::StackName}" -days 3650 -keyout /tmp/server.key -out /tmp/bundle.crt -extensions v3_ca 827 | - curl https://github.com/o2Labs/sentry-formation/archive/1.0.0.zip --output /tmp/repo.zip --location 828 | - unzip /tmp/repo.zip -d /tmp 829 | - echo "${DeploymentHosts}" > /tmp/hosts.base64 830 | - base64 -d /tmp/hosts.base64 > /tmp/hosts.encrypted 831 | - aws kms decrypt --region ${AWS::Region} --ciphertext-blob "fileb:///tmp/hosts.encrypted" --output text --query Plaintext > /tmp/hosts.decrypted 832 | - base64 -d /tmp/hosts.decrypted > /tmp/sentry-formation-1.0.0/hosts 833 | - ansible-playbook /tmp/sentry-formation-1.0.0/site.yml -i /tmp/sentry-formation-1.0.0/hosts 834 | - DeploymentHosts: !GetAtt EncryptedDeploymentHosts.CipherText 835 | SentryAutoScalingGroup: 836 | Type: AWS::AutoScaling::AutoScalingGroup 837 | UpdatePolicy: 838 | AutoScalingRollingUpdate: 839 | PauseTime: PT15M 840 | MaxBatchSize: 1 841 | MinInstancesInService: 1 842 | Properties: 843 | LoadBalancerNames: 844 | - Ref: SentryElasticLoadBalancer 845 | MinSize: 846 | Ref: ScalingMinNodes 847 | MaxSize: 848 | Ref: ScalingMaxNodes 849 | LaunchConfigurationName: 850 | Ref: SentryLaunchConfiguration 851 | Tags: 852 | - PropagateAtLaunch: true 853 | Key: Name 854 | Value: 855 | Ref: AWS::StackName 856 | VPCZoneIdentifier: 857 | - Ref: PrivateSubnet1 858 | - Ref: PrivateSubnet2 859 | SentryScalingPolicy: 860 | Type: AWS::AutoScaling::ScalingPolicy 861 | Properties: 862 | ScalingAdjustment: 1 863 | AutoScalingGroupName: 864 | Ref: SentryAutoScalingGroup 865 | AdjustmentType: ChangeInCapacity 866 | --------------------------------------------------------------------------------