Browse Source

added lets encrypt

dirkse 9 years ago
parent
commit
c3aa1d3047

+ 1 - 0
home.bat

@@ -1,3 +1,4 @@
 @echo off
 cd %~dp0
 d:\vls-trunk\env-win64\python27\python.exe -m auto.cli %*
+pause

+ 42 - 0
ngp_le/Dockerfile

@@ -0,0 +1,42 @@
+FROM hypriot/rpi-alpine-scratch
+
+RUN apk update \
+    && apk upgrade
+
+RUN apk add bash python git gcc musl-dev libffi-dev python-dev openssl-dev
+
+ENV DOCKER_GEN_VERSION=0.7.3 \
+    DOCKER_HOST=unix:///var/run/docker.sock
+
+RUN wget https://github.com/jwilder/docker-gen/releases/download/$DOCKER_GEN_VERSION/docker-gen-linux-armhf-$DOCKER_GEN_VERSION.tar.gz \
+ && tar -C /usr/local/bin -xvzf docker-gen-linux-armhf-$DOCKER_GEN_VERSION.tar.gz \
+ && rm /docker-gen-linux-armhf-$DOCKER_GEN_VERSION.tar.gz
+
+
+WORKDIR /app
+
+# Install simp_le program
+RUN python -m ensurepip --upgrade
+RUN pip install requests
+
+# Get Let's Encrypt simp_le client source
+RUN mkdir -p /src
+RUN git -C /src clone https://github.com/kuba/simp_le.git
+
+# Install simp_le in /usr/bin
+RUN cd /src/simp_le && \
+    python ./setup.py install
+
+# Make house cleaning
+RUN cd / && \
+    rm -rf /src && \
+    apk del git gcc py-pip musl-dev libffi-dev python-dev openssl-dev && \
+    rm -rf /var/cache/apk/*
+
+# used in entrypoint.sh
+ENV debug=false
+
+ENTRYPOINT ["/bin/bash", "/app/entrypoint.sh" ]
+CMD ["/bin/bash", "/app/start.sh"]
+
+COPY /app/ /app/

+ 86 - 0
ngp_le/app/entrypoint.sh

@@ -0,0 +1,86 @@
+#!/bin/bash
+
+set -u
+
+export CONTAINER_ID=$(cat /proc/self/cgroup | sed -nE 's/^.+docker[\/-]([a-f0-9]{64}).*/\1/p' | head -n 1)
+
+if [[ -z "$CONTAINER_ID" ]]; then
+    echo "Error: can't get my container ID !" >&2
+    exit 1
+fi
+
+function check_docker_socket {
+    if [[ $DOCKER_HOST == unix://* ]]; then
+        socket_file=${DOCKER_HOST#unix://}
+        if [[ ! -S $socket_file ]]; then
+            cat >&2 <<-EOT
+ERROR: you need to share your Docker host socket with a volume at $socket_file
+Typically you should run your container with: \`-v /var/run/docker.sock:$socket_file:ro\`
+See the documentation at http://git.io/vZaGJ
+EOT
+            exit 1
+        fi
+    fi
+}
+
+function get_nginx_proxy_cid {
+    # Look for a NGINX_VERSION environment variable in containers that we have mount volumes from.
+    local volumes_from=$(docker_api "/containers/$CONTAINER_ID/json" | jq -r '.HostConfig.VolumesFrom[]' 2>/dev/null)
+    for cid in $volumes_from; do
+        cid=${cid%:*} # Remove leading :ro or :rw set by remote docker-compose (thx anoopr)
+        if [[ $(docker_api "/containers/$cid/json" | jq -r '.Config.Env[]' | egrep -c '^NGINX_VERSION=') = "1" ]];then
+            export NGINX_PROXY_CONTAINER=$cid
+            break
+        fi
+    done
+    if [[ -z "${NGINX_PROXY_CONTAINER:-}" ]]; then
+        echo "Error: can't get nginx-proxy container id !" >&2
+        echo "Check that you use the --volumes-from option to mount volumes from the nginx-proxy." >&2
+        exit 1
+    fi
+}
+
+function check_writable_directory {
+    local dir="$1"
+    docker_api "/containers/$HOSTNAME/json" | jq ".Mounts[].Destination" | grep -q "^\"$dir\"$"
+    if [[ $? -ne 0 ]]; then
+        echo "Warning: '$dir' does not appear to be a mounted volume."
+    fi
+    if [[ ! -d "$dir" ]]; then
+        echo "Error: can't access to '$dir' directory !" >&2
+        echo "Check that '$dir' directory is declared has a writable volume." >&2
+        exit 1
+    fi
+    touch $dir/.check_writable 2>/dev/null
+    if [[ $? -ne 0 ]]; then
+        echo "Error: can't write to the '$dir' directory !" >&2
+        echo "Check that '$dir' directory is export as a writable volume." >&2
+        exit 1
+    fi
+    rm -f $dir/.check_writable
+}
+
+function check_dh_group {
+    if [[ ! -f /etc/nginx/certs/dhparam.pem ]]; then
+        echo "Creating Diffie-Hellman group (can take several minutes...)"
+        openssl dhparam -out /etc/nginx/certs/.dhparam.pem.tmp 2048
+        mv /etc/nginx/certs/.dhparam.pem.tmp /etc/nginx/certs/dhparam.pem || exit 1
+    fi
+}
+
+source /app/functions.sh
+
+[[ $DEBUG == true ]] && set -x
+
+if [[ "$*" == "/bin/bash /app/start.sh" ]]; then
+    check_docker_socket
+    if [[ -z "${NGINX_DOCKER_GEN_CONTAINER:-}" ]]; then
+        [[ -z "${NGINX_PROXY_CONTAINER:-}" ]] && get_nginx_proxy_cid
+    fi
+    check_writable_directory '/etc/nginx/certs'
+    check_writable_directory '/etc/nginx/vhost.d'
+    check_writable_directory '/usr/share/nginx/html'
+    check_dh_group
+fi
+
+exec "$@"

+ 88 - 0
ngp_le/app/functions.sh

@@ -0,0 +1,88 @@
+[[ -z "${VHOST_DIR:-}" ]] && \
+ declare -r VHOST_DIR=/etc/nginx/vhost.d
+[[ -z "${START_HEADER:-}" ]] && \
+ declare -r START_HEADER='## Start of configuration add by letsencrypt container'
+[[ -z "${END_HEADER:-}" ]] && \
+ declare -r END_HEADER='## End of configuration add by letsencrypt container'
+
+add_location_configuration() {
+    local domain="${1:-}"
+    [[ -z "$domain" || ! -f "${VHOST_DIR}/${domain}" ]] && domain=default
+    [[ -f "${VHOST_DIR}/${domain}" && \
+       -n $(sed -n "/$START_HEADER/,/$END_HEADER/p" "${VHOST_DIR}/${domain}") ]] && return 0
+    echo "$START_HEADER" > "${VHOST_DIR}/${domain}".new
+    cat /app/nginx_location.conf >> "${VHOST_DIR}/${domain}".new
+    echo "$END_HEADER" >> "${VHOST_DIR}/${domain}".new
+    [[ -f "${VHOST_DIR}/${domain}" ]] && cat "${VHOST_DIR}/${domain}" >> "${VHOST_DIR}/${domain}".new
+    mv -f "${VHOST_DIR}/${domain}".new "${VHOST_DIR}/${domain}"
+    return 1
+}
+
+remove_all_location_configurations() {
+    local old_shopt_options=$(shopt -p) # Backup shopt options
+    shopt -s nullglob
+    for file in "${VHOST_DIR}"/*; do
+        [[ -n $(sed -n "/$START_HEADER/,/$END_HEADER/p" "$file") ]] && \
+         sed -i "/$START_HEADER/,/$END_HEADER/d" "$file"
+    done
+    eval "$old_shopt_options" # Restore shopt options
+}
+
+## Docker API
+function docker_api {
+    local scheme
+    local curl_opts=(-s)
+    local method=${2:-GET}
+    # data to POST
+    if [[ -n "${3:-}" ]]; then
+        curl_opts+=(-d "$3")
+    fi
+    if [[ -z "$DOCKER_HOST" ]];then
+        echo "Error DOCKER_HOST variable not set" >&2
+        return 1
+    fi
+    if [[ $DOCKER_HOST == unix://* ]]; then
+        curl_opts+=(--unix-socket ${DOCKER_HOST#unix://})
+        scheme='http:'
+    else
+        scheme="http://${DOCKER_HOST#*://}"
+    fi
+    [[ $method = "POST" ]] && curl_opts+=(-H 'Content-Type: application/json')
+    curl "${curl_opts[@]}" -X${method} ${scheme}$1
+}
+
+function docker_exec {
+    local id="${1?missing id}"
+    local cmd="${2?missing command}"
+    local data=$(printf '{ "AttachStdin": false, "AttachStdout": true, "AttachStderr": true, "Tty":false,"Cmd": %s }' "$cmd")
+    exec_id=$(docker_api "/containers/$id/exec" "POST" "$data" | jq -r .Id)
+    if [[ -n "$exec_id" ]]; then
+        docker_api /exec/$exec_id/start "POST" '{"Detach": false, "Tty":false}'
+    fi
+}
+
+function docker_kill {
+    local id="${1?missing id}"
+    local signal="${2?missing signal}"
+    docker_api "/containers/$id/kill?signal=$signal" "POST"
+}
+
+## Nginx
+reload_nginx() {
+    if [[ -n "${NGINX_DOCKER_GEN_CONTAINER:-}" ]]; then
+        # Using docker-gen separate container
+        echo "Reloading nginx proxy (using separate container ${NGINX_DOCKER_GEN_CONTAINER})..."
+        docker_kill "$NGINX_DOCKER_GEN_CONTAINER" SIGHUP
+    else
+        if [[ -n "${NGINX_PROXY_CONTAINER:-}" ]]; then
+            echo "Reloading nginx proxy..."
+            docker_exec "$NGINX_PROXY_CONTAINER" \
+                        '[ "sh", "-c", "/usr/local/bin/docker-gen -only-exposed /app/nginx.tmpl /etc/nginx/conf.d/default.conf; /usr/sbin/nginx -s reload" ]'
+        fi
+    fi
+}
+
+# Convert argument to lowercase (bash 4 only)
+function lc() {
+	echo "${@,,}"
+}

+ 135 - 0
ngp_le/app/letsencrypt_service

@@ -0,0 +1,135 @@
+#!/bin/bash
+
+DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+
+seconds_to_wait=3600
+ACME_CA_URI="${ACME_CA_URI:-https://acme-v01.api.letsencrypt.org/directory}"
+
+source /app/functions.sh
+
+create_link() {
+    local readonly target=${1?missing target argument}
+    local readonly source=${2?missing source argument}
+    [[ -f "$target" ]] && return 1
+    ln -sf "$source" "$target"
+}
+
+create_links() {
+    local readonly base_domain=${1?missing base_domain argument}
+    local readonly domain=${2?missing base_domain argument}
+
+    if [[ ! -f "/etc/nginx/certs/$base_domain"/fullchain.pem || \
+          ! -f "/etc/nginx/certs/$base_domain"/key.pem ]]; then
+        return 1
+    fi
+    local return_code=1
+    create_link "/etc/nginx/certs/$domain".crt "./$base_domain"/fullchain.pem
+    return_code=$(( $return_code & $? ))
+    create_link "/etc/nginx/certs/$domain".key "./$base_domain"/key.pem
+    return_code=$(( $return_code & $? ))
+    if [[ -f "/etc/nginx/certs/dhparam.pem" ]]; then
+        create_link "/etc/nginx/certs/$domain".dhparam.pem ./dhparam.pem
+        return_code=$(( $return_code & $? ))
+    fi
+    return $return_code
+}
+
+update_certs() {
+    [[ ! -f "$DIR"/letsencrypt_service_data ]] && return
+
+    # Load relevant container settings
+    unset LETSENCRYPT_CONTAINERS
+    source "$DIR"/letsencrypt_service_data
+
+    reload_nginx='false'
+    for cid in "${LETSENCRYPT_CONTAINERS[@]}"; do
+        # Derive host and email variable names
+        host_varname="LETSENCRYPT_${cid}_HOST"
+        # Array variable indirection hack: http://stackoverflow.com/a/25880676/350221
+        hosts_array=$host_varname[@]
+        email_varname="LETSENCRYPT_${cid}_EMAIL"
+
+        test_certificate_varname="LETSENCRYPT_${cid}_TEST"
+        create_test_certificate=false
+        if [[ $(lc "${!test_certificate_varname:-}") == true ]]; then
+            create_test_certificate=true
+        fi
+
+        params_d_str=""
+        [[ $DEBUG == true ]] && params_d_str+=" -v"
+
+        hosts_array_expanded=("${!hosts_array}")
+        # First domain will be our base domain
+        base_domain="${hosts_array_expanded[0]}"
+
+        if [[ "$create_test_certificate" == true ]]; then
+            # Use staging acme end point
+            acme_ca_uri="https://acme-staging.api.letsencrypt.org/directory"
+            if [[ ! -f /etc/nginx/certs/.${base_domain}.test ]]; then
+                # Remove old certificates
+                rm -rf /etc/nginx/certs/${base_domain}
+                for domain in "${!hosts_array}"; do
+                    rm -f /etc/nginx/certs/$domain.{crt,key,dhparam.pem}
+                done
+                touch /etc/nginx/certs/.${base_domain}.test
+            fi
+        else
+            acme_ca_uri="$ACME_CA_URI"
+            if [[ -f /etc/nginx/certs/.${base_domain}.test ]]; then
+                # Remove old test certificates
+                rm -rf /etc/nginx/certs/${base_domain}
+                for domain in "${!hosts_array}"; do
+                    rm -f /etc/nginx/certs/$domain.{crt,key,dhparam.pem}
+                done
+                rm -f /etc/nginx/certs/.${base_domain}.test
+            fi
+        fi
+
+        # Create directory for the first domain
+        mkdir -p /etc/nginx/certs/$base_domain
+        cd /etc/nginx/certs/$base_domain
+
+        for domain in "${!hosts_array}"; do
+            # Add all the domains to certificate
+            params_d_str+=" -d $domain"
+            # Add location configuration for the domain
+            add_location_configuration "$domain" || reload_nginx
+        done
+
+        echo "Creating/renewal $base_domain certificates... (${hosts_array_expanded[*]})"
+        /usr/bin/simp_le \
+            -f account_key.json -f key.pem -f fullchain.pem -f cert.pem \
+            --tos_sha256 6373439b9f29d67a5cd4d18cbc7f264809342dbf21cb2ba2fc7588df987a6221 \
+            $params_d_str \
+            --email "${!email_varname}" \
+            --server=$acme_ca_uri \
+            --default_root /usr/share/nginx/html/
+
+        simp_le_return=$?
+
+        for altnames in ${hosts_array_expanded[@]:1}; do
+            # Remove old CN domain that now are altnames
+            rm -rf /etc/nginx/certs/$altnames
+        done
+
+        for domain in "${!hosts_array}"; do
+            create_links $base_domain $domain && reload_nginx='true'
+            [[ $simp_le_return -eq 0 ]] && reload_nginx='true'
+        done
+    done
+
+    [[ "$reload_nginx" == 'true' ]] && reload_nginx
+}
+
+pid=
+# Service Loop: When this script exits, start it again.
+trap '[[ $pid ]] && kill $pid; exec $0' EXIT
+trap 'trap - EXIT' INT TERM
+
+update_certs
+
+# Wait some amount of time
+echo "Sleep for ${seconds_to_wait}s"
+sleep $seconds_to_wait & pid=$!
+wait
+pid=

+ 11 - 0
ngp_le/app/letsencrypt_service_data.tmpl

@@ -0,0 +1,11 @@
+LETSENCRYPT_CONTAINERS=({{ range $host, $containers := groupBy $ "Env.LETSENCRYPT_HOST" }}{{ range $container := $containers }} '{{ printf "%.12s" $container.ID }}' {{ end }}{{ end }})
+
+{{ range $hosts, $containers := groupBy $ "Env.LETSENCRYPT_HOST" }}
+
+{{ range $container := $containers }}{{ $cid := printf "%.12s" $container.ID }}
+LETSENCRYPT_{{ $cid }}_HOST=( {{ range $host := split $hosts "," }}'{{ $host }}' {{ end }})
+LETSENCRYPT_{{ $cid }}_EMAIL="{{ $container.Env.LETSENCRYPT_EMAIL }}"
+LETSENCRYPT_{{ $cid }}_TEST="{{ $container.Env.LETSENCRYPT_TEST }}"
+{{ end }}
+
+{{ end }}

+ 6 - 0
ngp_le/app/nginx_location.conf

@@ -0,0 +1,6 @@
+location /.well-known/acme-challenge/ {
+    allow all;
+    root /usr/share/nginx/html;
+    try_files $uri =404;
+    break;
+}

+ 28 - 0
ngp_le/app/start.sh

@@ -0,0 +1,28 @@
+#!/bin/bash
+
+# SIGTERM-handler
+term_handler() {
+    [[ -n "$docker_gen_pid" ]] && kill $docker_gen_pid
+    [[ -n "$letsencrypt_service_pid" ]] && kill $letsencrypt_service_pid
+
+    source /app/functions.sh
+    remove_all_location_configurations
+
+    exit 143; # 128 + 15 -- SIGTERM
+}
+
+trap 'term_handler' INT QUIT KILL TERM
+
+/app/letsencrypt_service &
+letsencrypt_service_pid=$!
+
+docker-gen -watch -only-exposed -notify '/app/update_certs' -wait 15s:60s /app/letsencrypt_service_data.tmpl /app/letsencrypt_service_data &
+docker_gen_pid=$!
+
+# wait "indefinitely"
+while [[ -e /proc/$docker_gen_pid ]]; do
+    wait $docker_gen_pid # Wait for any signals or end of execution of docker-gen
+done
+
+# Stop container properly
+term_handler

+ 3 - 0
ngp_le/app/update_certs

@@ -0,0 +1,3 @@
+#!/bin/bash
+
+pkill -f -SIGUSR1 /app/letsencrypt_service

+ 21 - 13
services.yml

@@ -25,20 +25,28 @@ svn:
 ngp:
   host: himbeere
   docker:
-    build: ngp
-    ports: 0.0.0.0:1080:80, 0.0.0.0:1443:443
-    volumes:
-      - /usr/share/nginx/html
-      - /var/run/docker.sock:/tmp/docker.sock:ro
-      - /opt/fast/nginx/certs:/etc/nginx/certs:ro
-      # see https://github.com/jwilder/nginx-proxy#per-virtual_host
-      - /opt/fast/nginx/vhost.d:/etc/nginx/vhost.d:ro
+    - name: ngp
+      build: ngp
+      ports: 0.0.0.0:1080:80, 0.0.0.0:1443:443
+      volumes:
+        - /usr/share/nginx/html
+        - /var/run/docker.sock:/tmp/docker.sock:ro
+        - /opt/fast/nginx/certs:/etc/nginx/certs:ro
+        # see https://github.com/jwilder/nginx-proxy#per-virtual_host
+        - /opt/fast/nginx/vhost.d:/etc/nginx/vhost.d:ro
 
-    commands:
-      nginx_conf:
-        method: exec
-        cmd: cat /etc/nginx/conf.d/default.conf
-        docs: show dockergen-generated config
+      commands:
+        nginx_conf:
+          method: exec
+          cmd: cat /etc/nginx/conf.d/default.conf
+          docs: show dockergen-generated config
+    - name: ngp_le
+      build: ngp_le
+      volumes_from: ngp
+      volumes:
+        - /opt/fast/nginx/certs:/etc/nginx/certs:rw
+        - /var/run/docker.sock:/var/run/docker.sock:ro
+        - /opt/fast/nginx/vhost.d:/etc/nginx/vhost.d:rw
 
 nexus:
   host: himbeere