From 9cb0c6f6768a047b0ee3e39776a2087ab3a0bf98 2023-05-02 06:35:51 From: Ben Sturmfels Date: 2023-05-02 06:35:51 Subject: [PATCH] Add deployment scripts --- diff --git a/deploy/basics.yml b/deploy/basics.yml new file mode 100644 index 0000000000000000000000000000000000000000..5d80a067c8311d0b37e18e3ac7daa58c1dc8ceb2 --- /dev/null +++ b/deploy/basics.yml @@ -0,0 +1,147 @@ +# Basic Ansible playbook to set up security essentials: Nginx dhparams, fail2ban, +# unattended-upgrades, history logging, firewall, no SSH keys and Postfix +# relay/rewriting/aliases. +# +# Run with: +# ANSIBLE_STDOUT_CALLBACK=debug ansible-playbook deploy/basics.yml -i deploy/inventory.yml --verbose +- hosts: web + become: true + vars: + ansible_ssh_pipelining: true + tasks: + - name: Generate dhparams file for HTTP2 + ansible.builtin.command: + cmd: openssl dhparam -out /etc/nginx/dhparam.pem 2048 + creates: /etc/nginx/dhparam.pem + - name: Install fail2ban + apt: + pkg: fail2ban + + - name: Install unattended-upgrades + apt: + pkg: unattended-upgrades + + - name: Configure unattended upgrades overrides + # See defaults in 50unattended-upgrades. + copy: + dest: /etc/apt/apt.conf.d/20auto-upgrades + content: | + APT::Periodic::Update-Package-Lists "1"; + APT::Periodic::Unattended-Upgrade "1"; + Unattended-Upgrade::Automatic-Reboot "true"; + Unattended-Upgrade::Automatic-Reboot-Time "02:00"; + Unattended-Upgrade::Mail "root"; + + - name: Add extensive history logging + blockinfile: + path: /etc/bash.bashrc + block: | + # Write to history file immediately (rather than only when shell is + # closed). For setting history length see HISTSIZE and HISTFILESIZE in + # bash(1). + shopt -s histappend + PROMPT_COMMAND='history -a' + HISTSIZE=1000000 + HISTFILESIZE=1000000 + insertafter: EOF + + - name: Install `netfilter-persistent` && `iptables-persistent` packages + apt: + pkg: + - iptables-persistent + - netfilter-persistent + + - name: Flush existing firewall rules + iptables: + flush: true + + - name: Firewall rule - allow all loopback traffic + iptables: + action: append + chain: INPUT + in_interface: lo + jump: ACCEPT + + - name: Firewall rule - allow established connections + iptables: + chain: INPUT + ctstate: ESTABLISHED,RELATED + jump: ACCEPT + + - name: Firewall rule - allow port ping traffic + iptables: + chain: INPUT + jump: ACCEPT + protocol: icmp + + - name: Firewall rule - allow port 22/SSH traffic + iptables: + chain: INPUT + destination_port: '22' + jump: ACCEPT + protocol: tcp + + - name: Firewall rule - allow port 80/HTTP traffic + iptables: + chain: INPUT + destination_port: '80' + jump: ACCEPT + protocol: tcp + + - name: Firewall rule - allow port 443/HTTPS traffic + iptables: + chain: INPUT + destination_port: '443' + jump: ACCEPT + protocol: tcp + + - name: Firewall rule - drop any traffic without rule + iptables: + chain: INPUT + jump: DROP + + - name: Disable SSH password authentication + lineinfile: + path: /etc/ssh/sshd_config + line: 'PasswordAuthentication no' + regexp: 'PasswordAuthentication ' + + + # Postfix + - name: Postfix + apt: + pkg: + - postfix + - mailutils + + ## Commented because you only want this on first run ever. + # - name: Add file for SMTP credentials + # copy: + # dest: /etc/postfix/sasl_passwd + # content: |- + # # After updating, run `sudo postmap hash:/etc/postfix/sasl_passwd`. + # [pine.sfconservancy.org]:587 conference@sfconservancy.org:PASSWORD + + - name: Configure Postfix envelope rewriting + copy: + dest: /etc/postfix/canonical + content: |- + /./ conference@sfconservancy.org + + - name: Configure Postfix From header rewriting + copy: + dest: /etc/postfix/header_checks + content: |- + /^From:.*/ REPLACE From: conference@sfconservancy.org + + - name: Configure Postfix for relaying + copy: + src: postfix/main.cf + dest: /etc/postfix/main.cf + + - name: Alias mail to root + copy: + dest: /etc/aliases + content: |- + postmaster: root + root: sysadmin@sfconservancy.org, sysadmin@sturm.com.au diff --git a/deploy/postfix/main.cf b/deploy/postfix/main.cf new file mode 100644 index 0000000000000000000000000000000000000000..a7ff7537da191f80168c2e1c64d54daa2857d3d8 --- /dev/null +++ b/deploy/postfix/main.cf @@ -0,0 +1,66 @@ +# See /usr/share/postfix/main.cf.dist for a commented, more complete version + + +# Debian specific: Specifying a file name will cause the first +# line of that file to be used as the name. The Debian default +# is /etc/mailname. +#myorigin = /etc/mailname + +smtpd_banner = $myhostname ESMTP $mail_name (Debian/GNU) +biff = no + +# appending .domain is the MUA's job. +append_dot_mydomain = no + +# Uncomment the next line to generate "delayed mail" warnings +#delay_warning_time = 4h + +readme_directory = no + +# See http://www.postfix.org/COMPATIBILITY_README.html -- default to 2 on +# fresh installs. +compatibility_level = 2 + +# TLS parameters +smtpd_tls_cert_file=/etc/ssl/certs/ssl-cert-snakeoil.pem +smtpd_tls_key_file=/etc/ssl/private/ssl-cert-snakeoil.key +smtpd_use_tls=yes +smtpd_tls_session_cache_database = btree:${data_directory}/smtpd_scache +smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache + +# See /usr/share/doc/postfix/TLS_README.gz in the postfix-doc package for +# information on enabling SSL in the smtp client. + +smtpd_relay_restrictions = permit_mynetworks permit_sasl_authenticated defer_unauth_destination +myhostname = symposion.sfconservancy.org +alias_maps = hash:/etc/aliases +alias_database = hash:/etc/aliases +myorigin = /etc/mailname +mydestination = $myhostname, symposion.novalocal, symposion, localhost +relayhost = [pine.sfconservancy.org]:587 +mynetworks = 127.0.0.0/8 [::ffff:127.0.0.0]/104 [::1]/128 +mailbox_size_limit = 0 +recipient_delimiter = + +inet_interfaces = loopback-only +inet_protocols = all + +# Relay to Conservancy +# +smtp_sasl_auth_enable = yes +smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd +smtp_sasl_security_options = noanonymous +smtp_tls_security_level = secure +smtp_tls_CAfile = /etc/ssl/certs/ca-certificates.crt + +# Increase default limit of 10M to 50M +message_size_limit = 51200000 + +# This configuration rewrites both envelope sender and From header to a single +# fixed address so that all outgoing mail, including cron emails, can be +# delivered through a single user-grade SMTP account. +# +# Envelope +canonical_maps = regexp:/etc/postfix/canonical +canonical_classes = envelope_sender +# From header +smtp_header_checks = regexp:/etc/postfix/header_checks \ No newline at end of file diff --git a/fabfile.py b/fabfile.py new file mode 100644 index 0000000000000000000000000000000000000000..16c9912f28a9db5b138c18cc2580133d4f5ed0f8 --- /dev/null +++ b/fabfile.py @@ -0,0 +1,92 @@ +"""Automated deployment with Fabric. + +To deploy to production: + + python3 -m pip install --user vps-deploy + export FABRIC_HOSTS=user@hostname + fab deploy +""" + +import os + +from fabric import task # type: ignore +from invoke.collection import Collection # type: ignore +from patchwork.files import exists +from vps_deploy import django_fabric2 as df2 # type: ignore + +hosts = os.environ['FABRIC_HOSTS'].split(',') + + +def install_essentials(c): + # ImageMagick (convert) and Inkscape required for generating badges. + c.run('sudo apt-get install -yy git python3-dev python3-venv python3-wheel build-essential python3-cairocffi python3-psycopg2 postgresql uwsgi-emperor uwsgi-plugin-python3 memcached netcat nginx certbot libmemcached-dev xmlsec1 imagemagick inkscape') + + +@task(hosts=hosts) +def deploy(c): + install_essentials(c) + df2.transfer_files_git(c) + df2.init(c) + if not exists(c, c.env.virtualenv): + c.sudo(f'mkdir -p $(dirname {c.env.virtualenv})') + c.sudo(f'chown {c.user} $(dirname {c.env.virtualenv})') + c.run('{env.python} -m venv --system-site-packages {env.virtualenv}'.format(env=c.env)) + with c.cd(c.env.project_dir): + c.run('{env.virtualenv}/bin/python -m pip install -c constraints.txt -r requirements.txt'.format(env=c.env)) + # Fixes "AttributeError: install_layout". See: + # https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1003252 + # https://github.com/pypa/setuptools/issues/3278 + c.run('SETUPTOOLS_USE_DISTUTILS=stdlib {env.virtualenv}/bin/python -m pip install -c constraints.txt -r vendored_requirements.txt'.format(env=c.env)) + df2.prepare_django(c, fail_level='ERROR') + df2.fix_permissions( + c, + read=['pinaxcon', 'vendor', 'static'], + read_write=[], + ) + df2.reload_uwsgi(c) + df2.flush_memcached(c) + df2.update_nginx(c) + df2.install_scheduled_jobs( + c, + periodic_jobs=[ + 'deploy/cron/cron.daily', + ], + ) + df2.check_site_online(c) + + +# The "ns" appears to be a magic name. +ns = Collection( + deploy, + task(df2.download_postgres_db, hosts=hosts), + task(df2.mirror_postgres_db, hosts=hosts), + task(df2.mirror_media, hosts=hosts), + task(df2.django_shell, hosts=hosts), + task(df2.bash, hosts=hosts), +) +ns.configure({ + # Built-in Fabric config. + 'run': { + 'echo': True, + # Needed so local commands work. Can also use FABRIC_RUN_REPLACE_ENV. + 'replace_env': False, + }, + + # Our custom project config. + 'env': { + 'branch': 'fossy2023', + 'app_user': 'www-data', + 'db_name': 'symposion', + 'project_dir': '/srv/symposion_app', + 'media_dir': 'media', + 'virtualenv': '/srv/venvs/symposion-django-py39', + 'site_name': 'symposion', + 'requirements': 'requirements.txt', + 'settings': 'pinaxcon.settings', + 'uwsgi_conf': 'deploy/uwsgi.ini', + 'nginx_conf': 'deploy/nginx.conf', + 'python': '/usr/bin/python3.9', + 'url': 'https://2023.fossy.us/', + 'domain': '2023.fossy.us', + }, +})