Go to production

This commit is contained in:
Julien Palard 2023-12-20 12:27:36 +01:00
parent 0c962473ad
commit b6e38107c2
Signed by: mdk
GPG Key ID: 0EFC1AC1006886F8
34 changed files with 757 additions and 0 deletions

3
deploy/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
*.retry
.venv/
Pipfile.lock

5
deploy/README.md Normal file
View File

@ -0,0 +1,5 @@
# Run ansble
```
ansible-playbook website.yml
```

4
deploy/ansible.cfg Normal file
View File

@ -0,0 +1,4 @@
[defaults]
inventory = inventory
nocows = 1
vault_password_file = ~/.ansible-hkis-vault

View File

@ -0,0 +1,11 @@
---
ansible_python_interpreter: "/usr/bin/python3"
ansible_user: root
gandi_api_key: !vault |
$ANSIBLE_VAULT;1.1;AES256
65306331316361633033373833316433313632336336653030623733363936643363393236393637
6635326137663133333038336665613566373365356364610a386437646238636336343965323730
61306638653939653437386530386663313338396266666136396637653061313036643730613335
6538613861643632310a386561343237303137633066343130333764353263663364326161653638
62313561333737313864303335626264636562626536613465326162323164666262

View File

@ -0,0 +1,17 @@
$ANSIBLE_VAULT;1.1;AES256
65356533653563626232346137616263306533353638653434623434373466373639316433653261
6333616531306439383331313035306563656366303363610a646432353865323836396533343966
37383835633362616466646138666335663437393463346535656136373266313161336464316462
3335376432633766380a666437356434656564636536313162663630643034373338653366313334
32346562386136353531373361346139363733626261383031633762326137383031326136646563
63313939356236626138366538616361636230633530303633646562366237626462383736373262
37636632393839636531333237356537646465613962353835636262653531333063386338366139
63663563393134663064623461326537663935323832383730313863613130396633323533366566
66313465383931393965386261653233333039383363333364613163393637306134393762396262
37666164313661633866643037656466623136646161366531323433386639633334333236313337
62343835303461636330326531333564376630633339343030336163643566363930383531663861
32353961646336326238636236303539626661303864626135626638613865373738373131316365
66313666393331646235643664633061653162633962303664336263643466316632303738303833
36616439343238383463396139626134336131363666313164373033333964626630386463653134
34623263666433326530376665313962373531383966646534336336363136336463633037633065
35663537343339393866

2
deploy/inventory Normal file
View File

@ -0,0 +1,2 @@
[website]
eqy.fr

1
deploy/requirements.txt Normal file
View File

@ -0,0 +1 @@
ansible

View File

@ -0,0 +1,7 @@
---
- name: sshd
service: name=sshd state=reloaded
- name: nftables
service: name=nftables state=reloaded

View File

