diff --git a/Dockerfile b/Dockerfile index 1f46676..57366b4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,8 +11,8 @@ RUN apt-get update \ # Install Consul # Releases at https://releases.hashicorp.com/consul -RUN export CONSUL_VERSION=0.6.4 \ - && export CONSUL_CHECKSUM=abdf0e1856292468e2c9971420d73b805e93888e006c76324ae39416edcf0627 \ +RUN export CONSUL_VERSION=0.7.0 \ + && export CONSUL_CHECKSUM=b350591af10d7d23514ebaa0565638539900cdb3aaa048f077217c4c46653dd8 \ && curl --retry 7 --fail -vo /tmp/consul.zip "https://releases.hashicorp.com/consul/${CONSUL_VERSION}/consul_${CONSUL_VERSION}_linux_amd64.zip" \ && echo "${CONSUL_CHECKSUM} /tmp/consul.zip" | sha256sum -c \ && unzip /tmp/consul -d /usr/local/bin \ @@ -43,10 +43,31 @@ RUN export CONTAINERPILOT_CHECKSUM=ec9dbedaca9f4a7a50762f50768cbc42879c7208 \ && tar zxf /tmp/containerpilot.tar.gz -C /usr/local/bin \ && rm /tmp/containerpilot.tar.gz +# Add Dehydrated +RUN export DEHYDRATED_VERSION=v0.3.1 \ + && curl --retry 8 --fail -Lso /tmp/dehydrated.tar.gz "https://github.com/lukas2511/dehydrated/archive/${DEHYDRATED_VERSION}.tar.gz" \ + && tar xzf /tmp/dehydrated.tar.gz -C /tmp \ + && mv /tmp/dehydrated-0.3.1/dehydrated /usr/local/bin \ + && rm -rf /tmp/dehydrated-0.3.1 + +# Add jq +RUN export JQ_VERSION=1.5 \ + && curl --retry 8 --fail -Lso /usr/local/bin/jq "https://github.com/stedolan/jq/releases/download/jq-${JQ_VERSION}/jq-linux64" \ + && chmod a+x /usr/local/bin/jq + # Add our configuration files and scripts COPY etc /etc COPY bin /usr/local/bin +# Usable SSL certs written here +RUN mkdir -p /var/www/ssl +# Temporary/work space for keys +RUN mkdir -p /var/www/acme/ssl +# ACME challenge tokens written here +RUN mkdir -p /var/www/acme/challenge +# Consul session data written here +RUN mkdir -p /var/consul + CMD [ "/usr/local/bin/containerpilot", \ "nginx", \ "-g", \ diff --git a/README.md b/README.md index 70ed823..0a66ccf 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ Autopilot Pattern Nginx -========== +======================= *A re-usable Nginx base image implemented according to the [Autopilot Pattern](http://autopilotpattern.io/) for automatic discovery and configuration.* @@ -39,3 +39,32 @@ You can open the demo app that Nginx is proxying by opening a browser to the Ngi ```bash open "http://$(triton ip nginx_nginx_1)/example" ``` + +### Configuring LetsEncrypt (ACME) + +Setting the `ACME_DOMAIN` environment variable will enable LetsEncrypt within the image. The image will automatically acquire certificates for the given domain, and renew them over time. If you scale to multiple instances of Nginx, they will elect a leader who will be responsible for renewing the certificates. Any challenge response tokens as well as acquired certificates will be replicated to all Nginx instances. + +By default, this process will use the LetsEncrypt staging endpoint, so as not to impact your api limits. When ready for production, you must also set the `ACME_ENV` environment variable to `production`. + +You must ensure the domain resolves to your Nginx containers so that they can respond to the ACME http challenges. Triton users may [refer to this document](https://docs.joyent.com/public-cloud/network/cns/faq#can-i-use-my-own-domain-name-with-triton-cns) for more information on how to insure your domain resolves to your Triton containers. + +Example excerpt from `docker-compose.yml` with LetsEncrypt enabled: + +```yaml +nginx: + image: autopilotpattern/nginx + restart: always + mem_limit: 512m + env_file: _env + environment: + - BACKEND=example + - CONSUL_AGENT=1 + - ACME_ENV=staging + - ACME_DOMAIN=example.com + ports: + - 80 + - 443 + - 9090 + labels: + - triton.cns.services=nginx +``` diff --git a/bin/acme b/bin/acme new file mode 100755 index 0000000..fe9bbc6 --- /dev/null +++ b/bin/acme @@ -0,0 +1,164 @@ +#!/usr/bin/env bash + +pushd `dirname $0` > /dev/null +SCRIPTPATH=`pwd -P` +popd > /dev/null + +CONSUL_HOST_DEFAULT="localhost" +if [ "${CONSUL_AGENT}" = "" -a "${CONSUL}" != "" ]; then + CONSUL_HOST_DEFAULT=${CONSUL} +fi +CONSUL_HOST=${CONSUL_HOST:-$CONSUL_HOST_DEFAULT} +CONSUL_ROOT="http://${CONSUL_HOST}:8500/v1" +CONSUL_KEY_ROOT="${CONSUL_ROOT}/kv/nginx" + +SESSION_DIR_DEFAULT="/var/consul" +SESSION_DIR=${SESSION_DIR:-$SESSION_DIR_DEFAULT} +SESSION_FILE=${SESSION_DIR}/session + +TEMP_CERT_DIR="/var/www/acme/ssl" +CERT_DIR="/var/www/ssl" + +ACME_ENV=${ACME_ENV:-staging} + +function getConsulSession () { + if [ -f $SESSION_FILE ]; then + SID=$(cat ${SESSION_DIR}/session) + local STATUS=$(curl -s ${CONSUL_ROOT}/session/info/${SID}) + if [ "${STATUS}" != "[]" ]; then + echo $SID + return 0 + else + return 1 + fi + else + return 1 + fi +} + +function renewConsulSession () { + local SID="$(getConsulSession)" + rc=$? + if [ $rc -ne 0 ]; then + createConsulSession + return $? + else + printf "Renewing Consul session ${SID}... " + local STATUS=$(curl -s -o /dev/null -X PUT -w '%{http_code}' ${CONSUL_ROOT}/session/renew/${SID}) + if [ "${STATUS}" = "200" ]; then + echo "complete" + return 0 + else + echo "failed" + createConsulSession + return $? + fi + fi +} + +function createConsulSession () { + printf "Creating Consul session... " + local SID=$(curl -sX PUT -d '{"LockDelay":"0s","Name":"acme-lock","Behavior":"release","TTL":"600s"}' ${CONSUL_ROOT}/session/create | awk -F '"' '{print $4}') + rc=$? + if [[ $rc -ne 0 ]]; then + echo "failed" + return 1 + else + echo $SID + echo $SID > $SESSION_FILE + return 0 + fi +} + +function acquireLeader () { + local SID="$(getConsulSession)" + STATUS=$(curl -sX PUT -d "$(hostname)" "${CONSUL_KEY_ROOT}/acme/leader?acquire=${SID}") + + if [ "${STATUS}" = "true" ]; then + echo "ACME leader claimed" + return 0 + else + echo "Failed to claim ACME leader" + return 1 + fi +} + +function generateChallengeToken () { + IN=$1 + OUT=$2 + FILENAME="$(sed '1q;d' ${IN})" + VALUE="$(sed '2q;d' ${IN})" + LAST_FILENAME="$(sed '3q;d' ${IN})" + + if [ "${FILENAME}" ]; then + echo "${VALUE}" > ${OUT}/${FILENAME} + fi + + if [ "${LAST_FILENAME}" ]; then + rm -f ${OUT}/${LAST_FILENAME} + fi + + rm ${IN} +} + +function updateKeys () { + local TEMP_FULLCHAIN="${TEMP_CERT_DIR}/fullchain.pem" + local TEMP_PRIVKEY="${TEMP_CERT_DIR}/privkey.pem" + local FULLCHAIN="${CERT_DIR}/fullchain.pem" + local PRIVKEY="${CERT_DIR}/privkey.pem" + if [ -f ${TEMP_FULLCHAIN} -a -f ${TEMP_PRIVKEY} -a "$(cat ${TEMP_FULLCHAIN})" != "" -a "$(cat ${TEMP_PRIVKEY})" != "" ]; then + cp -f $TEMP_FULLCHAIN $FULLCHAIN + cp -f $TEMP_PRIVKEY $PRIVKEY + $SCRIPTPATH/reload.sh + fi +} + +case "$1" in + get-consul-session) + getConsulSession + ;; + create-consul-session) + createConsulSession + ;; + renew-consul-session) + renewConsulSession + ;; + acquire-leader) + acquireLeader + ;; + checkin) + renewConsulSession && + ( acquireLeader || exit 0 ) + ;; + init) + if [ -f ${CERT_DIR}/fullchain.pem -a -f ${CERT_DIR}/privkey.pem ]; then + exit 0 + fi + shift + renewConsulSession && + acquireLeader && + ${SCRIPTPATH}/dehydrated --cron --domain ${ACME_DOMAIN} --hook /etc/acme/dehydrated/hook.sh --config /etc/acme/dehydrated/config.${ACME_ENV} + ;; + renew-certs) + shift + renewConsulSession && + acquireLeader && + ${SCRIPTPATH}/dehydrated --cron --domain ${ACME_DOMAIN} --hook /etc/acme/dehydrated/hook.sh --config /etc/acme/dehydrated/config.${ACME_ENV} + ;; + clean-certs) + shift + renewConsulSession && + acquireLeader && + ${SCRIPTPATH}/dehydrated --cleanup --domain ${ACME_DOMAIN} --hook /etc/acme/dehydrated/hook.sh --config /etc/acme/dehydrated/config.${ACME_ENV} + ;; + generate-challenge-token) + generateChallengeToken $2 $3 + ;; + update-keys) + updateKeys + ;; + *) + echo $"Usage: $0 [ {get,create,renew}-consul-session | acquire-leader | init | checkin | renew-certs | clean-certs | generate-challenge-token ]" + exit 1 + ;; +esac diff --git a/bin/reload.sh b/bin/reload.sh index 9ef8bff..2b033c9 100755 --- a/bin/reload.sh +++ b/bin/reload.sh @@ -2,6 +2,7 @@ SERVICE_NAME=${SERVICE_NAME:-nginx} CONSUL=${CONSUL:-consul} +CERT_DIR="/var/www/ssl" # Render Nginx configuration template using values from Consul, # but do not reload because Nginx has't started yet @@ -16,11 +17,15 @@ preStart() { # Render Nginx configuration template using values from Consul, # then gracefully reload Nginx onChange() { + local TEMPLATE="nginx.conf.ctmpl" + if [ -f ${CERT_DIR}/fullchain.pem -a -f ${CERT_DIR}/privkey.pem ]; then + TEMPLATE="nginx-ssl.conf.ctmpl" + fi consul-template \ -once \ -dedup \ -consul ${CONSUL}:8500 \ - -template "/etc/nginx/nginx.conf.ctmpl:/etc/nginx/nginx.conf:nginx -s reload" + -template "/etc/nginx/${TEMPLATE}:/etc/nginx/nginx.conf:nginx -s reload" } help() { diff --git a/docker-compose.yml b/docker-compose.yml index fd7d3d7..d9d553a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,8 +11,10 @@ nginx: environment: - BACKEND=example - CONSUL_AGENT=1 + - ACME_ENV=staging ports: - 80 + - 443 - 9090 # so we can see telemetry labels: - triton.cns.services=nginx @@ -32,7 +34,7 @@ example: # Start with a single host which will bootstrap the cluster. # In production we'll want to use an HA cluster. consul: - image: progrium/consul:latest + image: consul:v0.7.0 restart: always mem_limit: 128m expose: @@ -48,4 +50,4 @@ consul: - 127.0.0.1 labels: - triton.cns.services=consul - command: -server -bootstrap -ui-dir /ui + command: agent -server -client=0.0.0.0 -bootstrap -ui diff --git a/etc/acme/dehydrated/config.production b/etc/acme/dehydrated/config.production new file mode 100644 index 0000000..9e7a77c --- /dev/null +++ b/etc/acme/dehydrated/config.production @@ -0,0 +1,2 @@ +CA="https://acme-v01.api.letsencrypt.org/directory" +WELLKNOWN="/var/www/acme/challenge" diff --git a/etc/acme/dehydrated/config.staging b/etc/acme/dehydrated/config.staging new file mode 100644 index 0000000..a6d20b5 --- /dev/null +++ b/etc/acme/dehydrated/config.staging @@ -0,0 +1,2 @@ +CA="https://acme-staging.api.letsencrypt.org/directory" +WELLKNOWN="/var/www/acme/challenge" diff --git a/etc/acme/dehydrated/hook.sh b/etc/acme/dehydrated/hook.sh new file mode 100755 index 0000000..1f147e7 --- /dev/null +++ b/etc/acme/dehydrated/hook.sh @@ -0,0 +1,82 @@ +#!/usr/bin/env bash +set -o pipefail + +CONSUL_HOST_DEFAULT="localhost" +if [ "${CONSUL_AGENT}" = "" -a "${CONSUL}" != "" ]; then + CONSUL_HOST_DEFAULT=${CONSUL} +fi +CONSUL_HOST=${CONSUL_HOST:-$CONSUL_HOST_DEFAULT} +CONSUL_ROOT="http://${CONSUL_HOST}:8500/v1" +CONSUL_KEY_ROOT="${CONSUL_ROOT}/kv/nginx" +CHALLENGE_PATH="/.well-known/acme-challenge" + +function deploy_challenge { + local DOMAIN="${1}" TOKEN_FILENAME="${2}" TOKEN_VALUE="${3}" + local TOKEN_DEPLOY_RETRY_LIMIT=6 + + (curl -sX PUT -d "${TOKEN_FILENAME}" ${CONSUL_KEY_ROOT}/acme/challenge/token-filename | log \ + && curl -sX PUT -d "${TOKEN_VALUE}" ${CONSUL_KEY_ROOT}/acme/challenge/token-value | log \ + ) || (log "${FUNCNAME} failed"; return 1) + + # verify all nginx containers are responding with challenge before continuing + local NGINX_INSTANCES=$(curl -s "${CONSUL_ROOT}/catalog/service/nginx" | jq -Mr '.[].ServiceAddress') + local NGINX_INSTANCE_COUNT=$(echo "${NGINX_INSTANCES}" | wc -l) + local RETRIES=0 + local MATCHING=0 + printf " + Waiting for challenge to be deployed..." + while [ $RETRIES -lt $TOKEN_DEPLOY_RETRY_LIMIT -a $MATCHING -lt $NGINX_INSTANCE_COUNT ]; do + MATCHING=0 + for NGINX_INSTANCE_HOST in $NGINX_INSTANCES; do + if [ "$(curl -s --header \"HOST: ${DOMAIN}\" http://${NGINX_INSTANCE_HOST}${CHALLENGE_PATH}/${TOKEN_FILENAME})" = "${TOKEN_VALUE}" ]; then + MATCHING=$((MATCHING+1)) + fi + done + RETRIES=$((RETRIES+1)) + printf "." + sleep 2 + done + echo +} + +function clean_challenge { + local DOMAIN="${1}" TOKEN_FILENAME="${2}" TOKEN_VALUE="${3}" + + curl -sX DELETE ${CONSUL_KEY_ROOT}/acme/challenge/token-filename | log \ + && curl -sX DELETE ${CONSUL_KEY_ROOT}/acme/challenge/token-value | log \ + && curl -sX PUT -d "${TOKEN_FILENAME}" ${CONSUL_KEY_ROOT}/acme/challenge/last-token-filename | log \ + && return 0 + log "${FUNCNAME} failed" + return 1 +} + +function deploy_cert { + local DOMAIN="${1}" KEYFILE="${2}" CERTFILE="${3}" FULLCHAINFILE="${4}" CHAINFILE="${5}" TIMESTAMP="${6}" + + curl -sX PUT -d "$(cat ${KEYFILE})" ${CONSUL_KEY_ROOT}/acme/key | log \ + && curl -sX PUT -d "$(cat ${CERTFILE})" ${CONSUL_KEY_ROOT}/acme/cert | log \ + && curl -sX PUT -d "$(cat ${CHAINFILE})" ${CONSUL_KEY_ROOT}/acme/chain | log \ + && curl -sX PUT -d "$(cat ${FULLCHAINFILE})" ${CONSUL_KEY_ROOT}/acme/fullchain | log \ + && curl -sX PUT -d "${TIMESTAMP}" ${CONSUL_KEY_ROOT}/acme/timestamp | log \ + && curl -sX PUT -d "$(date +%s)" ${CONSUL_KEY_ROOT}/acme/touched | log \ + && return 0 + log "${FUNCNAME} failed" + return 1 +} + +function unchanged_cert { + local DOMAIN="${1}" KEYFILE="${2}" CERTFILE="${3}" FULLCHAINFILE="${4}" CHAINFILE="${5}" +} + +function log { + if [ -n "$1" ]; then + IN="$1" + else + read IN + fi + + if [ "${IN}" != "true" ]; then + echo " - ${IN}" + fi +} + +HANDLER=$1; shift; $HANDLER $@ diff --git a/etc/acme/templates/cert.ctmpl b/etc/acme/templates/cert.ctmpl new file mode 100644 index 0000000..46d94d2 --- /dev/null +++ b/etc/acme/templates/cert.ctmpl @@ -0,0 +1 @@ +{{if key "nginx/acme/cert"}}{{key "nginx/acme/cert"}}{{end}} diff --git a/etc/acme/templates/chain.ctmpl b/etc/acme/templates/chain.ctmpl new file mode 100644 index 0000000..c914597 --- /dev/null +++ b/etc/acme/templates/chain.ctmpl @@ -0,0 +1 @@ +{{if key "nginx/acme/chain"}}{{key "nginx/acme/chain"}}{{end}} diff --git a/etc/acme/templates/challenge-token.ctmpl b/etc/acme/templates/challenge-token.ctmpl new file mode 100644 index 0000000..902c5ce --- /dev/null +++ b/etc/acme/templates/challenge-token.ctmpl @@ -0,0 +1,3 @@ +{{if key "nginx/acme/challenge/token-filename"}}{{key "nginx/acme/challenge/token-filename"}}{{end}} +{{if key "nginx/acme/challenge/token-value"}}{{key "nginx/acme/challenge/token-value"}}{{end}} +{{if key "nginx/acme/challenge/last-token-filename"}}{{key "nginx/acme/challenge/last-token-filename"}}{{end}} diff --git a/etc/acme/templates/fullchain.ctmpl b/etc/acme/templates/fullchain.ctmpl new file mode 100644 index 0000000..3a785ff --- /dev/null +++ b/etc/acme/templates/fullchain.ctmpl @@ -0,0 +1 @@ +{{if key "nginx/acme/fullchain"}}{{key "nginx/acme/fullchain"}}{{end}} diff --git a/etc/acme/templates/privkey.ctmpl b/etc/acme/templates/privkey.ctmpl new file mode 100644 index 0000000..0a4a20b --- /dev/null +++ b/etc/acme/templates/privkey.ctmpl @@ -0,0 +1 @@ +{{if key "nginx/acme/key"}}{{key "nginx/acme/key"}}{{end}} diff --git a/etc/acme/watch.hcl b/etc/acme/watch.hcl new file mode 100644 index 0000000..7b09653 --- /dev/null +++ b/etc/acme/watch.hcl @@ -0,0 +1,24 @@ +log_level = "err" +template { + source = "/etc/acme/templates/cert.ctmpl" + destination = "/var/www/acme/ssl/cert.pem" +} +template { + source = "/etc/acme/templates/privkey.ctmpl" + destination = "/var/www/acme/ssl/privkey.pem" + command = "/usr/local/bin/acme update-keys" +} +template { + source = "/etc/acme/templates/fullchain.ctmpl" + destination = "/var/www/acme/ssl/fullchain.pem" + command = "/usr/local/bin/acme update-keys" +} +template { + source = "/etc/acme/templates/chain.ctmpl" + destination = "/var/www/acme/ssl/chain.pem" +} +template { + source = "/etc/acme/templates/challenge-token.ctmpl" + destination = "/var/www/acme/challenge-token" + command = "/usr/local/bin/acme generate-challenge-token /var/www/acme/challenge-token /var/www/acme/challenge" +} diff --git a/etc/containerpilot.json b/etc/containerpilot.json index d1e68c2..8f21a19 100644 --- a/etc/containerpilot.json +++ b/etc/containerpilot.json @@ -18,7 +18,15 @@ "poll": 10, "ttl": 25, "interfaces": ["eth1", "eth0"] - } + }{{ if .ACME_DOMAIN }}, + { + "name": "nginx-public-ssl", + "port": 443, + "health": "/usr/local/bin/acme init && /usr/bin/curl --insecure --fail --silent --show-error --output /dev/null --header \"HOST: {{ .ACME_DOMAIN }}\" https://localhost/nginx-health", + "poll": 10, + "ttl": 25, + "interfaces": ["eth1", "eth0"] + }{{ end }} ], "backends": [ { @@ -37,6 +45,14 @@ "-retry-max", "10", "-retry-interval", "10s"], "restarts": "unlimited" + }{{ end }} + {{ if and .CONSUL_AGENT .ACME_DOMAIN }},{{ end }} + {{ if .ACME_DOMAIN }} + { + "command": ["/usr/local/bin/consul-template", + "-config", "/etc/acme/watch.hcl", + "-consul", "{{ if .CONSUL_AGENT }}localhost{{ else }}{{ .CONSUL }}{{ end }}:8500"], + "restarts": "unlimited" }{{ end }}], "telemetry": { "port": 9090, @@ -56,5 +72,25 @@ "check": ["/usr/local/bin/sensor.sh", "connections_load"] } ] - } + }, + "tasks": [{{ if .ACME_DOMAIN }} + { + "name": "acme-checkin", + "command": [ "/usr/local/bin/acme", "checkin" ], + "frequency": "5m", + "timeout": "10s" + }, + { + "name": "acme-renew-certs", + "command": [ "/usr/local/bin/acme", "renew-certs" ], + "frequency": "12h", + "timeout": "10m" + }, + { + "name": "clean-unused-certs", + "command": ["/usr/local/bin/acme", "clean-certs" ], + "frequency": "24h", + "timeout": "10m" + }{{ end }} + ] } diff --git a/etc/nginx/nginx-ssl.conf.ctmpl b/etc/nginx/nginx-ssl.conf.ctmpl new file mode 100644 index 0000000..4f14528 --- /dev/null +++ b/etc/nginx/nginx-ssl.conf.ctmpl @@ -0,0 +1,100 @@ +# This is an example Nginx configuration template file. +# Adjust the values below as required for your application. + +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; + + map $status $isError { + ~^2 0; + default 1; + } + + 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; + + #gzip on; + + {{ $backend := env "BACKEND" }} + {{ $acme_domain := env "ACME_DOMAIN" }} + {{ if service $backend }} + upstream {{ $backend }} { + # write the address:port pairs for each healthy backend instance + {{range service $backend }} + server {{.Address}}:{{.Port}}; + {{end}} + least_conn; + }{{ end }} + + server { + listen 80; + server_name _; + + location / { + return 301 https://$host$request_uri; + } + + location /nginx-health { + stub_status; + allow 127.0.0.1; + deny all; + access_log /var/log/nginx/access.log main if=$isError; + } + } + + server { + listen 443 ssl; + server_name _; + + location /nginx-health { + stub_status; + allow 127.0.0.1; + deny all; + access_log /var/log/nginx/access.log main if=$isError; + } + + location /.well-known/acme-challenge { + alias /var/www/acme/challenge; + } + + ssl_certificate /var/www/ssl/fullchain.pem; + ssl_certificate_key /var/www/ssl/privkey.pem; + ssl_protocols TLSv1 TLSv1.1 TLSv1.2; + ssl_prefer_server_ciphers on; + 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_session_timeout 1d; + ssl_session_cache shared:SSL:50m; + ssl_stapling on; + ssl_stapling_verify on; + add_header Strict-Transport-Security max-age=15768000; + + {{ if service $backend }} + location = /{{ $backend }} { + return 301 /{{ $backend }}/; + } + + location /{{ $backend }} { + proxy_pass http://{{ $backend }}; + proxy_redirect off; + }{{ end }} + } +} diff --git a/etc/nginx/nginx.conf.ctmpl b/etc/nginx/nginx.conf.ctmpl index eb1c9ba..54add5d 100644 --- a/etc/nginx/nginx.conf.ctmpl +++ b/etc/nginx/nginx.conf.ctmpl @@ -11,11 +11,15 @@ events { worker_connections 1024; } - http { include /etc/nginx/mime.types; default_type application/octet-stream; + map $status $isError { + ~^2 0; + default 1; + } + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; @@ -30,6 +34,7 @@ http { #gzip on; {{ $backend := env "BACKEND" }} + {{ $acme_domain := env "ACME_DOMAIN" }} {{ if service $backend }} upstream {{ $backend }} { # write the address:port pairs for each healthy backend instance @@ -47,7 +52,15 @@ http { stub_status; allow 127.0.0.1; deny all; + access_log /var/log/nginx/access.log main if=$isError; } + + {{ if $acme_domain }} + location /.well-known/acme-challenge { + alias /var/www/acme/challenge; + } + {{ end }} + {{ if service $backend }} location = /{{ $backend }} { return 301 /{{ $backend }}/; diff --git a/example-backend/Dockerfile b/example-backend/Dockerfile index 0d70898..55a0b7f 100755 --- a/example-backend/Dockerfile +++ b/example-backend/Dockerfile @@ -12,9 +12,10 @@ COPY package.json /opt/example/ RUN cd /opt/example && npm install # Add Consul from https://releases.hashicorp.com/consul -RUN export CHECKSUM=abdf0e1856292468e2c9971420d73b805e93888e006c76324ae39416edcf0627 \ - && curl -vo /tmp/consul.zip "https://releases.hashicorp.com/consul/0.6.4/consul_0.6.4_linux_amd64.zip" \ - && echo "${CHECKSUM} /tmp/consul.zip" | sha256sum -c \ +RUN export CONSUL_VERSION=0.7.0 \ + && export CONSUL_CHECKSUM=b350591af10d7d23514ebaa0565638539900cdb3aaa048f077217c4c46653dd8 \ + && curl --retry 7 --fail -vo /tmp/consul.zip "https://releases.hashicorp.com/consul/${CONSUL_VERSION}/consul_${CONSUL_VERSION}_linux_amd64.zip" \ + && echo "${CONSUL_CHECKSUM} /tmp/consul.zip" | sha256sum -c \ && unzip /tmp/consul -d /usr/local/bin \ && rm /tmp/consul.zip \ && mkdir /config diff --git a/local-compose.yml b/local-compose.yml index a0ae00f..211450f 100644 --- a/local-compose.yml +++ b/local-compose.yml @@ -3,14 +3,16 @@ nginx: file: docker-compose.yml service: nginx build: . - mem_limit: 128g + mem_limit: 128m environment: - CONSUL=consul - CONSUL_AGENT=1 + - ACME_ENV=staging links: - consul:consul ports: - 80:80 + - 443:443 - 9090:9090 # telemetry endpoint example: