Big rewrite so that we can enable sites as we get HTTPS certs

This commit is contained in:
Elliot Saba 2017-01-22 22:33:45 -08:00
parent e8f4c77ed7
commit 9f4becca7e
7 changed files with 137 additions and 99 deletions

View File

@ -1,18 +1,22 @@
FROM python:2
FROM nginx
MAINTAINER Elliot Saba <staticfloat@gmail.com>
VOLUME /etc/letsencrypt
EXPOSE 80
EXPOSE 443
RUN apt update && apt install -y cron
RUN pip install certbot
RUN mkdir /scripts
RUN apt update && apt install -y cron python python-dev python-pip libffi-dev libssl-dev
RUN pip install -U cffi certbot
# Copy in cron job and scripts for certbot
COPY ./crontab /etc/cron.d/certbot
RUN crontab /etc/cron.d/certbot
COPY ./scripts/ /scripts
RUN chmod +x /scripts/*.sh
# Copy in default nginx configuration
RUN rm -f /etc/nginx/conf.d/*
COPY nginx_conf.d/ /etc/nginx/conf.d/
ENTRYPOINT []
CMD ["/bin/bash", "/scripts/entrypoint.sh"]

View File

@ -2,6 +2,7 @@ all: build
build: Makefile Dockerfile
docker build --squash -t staticfloat/docker-certbot-cron .
echo "Done! Use docker run staticfloat/docker-certbot-cron to run"
push:
docker push staticfloat/docker-certbot-cron

View File

@ -1,57 +1,5 @@
# docker-certbot-cron
Create and automatically renew website SSL certificates using the letsencrypt free certificate authority, and its client *certbot*. Define the environment variables `DOMAINS` (space-separated list of fully-qualified domain names) and `EMAIL` (your letsencrypt registration email) to automatically run `certbot` to renew/fetch your SSL certificates in the background. Configure `nginx` to pass off the ACME validation challenge, and you'll have zero-downtime, 100% automatic SSL certificates for all your Docker containers!
# ACME Validation challenge
To authenticate the certificates, the you need to pass the ACME validation challenge. This requires requests made on port 80 to your.domain.com/.well-known/ to be forwarded to this container.
The recommended way to use this image is to set up your reverse proxy to automatically forward requests for the ACME validation challenges to this container.
## Nginx example
If you use nginx as a reverse proxy, you can add the following to your configuration file in order to pass the ACME challenge.
``` nginx
server {
listen 80;
location '/.well-known/acme-challenge' {
default_type "text/plain";
# Note: this works with docker-compose only if the service name is `certbot`,
# and the `nginx` service `depends_on` the `certbot` service!
proxy_pass http://certbot:80;
}
}
```
## `docker-compose` example
To use this container with `docker-compose`, put something like the following into your configuration:
```yml
version '2'
services:
...
certbot:
image: staticfloat/docker-certbot-cron
container_name: certbot
volumes:
- certbot_etc_letsencrypt:/etc/letsencrypt
restart: unless-stopped
environment:
- DOMAINS="foo.bar.com baz.bar.com"
- EMAIL=email@domain.com
...
nginx:
...
depends_on:
- certbot
volumes:
- certbot_etc_letsencrypt:/etc/letsencrypt:ro
...
volumes:
certbot_etc_letsencrypt:
external: true
```
I personally like having my certificates stored in an external volume so that if I ever accidentally run `docker-compose down` I don't have to re-issue myself the certificates.
Create and automatically renew website SSL certificates using the letsencrypt free certificate authority, and its client *certbot*.
# More information
@ -59,10 +7,16 @@ Find out more about letsencrypt: https://letsencrypt.org
Certbot github: https://github.com/certbot/certbot
This repository was originally forked from `@henridwyer`, many thanks to him for the good idea. I've basically taken his approach and made it less flexible/simpler for my own use cases, so if you want this repository to do something a particular way, make sure [his repo](https://github.com/henridwyer/docker-letsencrypt-cron) doesn't already do it.
This repository was originally forked from `@henridwyer`, many thanks to him for the good idea. I've rewritten about 90% of this repository, so it bears almost no resemblance to the original. This repository is _much_ more opinionated about the structure of your webservers/code, however it is easier to use as long as all of your webservers follow that pattern.
# Changelog
### 0.7
- Complete rewrite, build this image on top of the `nginx` image, and run `cron` alongside `nginx` so that we can have nginx configs dynamically enabled as we get SSL certificates.
### 0.6
- Add `nginx_auto_enable.sh` script to `/etc/letsencrypt/` so that users can bring nginx up before SSL certs are actually available.
### 0.5
- Change the name to `docker-certbot-cron`, update documentation, strip out even more stuff I don't care about.

15
nginx_conf.d/certbot.conf Normal file
View File

@ -0,0 +1,15 @@
server {
# Listen on plain old HTTP
listen 80;
# Pass this particular URL off to certbot, to authenticate HTTPS certificates
location '/.well-known/acme-challenge' {
default_type "text/plain";
proxy_pass http://localhost:80;
}
# Everything else gets shunted over to HTTPS
location / {
return 301 https://$http_host$request_uri;
}
}

View File

@ -1,6 +1,23 @@
#!/bin/bash
#!/bin/sh
# When we get killed, kill all our children
trap "exit" INT TERM
trap "kill 0" EXIT
/scripts/run_certbot.sh && cron -f &
# Source in util.sh so we can have our nice tools
. $(cd $(dirname $0); pwd)/util.sh
# Immediately run auto_enable_configs so that nginx is in a runnable state
auto_enable_configs
# Start up nginx, save PID so we can reload config inside of run_certbot.sh
nginx -g "daemon off;" &
export NGINX_PID=$!
# Next, run certbot to request all the ssl certs we can find
/scripts/run_certbot.sh
# Run `cron -f &` so that it's a background job owned by bash and then `wait`.
# This allows SIGINT (e.g. CTRL-C) to kill cron gracefully, due to our `trap`.
cron -f &
wait

View File

@ -1,48 +1,30 @@
error() {
(set +x; tput -Tscreen bold
tput -Tscreen setaf 1
echo $*
tput -Tscreen sgr0) >&2
}
#!/bin/sh
if [ -z "$DOMAINS" ]; then
error "DOMAINS environment variable undefined; certbot will do nothing"
# Source in util.sh so we can have our nice tools
. $(cd $(dirname $0); pwd)/util.sh
# We require an email to register the ssl certificate for
if [ -z "$CERTBOT_EMAIL" ]; then
error "CERTBOT_EMAIL environment variable undefined; certbot will do nothing"
exit 1
fi
if [ -z "$EMAIL" ]; then
error "EMAIL environment variable undefined; certbot will do nothing"
exit 1
fi
echo "Running certbot for domains $DOMAINS for user $EMAIL..."
get_certificate() {
# Gets the certificate for the domain(s) CERT_DOMAINS (a comma separated list)
# The certificate will be named after the first domain in the list
# To work, the following variables must be set:
# - CERT_DOMAINS : comma separated list of domains
# - EMAIL
local d=${CERT_DOMAINS//,*/} # read first domain
echo "Getting certificate for $CERT_DOMAINS"
certbot certonly --agree-tos --keep -n --text --email $EMAIL --server \
https://acme-v01.api.letsencrypt.org/directory -d $CERT_DOMAINS \
--standalone --standalone-supported-challenges http-01 --debug
ec=$?
echo "certbot exit code $ec"
if [ $ec -eq 0 ]; then
error "Certificates for $CERT_DOMAINS can be found in /etc/letsencrypt/live/$d"
else
error "Cerbot failed for $CERT_DOMAINS. Check the logs for details."
exit 1
fi
}
exit_code=0
set -x
for d in $DOMAINS; do
CERT_DOMAINS=$d
if ! get_certificate; then
# Loop over every domain we can find
for domain in $(parse_domains); do
if ! get_certificate $domain $CERTBOT_EMAIL; then
error "Cerbot failed for $domain. Check the logs for details."
exit_code=1
fi
done
# After trying to get all our certificates, auto enable any configs that we
# did indeed get certificates for
auto_enable_configs
# Finally, tell nginx to reload the configs
kill -HUP $NGINX_PID
set +x
exit $exit_code