@ -0,0 +1,93 @@
---
- name: Configure hostname
hostname:
name: "{{ inventory_hostname_short }}"
- name: Configure FQDN
lineinfile:
path: /etc/hosts
regexp: '^127\.0\.0\.1'
line: "127.0.0.1 {{ inventory_hostname }} {{ inventory_hostname_short }} localhost"
owner: root
group: root
mode: 0644
- name: apt-get some packages
apt:
state: present
name:
- aptitude
- fail2ban
- nftables
- python3
- python3-dev
- python3-pip
- python3-venv
- rsync
- name: nftable service is started
service:
name: nftables
enabled: yes
state: started
daemon_reload: yes
- name: Copy nftables config
copy:
content: |
#!/usr/sbin/nft -f
table inet filter
flush table inet filter
table inet filter {
chain input {
type filter hook input priority 0;
iif lo accept
ct state established,related accept
tcp dport { ssh, http, https } ct state new accept
# accept neighbour discovery otherwise connectivity breaks:
icmpv6 type { echo-request, nd-neighbor-solicit, nd-router-advert, nd-neighbor-advert } accept
ip protocol icmp icmp type { destination-unreachable, echo-reply, echo-request, source-quench, time-exceeded } accept
counter drop
}
}
dest: /etc/nftables.conf
owner: root
group: root
mode: 0755
notify: nftables
- name: Set some authorized keys
authorized_key: user=root key="{{item}}"
with_items:
- "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDKA7DgTQ0G7+kdsX0lIUOAAOllwGSCu8s8TxPvr/61Y8q+pIO5mrZycI0xYcKP5NZaABqlFyXUUNfLj7RLqteBxqq2QZP4NOJ1MutYRIkzJ9YW0f565jHaOqSguz0MY+1sCHtuEPiUUZoNexkKN7SIx60SfoaMEvGjAj46txA7VFbJUuKcJtA1Yvmn0C0KoXUUQ/G+JqvjQ7QuKLQYdTZ8S9OEvNaqNfwNSwvy1/LCnuajFw0O+H5bz7AcS5Iuj+9k8wgHPK1a1rQEdteOcn2XBCvta/VOVlFLv6/9K3iU3EJ1pyaZ88UkuJef8aWnH/AJGaF2gLqUbBuL+UeXyD41 julien+yubikey4@palard.fr"
- "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC8vv8vwmbyhFEa0chj8LklnnY6DRLKj2OM0NgaMTd9SsrtBeLMqTt34pU+kKl6/9EIe9P8Z1/fWFyOiTsE7Khf3rkNsoILPmEV14i18Bvtp4nMtljqZaKVkAcRjPvo7flRWNxxL2Zbo+BEr3wVCl3Sc6YV8oQzCwVPKf34AB39b+PW4f3580Aqcd4Ci6zca0Ol95tLDv1slX1A7QcpoZAne8kj5h6bb4cC7FLBC9+xOSKmzoLOlP7LsyxaUUGRyi/FeMoma1VES65aIJ5U23GtZrzZI3tKz+vpQvOVaozNTDkNLiiJkjd3Ew1I10wArpZixjwSndP8CvGFyJc1XUXZ julien+yubikey5@palard.fr"
- name: Drop mlocate or locate
apt:
name: ["mlocate", "locate"]
state: absent
# From https://infosec.mozilla.org/guidelines/openssh
- name: SSHd hardening
blockinfile:
marker: "# {mark} ANSIBLE MANAGED BLOCK (KexAlgorithms, Ciphers, MACs)"
path: /etc/ssh/sshd_config
state: present
create: yes
block: |
KexAlgorithms curve25519-sha256@libssh.org,ecdh-sha2-nistp521,ecdh-sha2-nistp384,ecdh-sha2-nistp256,diffie-hellman-group-exchange-sha256
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr
MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,umac-128-etm@openssh.com,hmac-sha2-512,hmac-sha2-256,umac-128@openssh.com
HostKey /etc/ssh/ssh_host_ed25519_key
HostKey /etc/ssh/ssh_host_rsa_key
HostKey /etc/ssh/ssh_host_ecdsa_key
AuthenticationMethods publickey
LogLevel VERBOSE
notify: sshd
tags: ssh

View File

@ -0,0 +1,7 @@
---
- include_role:
name: exim4
- include_tasks: common.yml
tags: common

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2016 Tobias Schifftner, ambimax® GmbH
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,23 @@
---
exim4_sendonly_fqdn: '{{ ansible_fqdn }}'
exim4_aws_access_key_id: ''
exim4_aws_secret_access_key: ''
exim4_aws_ses_region: 'eu-west-1'
exim4_sendonly_enable_tls: true
exim4_sendonly_smarthost: ''
exim4_sendonly_username: ''
exim4_sendonly_password: ''
exim4_sendonly_email_addresses: []
# root: 'your@email.com'
exim4_sendonly_email_aliases: []
# - regexp: '^root:'
# line: 'root: your@email.com'
exim4_sendonly_apt_packages:
- exim4-daemon-light
- mailutils

View File

