From b8e02d21890a557fee447c0024ab8e8042303023 Mon Sep 17 00:00:00 2001 From: mjsisley Date: Fri, 3 Jun 2016 08:07:39 -0700 Subject: [PATCH] Add certbot(letsencrypt) support for docker --- CONTRIBUTORS.rst | 2 + cookiecutter.json | 1 + docs/deployment-with-docker.rst | 44 +++++++++ hooks/post_gen_project.py | 33 +++++-- .../compose/nginx/Dockerfile | 9 +- .../compose/nginx/dhparams.example.pem | 3 + .../compose/nginx/nginx-secure.conf | 92 +++++++++++++++++++ .../compose/nginx/nginx.conf | 37 ++++++-- .../compose/nginx/start.sh | 81 ++++++++++++++++ .../docker-compose.yml | 30 +++++- 10 files changed, 312 insertions(+), 20 deletions(-) create mode 100755 {{cookiecutter.project_slug}}/compose/nginx/dhparams.example.pem create mode 100755 {{cookiecutter.project_slug}}/compose/nginx/nginx-secure.conf create mode 100755 {{cookiecutter.project_slug}}/compose/nginx/start.sh diff --git a/CONTRIBUTORS.rst b/CONTRIBUTORS.rst index 4e0bc3bc..b6de7a3c 100644 --- a/CONTRIBUTORS.rst +++ b/CONTRIBUTORS.rst @@ -96,6 +96,7 @@ Listed in alphabetical order. Matt Linares Matt Menzenski `@menzenski`_ Matt Warren `@mfwarren`_ + Matthew Sisley `@mjsisley`_ Meghan Heintz `@dot2dotseurat`_ mozillazg `@mozillazg`_ Pablo `@oubiga`_ @@ -175,6 +176,7 @@ Listed in alphabetical order. .. _@yunti: https://github.com/yunti .. _@zcho: https://github.com/zcho .. _@noisy: https://github.com/noisy +.. _@mjsisley: https://github.com/mjsisley Special Thanks ~~~~~~~~~~~~~~ diff --git a/cookiecutter.json b/cookiecutter.json index 8fe7010a..5ad55886 100644 --- a/cookiecutter.json +++ b/cookiecutter.json @@ -22,5 +22,6 @@ "use_heroku": "n", "use_grunt": "n", "use_angular": "n", + "use_certbot": "n", "open_source_license": ["MIT", "BSD", "Not open source"] } diff --git a/docs/deployment-with-docker.rst b/docs/deployment-with-docker.rst index de3abf7d..61ed05f4 100644 --- a/docs/deployment-with-docker.rst +++ b/docs/deployment-with-docker.rst @@ -110,3 +110,47 @@ To get the status, run:: If you have errors, you can always check your stack with `docker-compose`. Switch to your projects root directory and run:: docker-compose ps + +If you are using certbot for https, you must do the following before running anything with docker-compose: + +Replace dhparam.pem.example with a generated dhparams.pem file before running anything with docker-compose. You can generate this on ubuntu or OS X by running the following in the project root: + +:: + + $ openssl dhparam -out /path/to/project/compose/nginx/dhparams.pem 2048 + +If you would like to add additional subdomains to your certificate, you must add additional parameters to the certbot command in the `docker-compose.yml` file: + +Replace: + +:: + + command: bash -c "sleep 6 && certbot certonly -n --standalone -d {{ cookiecutter.domain_name }} --text --agree-tos --email mjsisley@relawgo.com --server https://acme-v01.api.letsencrypt.org/directory --rsa-key-size 4096 --verbose --keep-until-expiring --standalone-supported-challenges http-01" + +With: + +:: + + command: bash -c "sleep 6 && certbot certonly -n --standalone -d {{ cookiecutter.domain_name }} -d www.{{ cookiecutter.domain_name }} -d etc.{{ cookiecutter.domain_name }} --text --agree-tos --email {{ cookiecutter.email }} --server https://acme-v01.api.letsencrypt.org/directory --rsa-key-size 4096 --verbose --keep-until-expiring --standalone-supported-challenges http-01" + +Please be cognizant of Certbot/Letsencrypt certificate requests limits when getting this set up. The provide a test server that does not count against the limit while you are getting set up. + +The certbot certificates expire after 3 months. +If you would like to set up autorenewal of your certificates, the following commands can be put into a bash script: + +:: + + #!/bin/bash + cd + docker-compose run certbot bash -c "sleep 6 && certbot certonly --standalone -d {{ cookiecutter.domain_name }} --text --agree-tos --email {{ cookiecutter.email }} --server https://acme-v01.api.letsencrypt.org/directory --rsa-key-size 4096 --verbose --keep-until-expiring --standalone-supported-challenges http-01" + docker exec pearl_nginx_1 nginx -s reload + +And then set a cronjob by running `crontab -e` and placing in it (period can be adjusted as desired): + +0 4 * * 1 /path/to/bashscript/renew_certbot.sh + + + + + + diff --git a/hooks/post_gen_project.py b/hooks/post_gen_project.py index cffb052e..5f723550 100644 --- a/hooks/post_gen_project.py +++ b/hooks/post_gen_project.py @@ -139,6 +139,16 @@ def remove_grunt_files(): PROJECT_DIRECTORY, filename )) +def remove_certbot_files(): + """ + Removes files needed for certbot if it isn't going to be used + """ + nginx_dir_location = os.path.join(PROJECT_DIRECTORY, 'compose/nginx') + for filename in ["nginx-secure.conf", "start.sh", "dhparams.example.pem"]: + os.remove(os.path.join( + nginx_dir_location, filename + )) + # IN PROGRESS # def copy_doc_files(project_directory): # cookiecutters_dir = DEFAULT_CONFIG['cookiecutters_dir'] @@ -180,8 +190,11 @@ if '{{ cookiecutter.use_docker }}'.lower() != 'y': if '{{ cookiecutter.use_grunt }}'.lower() != 'y': remove_grunt_files() +# 7. Removes all certbot/letsencrypt files if it isn't going to be used +if '{{ cookiecutter.use_certbot }}'.lower() != 'y': + remove_certbot_files() -# 7. Display a warning if use_docker and use_grunt are selected. Grunt isn't supported by our +# 8. Display a warning if use_docker and use_grunt are selected. Grunt isn't supported by our # docker config atm. if '{{ cookiecutter.use_grunt }}'.lower() == 'y' and '{{ cookiecutter.use_docker }}'.lower() == 'y': print( @@ -190,13 +203,19 @@ if '{{ cookiecutter.use_grunt }}'.lower() == 'y' and '{{ cookiecutter.use_docker " grunt service to your docker configuration manually." ) -# 7. Display a warning if use_docker and use_mailhog are selected. Mailhog isn't supported by our -# docker config atm. -if '{{ cookiecutter.use_mailhog }}'.lower() == 'y' and '{{ cookiecutter.use_docker }}'.lower() == 'y': +# 9. Removes the certbot/letsencrypt files and display a warning if use_certbot is selected and use_docker isn't. +if '{{ cookiecutter.use_certbot }}'.lower() == 'y' and '{{ cookiecutter.use_docker }}'.lower() != 'y': + remove_certbot_files() print( - "You selected to use docker and mailhog. This is NOT supported out of the box for now. You" - " can continue to use the project like you normally would, but you will need to add a " - " mailhog service to your docker configuration manually." + "You selected to use certbot(letsencrypt) and didn't select to use docker. This is NOT supported out of the box for now. You " + "can continue to use the project like you normally would, but you will no certbot files have been included" + ) + +# 10. Directs the user to the documentation if certbot and docker are selected. +if '{{ cookiecutter.use_certbot }}'.lower() == 'y' and '{{ cookiecutter.use_docker }}'.lower() == 'y': + print( + "You selected to use certbot(letsencrypt), please see the documentation for instructions on how to use this in production. " + "You must generate a dhparams.pem file before running docker-compose in a production environment." ) # 4. Copy files from /docs/ to {{ cookiecutter.project_slug }}/docs/ diff --git a/{{cookiecutter.project_slug}}/compose/nginx/Dockerfile b/{{cookiecutter.project_slug}}/compose/nginx/Dockerfile index 19639576..6a08a744 100644 --- a/{{cookiecutter.project_slug}}/compose/nginx/Dockerfile +++ b/{{cookiecutter.project_slug}}/compose/nginx/Dockerfile @@ -1,2 +1,9 @@ FROM nginx:latest -ADD nginx.conf /etc/nginx/nginx.conf \ No newline at end of file +COPY nginx.conf /etc/nginx/nginx.conf + +{% if cookiecutter.use_certbot == 'y' and cookiecutter.use_docker == 'y' %} +COPY start.sh /start.sh +COPY nginx-secure.conf /etc/nginx/nginx-secure.conf +COPY dhparams.pem /etc/ssl/private/dhparams.pem +CMD /start.sh +{% endif %} diff --git a/{{cookiecutter.project_slug}}/compose/nginx/dhparams.example.pem b/{{cookiecutter.project_slug}}/compose/nginx/dhparams.example.pem new file mode 100755 index 00000000..e88d8123 --- /dev/null +++ b/{{cookiecutter.project_slug}}/compose/nginx/dhparams.example.pem @@ -0,0 +1,3 @@ +-----BEGIN DH PARAMETERS----- +EXAMPLE_FILE +-----END DH PARAMETERS----- diff --git a/{{cookiecutter.project_slug}}/compose/nginx/nginx-secure.conf b/{{cookiecutter.project_slug}}/compose/nginx/nginx-secure.conf new file mode 100755 index 00000000..78f90279 --- /dev/null +++ b/{{cookiecutter.project_slug}}/compose/nginx/nginx-secure.conf @@ -0,0 +1,92 @@ +user nginx; +worker_processes 1; + +error_log /var/log/nginx/error.log warn; +pid /var/run/nginx.pid; + + +events { + worker_connections 1024; +} + +http { + + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + sendfile on; + #tcp_nopush on; + + keepalive_timeout 65; + + proxy_headers_hash_bucket_size 52; + + gzip on; + + upstream app { + server django:5000; + } + server { + listen 80; + server_name ___my.example.com___ www.___my.example.com___; + + location /.well-known/acme-challenge { + proxy_pass http://___LETSENCRYPT_IP___:___LETSENCRYPT_PORT___; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Forwarded-Proto https; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location / { + return 301 https://$server_name$request_uri; + } + + } + + server { + listen 443; + server_name ___my.example.com___ www.___my.example.com___; + + ssl on; + ssl_certificate /etc/letsencrypt/live/___my.example.com___/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/___my.example.com___/privkey.pem; + ssl_session_timeout 5m; + ssl_protocols TLSv1 TLSv1.1 TLSv1.2; + ssl_ciphers 'EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH'; + ssl_prefer_server_ciphers on; + + ssl_session_cache shared:SSL:10m; + ssl_dhparam /etc/ssl/private/dhparams.pem; + + location /.well-known/acme-challenge { + proxy_pass http://___LETSENCRYPT_HTTPS_IP___:___LETSENCRYPT_HTTPS_PORT___; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Forwarded-Proto https; + } + + location / { + # checks for static file, if not found proxy to app + try_files $uri @proxy_to_app; + } + + # cookiecutter-django app + location @proxy_to_app { + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $http_host; + proxy_redirect off; + + proxy_pass http://app; + + } + + } + +} diff --git a/{{cookiecutter.project_slug}}/compose/nginx/nginx.conf b/{{cookiecutter.project_slug}}/compose/nginx/nginx.conf index 720b22e5..54d955d7 100644 --- a/{{cookiecutter.project_slug}}/compose/nginx/nginx.conf +++ b/{{cookiecutter.project_slug}}/compose/nginx/nginx.conf @@ -9,9 +9,9 @@ events { worker_connections 1024; } - http { - include /etc/nginx/mime.types; + + include /etc/nginx/mime.types; default_type application/octet-stream; log_format main '$remote_addr - $remote_user [$time_local] "$request" ' @@ -31,23 +31,40 @@ http { server django:5000; } - server { - listen 80; - charset utf-8; + server { + listen 80; + charset utf-8; + {% if cookiecutter.use_certbot == 'y' and cookiecutter.use_docker == 'y' %} + server_name ___my.example.com___ ; - location / { + location /.well-known/acme-challenge { + proxy_pass http://___LETSENCRYPT_IP___:___LETSENCRYPT_PORT___; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Forwarded-Proto https; + } + + {% endif %} + + location / { # checks for static file, if not found proxy to app try_files $uri @proxy_to_app; } - location @proxy_to_app { + + # cookiecutter-django app + location @proxy_to_app { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $http_host; proxy_redirect off; proxy_pass http://app; - } - } -} \ No newline at end of file + } + } +} + + + + diff --git a/{{cookiecutter.project_slug}}/compose/nginx/start.sh b/{{cookiecutter.project_slug}}/compose/nginx/start.sh new file mode 100755 index 00000000..2079e194 --- /dev/null +++ b/{{cookiecutter.project_slug}}/compose/nginx/start.sh @@ -0,0 +1,81 @@ +echo sleep 5 +sleep 5 + +echo build starting nginx config + + +echo replacing ___my.example.com___/$MY_DOMAIN_NAME +echo replacing ___LETSENCRYPT_IP___/$LETSENCRYPT_PORT_80_TCP_ADDR +echo replacing ___LETSENCRYPT_PORT___/$LETSENCRYPT_PORT_80_TCP_PORT +echo replacing ___APPLICATION_IP___/$APP_PORT_80_TCP_ADDR +echo replacing ___APPLICATION_PORT___/$APP_PORT_80_TCP_PORT + +# Put your domain name into the nginx reverse proxy config. +sed -i "s/___my.example.com___/$MY_DOMAIN_NAME/g" /etc/nginx/nginx.conf +# Add your app's container IP and port into config +sed -i "s/___APPLICATION_IP___/$APP_PORT_80_TCP_ADDR/g" /etc/nginx/nginx.conf +sed -i "s/___APPLICATION_PORT___/$APP_PORT_80_TCP_PORT/g" /etc/nginx/nginx.conf +sed -i "s/___LETSENCRYPT_IP___/$LETSENCRYPT_PORT_80_TCP_ADDR/g" /etc/nginx/nginx.conf +sed -i "s/___LETSENCRYPT_PORT___/$LETSENCRYPT_PORT_80_TCP_PORT/g" /etc/nginx/nginx.conf + +cat /etc/nginx/nginx.conf +echo . +echo Firing up nginx in the background. +nginx + +# # Check user has specified domain name +if [ -z "$MY_DOMAIN_NAME" ]; then + echo "Need to set MY_DOMAIN_NAME (to a letsencrypt-registered name)." + exit 1 +fi + +# This bit waits until the letsencrypt container has done its thing. +# We see the changes here bceause there's a docker volume mapped. +echo Waiting for folder /etc/letsencrypt/live/$MY_DOMAIN_NAME to exist +while [ ! -d /etc/letsencrypt/live/$MY_DOMAIN_NAME ] ; +do + sleep 2 +done + +while [ ! -f /etc/letsencrypt/live/$MY_DOMAIN_NAME/fullchain.pem ] ; +do + echo Waiting for file fullchain.pem to exist + sleep 2 +done + +while [ ! -f /etc/letsencrypt/live/$MY_DOMAIN_NAME/privkey.pem ] ; +do + echo Waiting for file privkey.pem to exist + sleep 2 +done + +# This is added so that when the certificate is being renewed or is already in place, nginx waits for everything to be good. +sleep 15 + +echo replacing ___my.example.com___/$MY_DOMAIN_NAME +echo replacing ___LETSENCRYPT_IP___/$LETSENCRYPT_PORT_80_TCP_ADDR +echo replacing ___LETSENCRYPT_PORT___/$LETSENCRYPT_PORT_80_TCP_PORT +echo replacing ___LETSENCRYPT_HTTPS_IP___/$LETSENCRYPT_PORT_443_TCP_ADDR +echo replacing ___LETSENCRYPT_HTTPS_PORT___/$LETSENCRYPT_PORT_443_TCP_PORT +echo replacing ___APPLICATION_IP___/$APP_PORT_80_TCP_ADDR +echo replacing ___APPLICATION_PORT___/$APP_PORT_80_TCP_PORT + + +# Put your domain name into the nginx reverse proxy config. +sed -i "s/___my.example.com___/$MY_DOMAIN_NAME/g" /etc/nginx/nginx-secure.conf + +# Add LE container IP and port into config +sed -i "s/___LETSENCRYPT_IP___/$LETSENCRYPT_PORT_80_TCP_ADDR/g" /etc/nginx/nginx-secure.conf +sed -i "s/___LETSENCRYPT_PORT___/$LETSENCRYPT_PORT_80_TCP_PORT/g" /etc/nginx/nginx-secure.conf +sed -i "s/___LETSENCRYPT_HTTPS_IP___/$LETSENCRYPT_PORT_443_TCP_ADDR/g" /etc/nginx/nginx-secure.conf +sed -i "s/___LETSENCRYPT_HTTPS_PORT___/$LETSENCRYPT_PORT_443_TCP_PORT/g" /etc/nginx/nginx-secure.conf + +# Add your app's container IP and port into config +sed -i "s/___APPLICATION_IP___/$APP_PORT_80_TCP_ADDR/g" /etc/nginx/nginx-secure.conf +sed -i "s/___APPLICATION_PORT___/$APP_PORT_80_TCP_PORT/g" /etc/nginx/nginx-secure.conf + +#go! +kill $(ps aux | grep 'nginx' | awk '{print $2}') +cp /etc/nginx/nginx-secure.conf /etc/nginx/nginx.conf + +nginx -g 'daemon off;' diff --git a/{{cookiecutter.project_slug}}/docker-compose.yml b/{{cookiecutter.project_slug}}/docker-compose.yml index df7ddb49..a67ec0bb 100644 --- a/{{cookiecutter.project_slug}}/docker-compose.yml +++ b/{{cookiecutter.project_slug}}/docker-compose.yml @@ -27,12 +27,38 @@ services: build: ./compose/nginx depends_on: - django +{% if cookiecutter.use_certbot == 'y' %} + - certbot +{% endif %} ports: - "0.0.0.0:80:80" +{% if cookiecutter.use_certbot == 'y' %} + environment: + - MY_DOMAIN_NAME={{ cookiecutter.domain_name }} + ports: + - "0.0.0.0:80:80" + - "0.0.0.0:443:443" + volumes: + - /etc/letsencrypt:/etc/letsencrypt + - /var/lib/letsencrypt:/var/lib/letsencrypt + + certbot: + image: quay.io/letsencrypt/letsencrypt + command: bash -c "sleep 6 && certbot certonly -n --standalone -d {{ cookiecutter.domain_name }} --text --agree-tos --email {{ cookiecutter.email }} --server https://acme-v01.api.letsencrypt.org/directory --rsa-key-size 4096 --verbose --keep-until-expiring --standalone-supported-challenges http-01" + entrypoint: "" + volumes: + - /etc/letsencrypt:/etc/letsencrypt + - /var/lib/letsencrypt:/var/lib/letsencrypt + ports: + - "80" + - "443" + environment: + - TERM=xterm +{% endif %} redis: image: redis:3.0 - {% if cookiecutter.use_celery == 'y' %} +{% if cookiecutter.use_celery == 'y' %} celeryworker: build: context: . @@ -54,4 +80,4 @@ services: - postgres - redis command: celery -A {{cookiecutter.project_slug}}.taskapp beat -l INFO - {% endif %} +{% endif %}