65
scripts/util.sh Normal file
View File

@ -0,0 +1,65 @@
#!/bin/sh
# Helper function to output error messages to STDERR, with red text
error() {
(set +x; tput -Tscreen bold
tput -Tscreen setaf 1
echo $*
tput -Tscreen sgr0) >&2
}
# Helper function that sifts through /etc/nginx/conf.d/, looking for lines that
# contain ssl_certificate_key, and try to find domain names in them. We accept
# a very restricted set of keys: Each key must map to a set of concrete domains
# (no wildcards) and each keyfile will be stored at the default location of
# /etc/letsencrypt/live/<primary_domain_name>/privkey.pem
parse_domains() {
# For each configuration file in /etc/nginx/conf.d/*.conf*
for conf_file in /etc/nginx/conf.d/*.conf*; do
sed -n -e 's/^\s*ssl_certificate_key\s*\/etc/letsencrypt/live/(.*\)/privkey.pem;/\1/p' $conf_file | tr '\n' ','
done
}
# Given a config file path, spit out all the ssl_certificate_key file paths
parse_keyfiles() {
sed -n -e 's/^\s*ssl_certificate_key\s*\(.*\);/\1/p' "$1"
}
# Given a config file path, return 0 if all keyfiles exist (or there are no
# keyfiles), return 1 otherwise
keyfiles_exist() {
for keyfile in $(parse_keyfiles $1); do
if [ ! -f $keyfile ]; then
echo "Couldn't find keyfile $keyfile for $1"
return 1
fi
done
return 0
}
# Helper function that sifts through /etc/nginx/conf.d/, looking for configs
# that don't have their keyfiles yet, and disabling them through renaming
auto_enable_configs() {
for conf_file in /etc/nginx/conf.d/*.conf*; do
if ! keyfiles_exist $conf_file; then
if [ $conf_file == *.nokey ]; then
echo "Found all the keyfiles for $conf_file, enabling..."
mv $conf_file ${conf_file%.*}
fi
else
if [ $conf_file == *.conf ]; then
echo "Keyfile(s) missing for $conf_file, disabling..."
mv $conf_file $conf_file.nokey
fi
fi
done
}
# Helper function to ask certbot for the given domain(s). Must have defined the
# EMAIL environment variable, to register the proper support email address.
get_certificate() {
echo "Getting certificate for domain $1 on behalf of user $2"
return certbot certonly --agree-tos --keep -n --text --email $2 --server \
https://acme-v01.api.letsencrypt.org/directory -d $1 \
--standalone --standalone-supported-challenges http-01 --debug
}