diff --git a/docker/Dockerfile b/docker/Dockerfile index 8091490..da2131c 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -3,7 +3,7 @@ FROM ubuntu:18.04 RUN set -ex; \ apt-get update; \ DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ - python3 python3-pip uwsgi uwsgi-plugin-python3 nginx postgresql-10 postgresql-client-10 \ + python3 python3-pip uwsgi uwsgi-plugin-python3 nginx postgresql-10 postgresql-client-10 vim-nox \ python3-dev build-essential; \ rm -rf /var/lib/apt/lists/*; \ locale-gen en_US.UTF-8; @@ -11,6 +11,7 @@ ENV LANG en_US.UTF-8 VOLUME /var/log # Configure Postgresql +ENV PGDATA=/var/lib/postgresql/10/main RUN set -ex; \ pg_conftool set lc_messages 'en_US.UTF-8'; \ pg_conftool set lc_monetary 'en_US.UTF-8'; \ @@ -18,8 +19,7 @@ RUN set -ex; \ pg_conftool set lc_time 'en_US.UTF-8'; \ pg_conftool set listen_addresses ''; COPY pg_hba.conf /etc/postgresql/10/main -COPY create_db.sh / -VOLUME /var/lib/postgresql/10/main +VOLUME /var/lib/postgresql # Create users and a group for the Django apps. # Their home dir does not contain the web files; they are in /var/www/{appname} @@ -43,19 +43,28 @@ COPY deploy/ /var/www/ # Set up My Data WORKDIR /var/www/mydata RUN pipenv install --deploy +COPY --chown=mydata:django deploytime-settings-mydata.py /var/www/mydata/mydata/settings.py RUN pipenv run python3 manage.py collectstatic --noinput -RUN rm /var/www/mydata/mydata/settings.py -VOLUME /var/www/mydata/mydata/settings.py # Set up Open Data WORKDIR /var/www/opendata RUN pipenv install --deploy +COPY --chown=mydata:django deploytime-settings-opendata.py /var/www/opendata/opendata/settings.py RUN pipenv run python3 manage.py collectstatic --noinput -RUN rm /var/www/opendata/opendata/settings.py -VOLUME /var/www/opendata/opendata/settings.py WORKDIR / +VOLUME /var/www/secrets/ EXPOSE 80 COPY entrypoint.sh / +COPY create_db.sh / CMD ["/bin/bash", "/entrypoint.sh"] + +# Generate on the host with: +# openssl dhparam -out /etc/nginx/ssl/dhparams.pem 4096 +VOLUME /etc/nginx/ssl + +# Configure nginx +COPY nginx/snippets/* /etc/nginx/snippets/ +COPY nginx/sites-available/* /etc/nginx/sites-available/ +RUN ln -s /etc/nginx/sites-available/mydata /etc/nginx/sites-enabled/ diff --git a/docker/create_db.sh b/docker/create_db.sh index 62290f6..bc1e58a 100755 --- a/docker/create_db.sh +++ b/docker/create_db.sh @@ -1,7 +1,20 @@ #!/bin/sh -ex # This should only be run once, to create the database + users. +chown postgres:postgres /var/lib/postgresql +su postgres -c '/usr/lib/postgresql/10/bin/initdb' + +echo "Starting postgresql" +pg_ctlcluster 10 main start + + +echo +echo "Creating 'mydata' user" su postgres -c 'createuser mydata -dPERS' + +echo +echo "Creating 'opendata' user" su postgres -c 'createuser opendata -dPERS' + su postgres -c 'createdb -E UTF8 -O mydata mydata' su postgres -c 'createdb -E UTF8 -O opendata opendata' diff --git a/docker/deploytime-settings-mydata.py b/docker/deploytime-settings-mydata.py index a2e3225..129983b 100644 --- a/docker/deploytime-settings-mydata.py +++ b/docker/deploytime-settings-mydata.py @@ -13,3 +13,10 @@ from mydata.common_settings import * DEBUG = False SECRET_KEY = r'''1234''' + +import sys +import os + +if os.path.exists('/var/www/secrets/mydata_secrets.py'): + sys.path.append('/var/www/secrets') + from mydata_secrets import * diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 0000000..b8fcb00 --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,12 @@ +version: '3.4' +services: + mydata: + image: blender-mydata:latest + container_name: mydata + restart: always + volumes: + # format: HOST:CONTAINER + - /var/www/mydata/config:/var/www/secrets:ro + - /var/www/mydata/log:/var/log + - /var/www/mydata/postgresql:/var/lib/postgresql + - /var/www/mydata/ssl:/etc/nginx/ssl diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 456cd95..2b33e25 100755 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -4,9 +4,25 @@ set -e echo -n "Starting up at " date -Iseconds -# Start our daemons -echo "Starting postgresql" -pg_ctlcluster 10 main start +if [ ! -e /etc/ssl/dhparam.pem ]; then + # We don't generate it automatically, because it should be mounted from + # the Docker host and generated only once, rather than every time a new + # container is created. + echo "/etc/ssl/dhparam.pem missing, generate with:" + echo + echo " openssl dhparam -out /etc/ssl/dhparams.pem 4096" + echo + echo "GENERATING TEMPORARY INSECURE FILE" + openssl dhparam -out /etc/ssl/dhparams.pem 512 +fi + +if [ ! -e $PGDATA ]; then + echo "$PGDATA does not exist, initialising database" + . /create_db.sh +else + echo "Starting postgresql" + pg_ctlcluster 10 main start +fi echo "Starting nginx in foreground" nginx -g 'daemon off;' diff --git a/docker/nginx/conf.d/ssl_ciphers.conf b/docker/nginx/conf.d/ssl_ciphers.conf new file mode 100644 index 0000000..8b1911d --- /dev/null +++ b/docker/nginx/conf.d/ssl_ciphers.conf @@ -0,0 +1,9 @@ +ssl_certificate /etc/ssl/certs/ssl-cert-snakeoil.pem; +ssl_certificate_key /etc/ssl/private/ssl-cert-snakeoil.key; + +ssl_session_cache shared:SSL:5m; +ssl_session_timeout 5m; + +ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA'; + +ssl_dhparam /etc/ssl/dhparams.pem; diff --git a/docker/nginx/sites-available/mydata b/docker/nginx/sites-available/mydata new file mode 100644 index 0000000..7fd9850 --- /dev/null +++ b/docker/nginx/sites-available/mydata @@ -0,0 +1,37 @@ +server { + listen 80; + listen [::]:80; + server_name www.mydata.local; + return 301 https://mydata.local/; +} + +server { + listen 443 ssl http2; + listen [::]:443 ssl http2; + ssl on; + server_name www.mydata.local; + return 301 https://mydata.local/; +} + +server { + listen 443 ssl http2; + listen [::]:443 ssl http2; + ssl on; + + location / { + uwsgi_pass unix:/var/www/mydata/uwsgi.sock; + include uwsgi_params; + uwsgi_param HTTPS on; + } + + server_name mydata.local; + access_log /var/log/nginx/mydata_access.log; + error_log /var/log/nginx/mydata_error.log; + + include snippets/letsencryptauth.conf; + include snippets/securityheadersio.conf; + + location /assets/ { + alias /var/www/mydata/assets/; + } +} diff --git a/docker/nginx/snippets/letsencryptauth.conf b/docker/nginx/snippets/letsencryptauth.conf new file mode 100644 index 0000000..704c225 --- /dev/null +++ b/docker/nginx/snippets/letsencryptauth.conf @@ -0,0 +1,6 @@ +location /.well-known/acme-challenge { + alias /etc/letsencrypt/webrootauth/.well-known/acme-challenge; + location ~ /.well-known/acme-challenge/(.*) { + add_header Content-Type application/jose+json; + } +} diff --git a/docker/nginx/snippets/securityheadersio.conf b/docker/nginx/snippets/securityheadersio.conf new file mode 100644 index 0000000..ebbebcc --- /dev/null +++ b/docker/nginx/snippets/securityheadersio.conf @@ -0,0 +1,13 @@ +# HTTP header security, from +# https://securityheaders.io/?q=https%3A%2F%2Fstuvel.eu%2F&hide=on + +# TODO: enable this once we have TLS properly configured +# add_header Strict-Transport-Security "max-age=15768000; includeSubDomains; preload;"; + +# Add headers to serve security related headers +add_header X-Content-Type-Options nosniff; +add_header X-Frame-Options "SAMEORIGIN"; +add_header X-XSS-Protection "1; mode=block"; +add_header X-Robots-Tag none; + +#add_header Content-Security-Policy-Report-Only "default-src https: 'unsafe-inline'; report-uri https://stuvel.eu/csp";