@ -0,0 +1,14 @@
/var/mail/* {
su root mail
daily
missingok
rotate 8
size 5M
maxage 30
notifempty
compress
delaycompress
notifempty
create 0640 root root
sharedscripts
}

View File

@ -0,0 +1,6 @@
---
- name: restart exim4
service:
name: 'exim4'
state: restarted
enabled: yes

View File

@ -0,0 +1,50 @@
---
- name: Configure exim4
notify: restart exim4
template:
src: 'update-exim4.conf.conf'
dest: /etc/exim4/update-exim4.conf.conf
- name: Update mailname
notify: restart exim4
copy:
content: '{{ exim4_sendonly_fqdn }}'
dest: '/etc/mailname'
- name: Define email aliases
notify: restart exim4
lineinfile:
dest: /etc/aliases
regexp: "{{ item.regexp }}"
line: "{{ item.line }}"
with_items: '{{ exim4_sendonly_email_aliases }}'
when: exim4_sendonly_email_aliases|length
- name: Define email addresses
notify: restart exim4
template:
src: 'email-addresses.j2'
dest: '/etc/email-addresses'
when: exim4_sendonly_email_addresses|length
- name: Set auth for relay host
notify: restart exim4
template:
src: 'passwd.client'
dest: '/etc/exim4/passwd.client'
- name: Enable TLS
notify: restart exim4
template:
src: 'exim4.conf.localmacros'
dest: '/etc/exim4/exim4.conf.localmacros'
when: exim4_sendonly_enable_tls
- name: 'Set logrotate for local user mails in /var/mail'
copy:
src: 'logrotate'
dest: /etc/logrotate.d/local_user_mails
owner: root
group: root
mode: '0644'

View File

@ -0,0 +1,7 @@
---
- name: Install exim4 packages
apt:
name: '{{ exim4_sendonly_apt_packages }}'
state: present
cache_valid_time: 86400

View File

@ -0,0 +1,15 @@
---
- name: Install exim4
import_tasks: install.yml
- name: Configure exim4
import_tasks: configure.yml
- name: Start exim4
service:
name: exim4
state: started
enabled: true
changed_when: false
tags: ['always']

View File

@ -0,0 +1,3 @@
{% for user, email in exim4_sendonly_email_addresses.items() %}
{{ user }}: {{ email }}
{% endfor %}

View File

@ -0,0 +1 @@
MAIN_TLS_ENABLE = 1

View File

@ -0,0 +1,10 @@
# password file used when the local exim is authenticating to a remote
# host as a client.
#
# see exim4_passwd_client(5) for more documentation
#
# Example:
### target.mail.server.example:login:password
{% if exim4_sendonly_username != '' %}
*:{{ exim4_sendonly_username }}:{{ exim4_sendonly_password }}
{% endif %}

View File

@ -0,0 +1,30 @@
# /etc/exim4/update-exim4.conf.conf
#
# Edit this file and /etc/mailname by hand and execute update-exim4.conf
# yourself or use 'dpkg-reconfigure exim4-config'
#
# Please note that this is _not_ a dpkg-conffile and that automatic changes
# to this file might happen. The code handling this will honor your local
# changes, so this is usually fine, but will break local schemes that mess
# around with multiple versions of the file.
#
# update-exim4.conf uses this file to determine variable values to generate
# exim configuration macros for the configuration file.
#
# Most settings found in here do have corresponding questions in the
# Debconf configuration, but not all of them.
#
# This is a Debian specific file
dc_eximconfig_configtype="{{ 'internet' if exim4_sendonly_smarthost == '' else 'satellite' }}"
dc_other_hostnames='{{ ansible_hostname }}; localhost.localdomain; localhost'
dc_local_interfaces='127.0.0.1'
dc_readhost=''
dc_relay_domains=''
dc_minimaldns='false'
dc_relay_nets=''
dc_smarthost='{{ exim4_sendonly_smarthost }}'
CFILEMODE='644'
dc_use_split_config='true'
dc_hide_mailname='true'
dc_mailname_in_oh='true'
dc_localdelivery='mail_spool'

View File

@ -0,0 +1,10 @@
# Letsencrypt role
This role uses the standalone mode of certbot if no webserver is
running (typically during the first installation), else uses the nginx
module.
Note that existing certificates are renewed (using the nginx module)
as a cron task/systemd timer.
It creates snippets in `/etc/nginx/snippets/letsencrypt-{{ fqdn }}.conf`.

View File

@ -0,0 +1,3 @@
---
domains: [example.com]

View File

@ -0,0 +1,66 @@
---
- name: Install ca-certificates
apt:
state: present
name: [cron, ca-certificates, nginx]
- name: Setup or upgrade venv
command: python3 -m venv --upgrade-deps /root/certbot-venv/
changed_when: false
- name: Prepare certbot+gandi venv
pip:
chdir: /root/
virtualenv_command: /usr/bin/python3 -m venv
virtualenv: /root/certbot-venv/
state: latest
name:
- pip
- name: Install certbot+gandi in venv
pip:
chdir: /root/
state: latest
virtualenv_command: /usr/bin/python3 -m venv
virtualenv: /root/certbot-venv/
name:
- certbot
- certbot-plugin-gandi
- name: Setup Gandi credentials
copy:
content: "dns_gandi_api_key = {{ gandi_api_key }}"
mode: 0600
dest: /root/gandi.ini
- name: Create SSL dhparam
get_url:
url: https://ssl-config.mozilla.org/ffdhe2048.txt
dest: /etc/ssl/certs/dhparam.pem
mode: 0644
- name: Generate TLS certificates
command: /root/certbot-venv/bin/certbot certonly --cert-name {{ cert_name | quote }} -n --agree-tos -d {{ domains | join(",") | quote }} -m {{ admin_email | quote }} --authenticator dns-gandi --dns-gandi-credentials /root/gandi.ini
register: certbot
changed_when: '"no action taken." not in certbot.stdout'
- name: Create letsencrypt snippets
template:
src: letsencrypt.conf.j2
dest: '/etc/nginx/snippets/letsencrypt-{{ cert_name }}.conf'
- name: Setup renewal cron
cron:
name: certbot
minute: "55"
hour: "8"
job: '/root/certbot-venv/bin/certbot -q renew'
- name: Setup renewal hook to reload nginx
copy:
mode: 0755
content: |
#!/bin/sh
systemctl reload nginx
dest: /etc/letsencrypt/renewal-hooks/deploy/01-reload-nginx

View File

@ -0,0 +1,29 @@
# https://ssl-config.mozilla.org/
ssl_certificate /etc/letsencrypt/live/{{ cert_name }}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/{{ cert_name }}/privkey.pem;
ssl_session_timeout 1d;
ssl_session_cache shared:MozSSL:10m; # about 40000 sessions
ssl_session_tickets off;
# curl https://ssl-config.mozilla.org/ffdhe2048.txt > /path/to/dhparam
ssl_dhparam /etc/ssl/certs/dhparam.pem;
# intermediate configuration
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
# HSTS (ngx_http_headers_module is required) (63072000 seconds)
add_header Strict-Transport-Security "max-age=63072000" always;
# OCSP stapling
ssl_stapling on;
ssl_stapling_verify on;
# verify chain of trust of OCSP response using Root CA and Intermediate certs
ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt;
# replace with the IP address of your resolver
resolver 155.133.140.130;

View File

@ -0,0 +1,19 @@
[Unit]
Description=website daemon
After=network.target
[Service]
PIDFile=/opt/website/website.pid
User=website
Group=website
WorkingDirectory=/opt/website/src
ExecStart=/opt/website/venv/bin/gunicorn --pid /opt/website/website.pid \
--bind unix:/opt/website/website.sock --timeout 60 eqy_fr.wsgi \
--access-logfile -
ExecReload=/bin/kill -s HUP $MAINPID
ExecStop=/bin/kill -s TERM $MAINPID
PrivateTmp=true
Restart=always
[Install]
WantedBy=multi-user.target

View File

@ -0,0 +1,10 @@
---
- name: restart website
systemd: name=website state=restarted daemon_reload=yes
- name: reload nginx
service: name=nginx state=reloaded
- name: reload psql
service: name=postgresql state=reloaded

View File

@ -0,0 +1,4 @@
---
dependencies:
- role: letsencrypt

View File

@ -0,0 +1,212 @@
---
- name: Install dependencies
apt:
state: present
name:
- cron
- gettext
- git
- nginx
- postgresql
- postgresql-server-dev-all # To compile Python client.
- pgbadger
- python3
- python3-pip
- python3-psycopg2
- python3-venv
update_cache: true
tags: website
- name: Add unix user website
user:
name: website
shell: /bin/false
system: yes
home: /opt/website
tags: website
- name: install website.service (systemd)
copy:
src: systemd/website.service
dest: /etc/systemd/system/website.service
owner: root
group: root
mode: 0644
notify: restart website
tags: website
- name: add user website to pgsql
become: true
become_user: postgres
postgresql_user:
user: website
tags: website
- name: add database media
become: true
become_user: postgres
postgresql_db:
name: media
owner: website
tags: website
- name: Collect PostgreSQL version and extensions
become: yes
become_user: postgres
postgresql_info:
filter: ver*
register: db_info
- name: Configure psql
notify: reload psql
copy:
dest: "/etc/postgresql/{{ db_info.version.major }}/main/conf.d/media.conf"
owner: postgres
group: postgres
mode: 0644
content: |
log_min_duration_statement = 0
log_checkpoints = on
log_connections = on
log_disconnections = on
log_lock_waits = on
log_temp_files = 0
log_autovacuum_min_duration = 0
log_error_verbosity = default
lc_messages='en_US.UTF-8'
lc_messages='C'
- name: Synchronize source
ansible.posix.synchronize:
src: /home/mdk/eqy.fr/
dest: /opt/website/src/
notify: restart website
- name: Creates a /opt/website/venv for virtual environments
file:
path: /opt/website/venv
state: directory
mode: 0755
tags: website
- name: Setup or upgrade venv
command: python3 -m venv --upgrade-deps /opt/website/venv
changed_when: false
- name: Creates a /opt/website/locale for translations
file:
path: /opt/website/locale
state: directory
mode: 0755
owner: root
group: root
tags: website
- name: Creates a /opt/website/media for medias
file:
path: /opt/website/media
state: directory
mode: 0755
owner: website
group: website
tags: website
- name: Creates a /opt/website/static for static
file:
path: /opt/website/locale
state: directory
mode: 0755
owner: root
group: root
tags: website
- name: pip installs requirements
pip:
chdir: /opt/website/src
requirements: requirements.txt
virtualenv: /opt/website/venv
virtualenv_command: /usr/bin/python3 -m venv
tags: website
- name: pip installs psycopg2
pip:
chdir: /opt/website/src
name: psycopg2
virtualenv: /opt/website/venv
virtualenv_command: /usr/bin/python3 -m venv
tags: website
- name: pip installs gunicorn
pip:
chdir: /opt/website/src
name: gunicorn
virtualenv: /opt/website/venv
virtualenv_command: /usr/bin/python3 -m venv
tags: website
- name: Install website configuration
template:
src: local_settings.py.j2
dest: /opt/website/src/local_settings.py
owner: root
group: website
mode: 0640
notify: restart website
tags: website
- name: Migrate db
command: "/opt/website/venv/bin/python manage.py migrate"
args:
chdir: "/opt/website/src"
register: migrate_result
changed_when: '" Applying " in migrate_result.stdout'
run_once: true
become: true
become_user: website
tags: [website, test]
- name: Collectstatic
command: "/opt/website/venv/bin/python manage.py collectstatic --noinput"
args:
chdir: "/opt/website/src"
register: collectstatic_result
changed_when: '"Copying " in collectstatic_result.stdout'
tags: [website, test]
- name: Compile gettext
command: "/opt/website/venv/bin/python manage.py compilemessages"
args:
chdir: "/opt/website/src"
notify: restart website
tags: [website, test]
- name: Ensure website is running
service: name=website state=started enabled=yes
tags: website
- name: Configure nginx host
template:
src: nginx-vhost
dest: "/etc/nginx/sites-available/{{ website_vhost }}"
owner: root
group: root
mode: 0644
notify: reload nginx
tags: website
- name: Create symlink for API nginx site
file:
src: "/etc/nginx/sites-available/{{ website_vhost }}"
dest: "/etc/nginx/sites-enabled/{{ website_vhost }}"
state: link
notify: reload nginx
tags: website
- name: Daily backup
cron:
user: website
name: "backup"
job: "/usr/bin/pg_dump --clean media > backup.sql"
hour: '2'
minute: '0'

View File

@ -0,0 +1,22 @@
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": "media",
"USER": "website",
}
}
STATIC_ROOT = "/opt/website/static/"
MEDIA_ROOT = "/opt/website/media/"
DEBUG = False
SERVER_EMAIL = "julien@palard.fr"
ALLOWED_HOSTS = ["eqy.fr"]
SECRET_KEY = r'ephae4Xo Aeve8aic eeph7Eed Sho9oloo johY4ae5 ish0Eela areiy3Ia Joechoo6'
LOCALE_PATHS = ["/opt/website/locale"]
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTOCOL", "https")

View File

@ -0,0 +1,34 @@
server {
listen [::]:80;
listen 80;
server_name {{ website_vhost }};
location / {
return 301 https://{{ website_vhost }}$request_uri;
}
}
server {
listen 443 http2 ssl;
listen [::]:443 http2 ssl;
server_name {{ website_vhost }};
client_max_body_size 666M;
include snippets/letsencrypt-{{ cert_name }}.conf;
location /static/ {
alias /opt/website/static/;
}
location /media/ {
alias /opt/website/media/;
}
location / {
proxy_pass http://unix:/opt/website/website.sock;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Protocol $scheme;
}
}

12
deploy/website.yml Normal file
View File

@ -0,0 +1,12 @@
---
- hosts: website
roles:
- common
- website
vars:
website_vhost: eqy.fr
admin_email: julien+photos@palard.fr
cert_name: eqy
domains:
- eqy.fr

View File

@ -123,3 +123,8 @@ MEDIA_URL = 'media/'
# https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
try:
from local_settings import *
except ImportError:
pass

1
requirements.txt Normal file
View File

@ -0,0 +1 @@
django