/django-docker-nginx-uWSGI-ssl-test

Test django app for testing the deployment using nginx( reverse proxy ( serve static files) ) , uWSGI, certbot, Lets Encrypt and aws-ec2

Primary LanguagePythonMIT LicenseMIT

django-docker-nginx-uWSGI-ssl-test

Test django app for testing the deployment using nginx( reverse proxy ( serve static files) ) , uWSGI, certbot, Lets Encrypt and aws-ec2.


This is a refference django app to test how to impliment SSL secured connection with Let's Encrypt-certbot, NGINX, uWSGI, and aws EC2 deployment with docker-compose.


Services

  • NGINX
  • certbot
  • django app

arch


NGINX

  • Redirect HTTP requests to HTTPS
  • Handle Django static files
  • Forward requests to uWSGI

default.conf.tpl

server {
    listen 80;
    server_name ${DOMAIN} www.${DOMAIN};

    location /.well-known/acme-challenge/ {
        root /vol/www/;
    }

    location / {
        return 301 https://$host$request_uri;
    }
} 
  • Listen port 80
  • Specify server name
  • Add a location block for /.well-known/acme-challenge/ that serves data from /vol/www/ – this will serve a one time password generated by the certbot which needs to be accessible on the internet for letsencrypt to give us a certificate.
  • Redirect all other requests to https

default-ssl.conf.tpl

server {
    listen 80;
    server_name ${DOMAIN} www.${DOMAIN};

    location /.well-known/acme-challenge/ {
        root /vol/www/;
    }

    location / {
        return 301 https://$host$request_uri;
    }
}

server {
    listen      443 ssl;
    server_name ${DOMAIN} www.${DOMAIN};

    ssl_certificate     /etc/letsencrypt/live/${DOMAIN}/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/${DOMAIN}/privkey.pem;

    include     /etc/nginx/options-ssl-nginx.conf;

    ssl_dhparam /vol/proxy/ssl-dhparams.pem;

    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

    location /static {
        alias /vol/static;
    }

    location / {
        uwsgi_pass           ${APP_HOST}:${APP_PORT};
        include              /etc/nginx/uwsgi_params;
        client_max_body_size 10M;
    }
}

The first block is the same as the default.conf.tpl file we created previously. This is so we can continue to redirect HTTP to HTTPS and handle the acme challenge for certificate renewals.

The second server block does the following:

  • Listens on port 443 with SSL
  • Sets the server name as configured by the DOMAIN variable
  • Configure our ssl_certificate and ssl_certificate_key which will be set by certbot and mapped to /etc/letsencrypt/ via a volume later on
  • Include our options-ssl-nginx.conf file which we’ll add in a minute
  • Set our ssl_dhparam file.
  • Adds a header which enables Strict-Transport-Security which is a way to tell the client’s browser to always use HTTPS for our domain and subdomains.
  • Handles requests to /static and serves them from the /vol/static directory – this is a way to handle Django static files (we don’t cover that in this guide, but you may want to add it later)
  • Adds a location block for / which will take the rest of the requests and forward them to our uWSGI service running on APP_HOST and APP_PORT.

options-ssl-nginx.conf

Taken from: https://github.com/certbot/certbot/blob/1.28.0/certbot-nginx/certbot_nginx/_internal/tls_configs/options-ssl-nginx.conf

ssl_session_cache shared:le_nginx_SSL:10m;
ssl_session_timeout 1440m;
ssl_session_tickets off;

ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;

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";

uwsgi_params

uwsgi_param QUERY_STRING $query_string;
uwsgi_param REQUEST_METHOD $request_method;
uwsgi_param CONTENT_TYPE $content_type;
uwsgi_param CONTENT_LENGTH $content_length;
uwsgi_param REQUEST_URI $request_uri;
uwsgi_param PATH_INFO $document_uri;
uwsgi_param DOCUMENT_ROOT $document_root;
uwsgi_param SERVER_PROTOCOL $server_protocol;
uwsgi_param REMOTE_ADDR $remote_addr;
uwsgi_param REMOTE_PORT $remote_port;
uwsgi_param SERVER_ADDR $server_addr;
uwsgi_param SERVER_PORT $server_port;
uwsgi_param SERVER_NAME $server_name;

docker/proxy/run.sh

#!/bin/bash

set -e

echo "Checking for dhparams.pem"
if [ ! -f "/vol/proxy/ssl-dhparams.pem" ]; then
  echo "dhparams.pem does not exist - creating it"
  openssl dhparam -out /vol/proxy/ssl-dhparams.pem 2048
fi

# Avoid replacing these with envsubst
export host=\$host
export request_uri=\$request_uri

echo "Checking for fullchain.pem"
if [ ! -f "/etc/letsencrypt/live/${DOMAIN}/fullchain.pem" ]; then
  echo "No SSL cert, enabling HTTP only..."
  envsubst < /etc/nginx/default.conf.tpl > /etc/nginx/conf.d/default.conf
else
  echo "SSL cert exists, enabling HTTPS..."
  envsubst < /etc/nginx/default-ssl.conf.tpl > /etc/nginx/conf.d/default.conf
fi

nginx -g 'daemon off;'
  • Check if /vol/proxy/ssl-dhparams.pem exists, if not then run openssl dhparam to generate it – this is required the first time we run the proxy.
  • Check if /etc/letsencrypt/live/${DOMAIN}/fullchain.pem exists, if not then copy the default.conf.tpl – this will cause the server to run without SSL so it can serve the acme challenge.
  • Set host and require_uri variables to prevent them being overwritten with blank values in the configs.
  • If the fullchain.pem file does exist, copy the default-ssl.conf.tpl – this will cause the server to enable SSL.

docker/proxy/Dockerfile

FROM nginx:1.23.0-alpine

COPY ./nginx/* /etc/nginx/
COPY ./run.sh /run.sh

ENV APP_HOST=app
ENV APP_PORT=9000

USER root

RUN apk add --no-cache openssl bash
RUN chmod +x /run.sh

VOLUME /vol/static
VOLUME /vol/www

CMD ["/run.sh"]

  • Based on the nginx image
  • Copies the config files and scripts from our project folder to the docker image
  • Set default configuration values for APP_HOST and APP_PORT which are used to configure which uWSGI service requests will be forwarded too
  • Install openssl which is required to generate the dh params and bash which is used to run our run.sh script
  • Make our run.sh file executable
  • Define two values: /vol/static which can be used to map static files from our Django app to the proxy, and /vol/www which will serve our acme challenge
  • Set the command to /run.sh so we don’t need to specify it when running containers from our image.

certbot

docker/certbot/certify-init.sh

#!/bin/sh

# Waits for proxy to be available, then gets the first certificate.

set -e

# Use netcat (nc) to check port 80, and keep checking every 5 seconds
# until it is available. This is so nginx has time to start before
# certbot runs.
until nc -z proxy 80; do
    echo "Waiting for proxy..."
    sleep 5s & wait ${!}
done

echo "Getting certificate..."

certbot certonly \
    --webroot \
    --webroot-path "/vol/www/" \
    -d "$DOMAIN" \
    --email $EMAIL \
    --rsa-key-size 4096 \
    --agree-tos \
    --noninteractive
  • certonly means get the certificate only (don’t try and install it into the web server)
  • --webroot tells it to obtain a certificate by writing to the wrbroot directory of an already running server
  • --webroot-path is used to specify the path of the webroot
  • -d is used to specify the domain we want to get the certificate for – we’ll set the value as an environment variable later
  • --email needs to be set to a valid email address for renewal to work – we’ll also set this as an environment variable later
  • --rsa-key-size is the size of the rsa key (4096 is better than 2048)
  • --agree-tos confirms that we agree to the Let’s Encrypt Subscriber Agreement
  • --noninteractive tells certbot we are running this as a script, so we don’t want it to prompt for any inputs

certbot dockerfile

FROM certbot/certbot:v1.27.0

COPY certify-init.sh /opt/
RUN chmod +x /opt/certify-init.sh

ENTRYPOINT []
CMD ["certbot", "renew"]

  • Bases our image from certbot so we can get the certbot executable as well as netcat
  • Adds our certify-init.sh script which we created above
  • Sets the entrypoint to an empty array – this is to override the default certbot entrypoint so we can run our script easier
  • Set the default command to certbot renew