Selaa lähdekoodia

upped to acme v2

cc 4 vuotta sitten
vanhempi
commit
3c129bb927

+ 31 - 30
ngp_le/Dockerfile

@@ -1,39 +1,40 @@
-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 curl jq
-
-ENV DOCKER_GEN_VERSION=0.7.3 \
-    DOCKER_HOST=unix:///var/run/docker.sock
-
+# derived from https://github.com/grewhit25/docker-letsencrypt-nginx-proxy-companion
+FROM alpine:3.10
+
+# Install build dependencies
+RUN apk add --update \
+        bash \
+        curl \
+        gcc \
+        git \
+        make \
+        musl-dev \
+        ca-certificates \
+        coreutils \
+        jq \
+        openssl \
+        && rm /var/cache/apk/*
+
+ENV DOCKER_GEN_VERSION=0.7.4
 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
 
+ENV DEBUG=false \
+    DOCKER_HOST=unix:///var/run/docker.sock
 
-WORKDIR /app
-#RUN apk add py-pip
-#RUN pip install --upgrade pip setuptools
-ADD https://bootstrap.pypa.io/get-pip.py /tmp/get-pip.py
-RUN python /tmp/get-pip.py
-RUN pip install requests
-
-# Install simp_le program
-RUN mkdir -p /src && git -C /src clone --depth=1 --branch "0.2.0" https://github.com/zenhack/simp_le.git
+ENV CRYPTOGRAPHY_DONT_BUILD_RUST=1
 
-# Install simp_le in /usr/bin
-RUN cd /src/simp_le && python ./setup.py install
+# Install simp_le
+COPY /install_simp_le.sh /app/install_simp_le.sh
+RUN chmod +rx /app/install_simp_le.sh \
+    && sync \
+    && /app/install_simp_le.sh \
+    && rm -f /app/install_simp_le.sh
 
-# Make house cleaning
-RUN rm -rf /src && \
-    apk del git gcc py-pip musl-dev libffi-dev python-dev openssl-dev && \
-    rm -rf /var/cache/apk/*
+COPY /app/ /app/
 
-ENTRYPOINT ["/bin/bash", "/app/entrypoint.sh" ]
-CMD ["/bin/bash", "/app/start.sh" ]
+WORKDIR /app
 
-COPY /app/ /app/
-# [dr] made executable
-RUN chmod +rx /app/*.sh && chmod +rx /app/letsencrypt_service && chmod +rx /app/update_certs
+ENTRYPOINT [ "/bin/bash", "/app/entrypoint.sh" ]
+CMD [ "/bin/bash", "/app/start.sh" ]

+ 123 - 46
ngp_le/app/entrypoint.sh

@@ -1,60 +1,37 @@
 #!/bin/bash
+# shellcheck disable=SC2155
 
 set -u
-DEBUG=false
 
-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_deprecated_env_var {
+    if [[ -n "${ACME_TOS_HASH:-}" ]]; then
+        echo "Info: the ACME_TOS_HASH environment variable is no longer used by simp_le and has been deprecated."
+        echo "simp_le now implicitly agree to the ACME CA ToS."
+    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
+            echo "Error: you need to share your Docker host socket with a volume at $socket_file" >&2
+            echo "Typically you should run your container with: '-v /var/run/docker.sock:$socket_file:ro'" >&2
             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
-    # Check if any container has been labelled as the nginx proxy container.
-    local labeled_cid=$(docker_api "/containers/json" | jq -r '.[] | select( .Labels["com.github.jrcs.letsencrypt_nginx_proxy_companion.nginx_proxy"] == "true")|.Id')
-    if [[ ! -z "${labeled_cid:-}" ]]; then
-        export NGINX_PROXY_CONTAINER=$labeled_cid
-    fi
-    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 or label the nginx proxy container to use with 'com.github.jrcs.letsencrypt_nginx_proxy_companion.nginx_proxy=true'." >&2
-        exit 1
-    fi
-}
-
 function check_writable_directory {
     local dir="$1"
-    docker_api "/containers/$CONTAINER_ID/json" | jq ".Mounts[].Destination" | grep -q "^\"$dir\"$"
-    if [[ $? -ne 0 ]]; then
-        echo "Warning: '$dir' does not appear to be a mounted volume."
+    if [[ $(get_self_cid) ]]; then
+        docker_api "/containers/$(get_self_cid)/json" | jq ".Mounts[].Destination" | grep -q "^\"$dir\"$"
+        [[ $? -ne 0 ]] && echo "Warning: '$dir' does not appear to be a mounted volume."
+    else
+        echo "Warning: can't check if '$dir' is a mounted volume without self container ID."
     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
+        echo "Check that '$dir' directory is declared as a writable volume." >&2
         exit 1
     fi
     touch $dir/.check_writable 2>/dev/null
@@ -67,26 +44,126 @@ function check_writable_directory {
 }
 
 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
+    # Credits to Steve Kamerman for the background Diffie-Hellman creation logic.
+    # https://github.com/jwilder/nginx-proxy/pull/589
+    local DHPARAM_BITS="${DHPARAM_BITS:-2048}"
+    re='^[0-9]*$'
+    if ! [[ "$DHPARAM_BITS" =~ $re ]] ; then
+       echo "Error: invalid Diffie-Hellman size of $DHPARAM_BITS !" >&2
+       exit 1
+    fi
+
+    # If a dhparam file is not available, use the pre-generated one and generate a new one in the background.
+    local PREGEN_DHPARAM_FILE="/app/dhparam.pem.default"
+    local DHPARAM_FILE="/etc/nginx/certs/dhparam.pem"
+    local GEN_LOCKFILE="/tmp/le_companion_dhparam_generating.lock"
+
+    # The hash of the pregenerated dhparam file is used to check if the pregen dhparam is already in use
+    local PREGEN_HASH=$(sha256sum "$PREGEN_DHPARAM_FILE" | cut -d ' ' -f1)
+    if [[ -f "$DHPARAM_FILE" ]]; then
+        local CURRENT_HASH=$(sha256sum "$DHPARAM_FILE" | cut -d ' ' -f1)
+        if [[ "$PREGEN_HASH" != "$CURRENT_HASH" ]]; then
+            # There is already a dhparam, and it's not the default
+            set_ownership_and_permissions "$DHPARAM_FILE"
+            echo "Info: Custom Diffie-Hellman group found, generation skipped."
+            return 0
+          fi
+
+        if [[ -f "$GEN_LOCKFILE" ]]; then
+            # Generation is already in progress
+            return 0
+        fi
     fi
+
+    echo "Info: Creating Diffie-Hellman group in the background."
+    echo "A pre-generated Diffie-Hellman group will be used for now while the new one
+is being created."
+
+    # Put the default dhparam file in place so we can start immediately
+    cp "$PREGEN_DHPARAM_FILE" "$DHPARAM_FILE"
+    set_ownership_and_permissions "$DHPARAM_FILE"
+    touch "$GEN_LOCKFILE"
+
+    # Generate a new dhparam in the background in a low priority and reload nginx when finished (grep removes the progress indicator).
+    (
+        (
+            nice -n +5 openssl dhparam -out "${DHPARAM_FILE}.new" "$DHPARAM_BITS" 2>&1 \
+            && mv "${DHPARAM_FILE}.new" "$DHPARAM_FILE" \
+            && echo "Info: Diffie-Hellman group creation complete, reloading nginx." \
+            && set_ownership_and_permissions "$DHPARAM_FILE" \
+            && reload_nginx
+        ) | grep -vE '^[\.+]+'
+        rm "$GEN_LOCKFILE"
+    ) & disown
 }
 
-source /app/functions.sh
+function check_default_cert_key {
+    local cn='letsencrypt-nginx-proxy-companion'
 
-[[ $DEBUG == true ]] && set -x
+    if [[ -e /etc/nginx/certs/default.crt && -e /etc/nginx/certs/default.key ]]; then
+        default_cert_cn="$(openssl x509 -noout -subject -in /etc/nginx/certs/default.crt)"
+        # Check if the existing default certificate is still valid for more
+        # than 3 months / 7776000 seconds (60 x 60 x 24 x 30 x 3).
+        check_cert_min_validity /etc/nginx/certs/default.crt 7776000
+        cert_validity=$?
+        [[ "$(lc $DEBUG)" == true ]] && echo "Debug: a default certificate with $default_cert_cn is present."
+    fi
+
+    # Create a default cert and private key if:
+    #   - either default.crt or default.key are absent
+    #   OR
+    #   - the existing default cert/key were generated by the container
+    #     and the cert validity is less than three months
+    if [[ ! -e /etc/nginx/certs/default.crt || ! -e /etc/nginx/certs/default.key ]] || [[ "${default_cert_cn:-}" =~ $cn && "${cert_validity:-}" -ne 0 ]]; then
+        openssl req -x509 \
+            -newkey rsa:4096 -sha256 -nodes -days 365 \
+            -subj "/CN=$cn" \
+            -keyout /etc/nginx/certs/default.key.new \
+            -out /etc/nginx/certs/default.crt.new \
+        && mv /etc/nginx/certs/default.key.new /etc/nginx/certs/default.key \
+        && mv /etc/nginx/certs/default.crt.new /etc/nginx/certs/default.crt
+        echo "Info: a default key and certificate have been created at /etc/nginx/certs/default.key and /etc/nginx/certs/default.crt."
+    elif [[ "$(lc $DEBUG)" == true && "${default_cert_cn:-}" =~ $cn ]]; then
+        echo "Debug: the self generated default certificate is still valid for more than three months. Skipping default certificate creation."
+    elif [[ "$(lc $DEBUG)" == true ]]; then
+        echo "Debug: the default certificate is user provided. Skipping default certificate creation."
+    fi
+    set_ownership_and_permissions "/etc/nginx/certs/default.key"
+    set_ownership_and_permissions "/etc/nginx/certs/default.crt"
+}
+
+source /app/functions.sh
 
 if [[ "$*" == "/bin/bash /app/start.sh" ]]; then
+    acmev1_r='acme-(v01\|staging)\.api\.letsencrypt\.org'
+    if [[ "${ACME_CA_URI:-}" =~ $acmev1_r ]]; then
+        echo "Error: the ACME v1 API is no longer supported by simp_le."
+        echo "See https://github.com/zenhack/simp_le/pull/119"
+        echo "Please use one of Let's Encrypt ACME v2 endpoints instead."
+        exit 1
+    fi
     check_docker_socket
-    if [[ -z "${NGINX_DOCKER_GEN_CONTAINER:-}" ]]; then
-        [[ -z "${NGINX_PROXY_CONTAINER:-}" ]] && get_nginx_proxy_cid
+    if [[ -z "$(get_nginx_proxy_container)" ]]; then
+        echo "Error: can't get nginx-proxy container ID !" >&2
+        echo "Check that you are doing one of the following :" >&2
+        echo -e "\t- Use the --volumes-from option to mount volumes from the nginx-proxy container." >&2
+        echo -e "\t- Set the NGINX_PROXY_CONTAINER env var on the letsencrypt-companion container to the name of the nginx-proxy container." >&2
+        echo -e "\t- Label the nginx-proxy container to use with 'com.github.jrcs.letsencrypt_nginx_proxy_companion.nginx_proxy'." >&2
+        exit 1
+    elif [[ -z "$(get_docker_gen_container)" ]] && ! is_docker_gen_container "$(get_nginx_proxy_container)"; then
+        echo "Error: can't get docker-gen container id !" >&2
+        echo "If you are running a three containers setup, check that you are doing one of the following :" >&2
+        echo -e "\t- Set the NGINX_DOCKER_GEN_CONTAINER env var on the letsencrypt-companion container to the name of the docker-gen container." >&2
+        echo -e "\t- Label the docker-gen container to use with 'com.github.jrcs.letsencrypt_nginx_proxy_companion.docker_gen.'" >&2
+        exit 1
     fi
     check_writable_directory '/etc/nginx/certs'
     check_writable_directory '/etc/nginx/vhost.d'
     check_writable_directory '/usr/share/nginx/html'
+    check_deprecated_env_var
+    check_default_cert_key
     check_dh_group
+    reload_nginx
 fi
 
 exec "$@"

+ 293 - 29
ngp_le/app/functions.sh

@@ -1,3 +1,6 @@
+#!/bin/bash
+# shellcheck disable=SC2155
+
 [[ -z "${VHOST_DIR:-}" ]] && \
  declare -r VHOST_DIR=/etc/nginx/vhost.d
 [[ -z "${START_HEADER:-}" ]] && \
@@ -5,27 +8,138 @@
 [[ -z "${END_HEADER:-}" ]] && \
  declare -r END_HEADER='## End of configuration add by letsencrypt container'
 
-add_location_configuration() {
+function check_nginx_proxy_container_run {
+    local _nginx_proxy_container=$(get_nginx_proxy_container)
+    if [[ -n "$_nginx_proxy_container" ]]; then
+        if [[ $(docker_api "/containers/${_nginx_proxy_container}/json" | jq -r '.State.Status') = "running" ]];then
+            return 0
+        else
+            echo "$(date "+%Y/%m/%d %T") Error: nginx-proxy container ${_nginx_proxy_container} isn't running." >&2
+            return 1
+        fi
+    else
+        echo "$(date "+%Y/%m/%d %T") Error: could not get a nginx-proxy container ID." >&2
+        return 1
+fi
+}
+
+function ascending_wildcard_locations {
+    # Given foo.bar.baz.example.com as argument, will output:
+    # - *.bar.baz.example.com
+    # - *.baz.example.com
+    # - *.example.com
+    local domain="${1:?}"
+    local first_label
+    regex="^[[:alnum:]_\-]+(\.[[:alpha:]]+)?$"
+    until [[ "$domain" =~ $regex ]]; do
+      first_label="${domain%%.*}"
+      domain="${domain/${first_label}./}"
+      echo "*.${domain}"
+    done
+}
+
+function descending_wildcard_locations {
+    # Given foo.bar.baz.example.com as argument, will output:
+    # - foo.bar.baz.example.*
+    # - foo.bar.baz.*
+    # - foo.bar.*
+    # - foo.*
+    local domain="${1:?}"
+    local last_label
+    regex="^[[:alnum:]_\-]+$"
+    until [[ "$domain" =~ $regex ]]; do
+      last_label="${domain##*.}"
+      domain="${domain/.${last_label}/}"
+      echo "${domain}.*"
+    done
+}
+
+function enumerate_wildcard_locations {
+    # Goes through ascending then descending wildcard locations for a given FQDN
+    local domain="${1:?}"
+    ascending_wildcard_locations "$domain"
+    descending_wildcard_locations "$domain"
+}
+
+function 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
+    local wildcard_domain
+    # If no domain was passed use default instead
+    [[ -z "$domain" ]] && domain='default'
+
+    # If the domain does not have an exact matching location file, test the possible
+    # wildcard locations files. Use default is no location file is present at all.
+    if [[ ! -f "${VHOST_DIR}/${domain}" ]]; then
+      for wildcard_domain in $(enumerate_wildcard_locations "$domain"); do
+        if [[ -f "${VHOST_DIR}/${wildcard_domain}" ]]; then
+          domain="$wildcard_domain"
+          break
+        fi
+        domain='default'
+      done
+    fi
+
+    if [[ -f "${VHOST_DIR}/${domain}" && -n $(sed -n "/$START_HEADER/,/$END_HEADER/p" "${VHOST_DIR}/${domain}") ]]; then
+        # If the config file exist and already have the location configuration, end with exit code 0
+        return 0
+    else
+        # Else write the location configuration to a temp file ...
+        echo "$START_HEADER" > "${VHOST_DIR}/${domain}".new
+        cat /app/nginx_location.conf >> "${VHOST_DIR}/${domain}".new
+        echo "$END_HEADER" >> "${VHOST_DIR}/${domain}".new
+        # ... append the existing file content to the temp one ...
+        [[ -f "${VHOST_DIR}/${domain}" ]] && cat "${VHOST_DIR}/${domain}" >> "${VHOST_DIR}/${domain}".new
+        # ... and copy the temp file to the old one (if the destination file is bind mounted, you can't change
+        # its inode from within the container, so mv won't work and cp has to be used), then remove the temp file.
+        cp -f "${VHOST_DIR}/${domain}".new "${VHOST_DIR}/${domain}" && rm -f "${VHOST_DIR}/${domain}".new
+        return 1
+    fi
 }
 
-remove_all_location_configurations() {
-    local old_shopt_options=$(shopt -p) # Backup shopt options
-    shopt -s nullglob
+function remove_all_location_configurations {
     for file in "${VHOST_DIR}"/*; do
-        [[ -n $(sed -n "/$START_HEADER/,/$END_HEADER/p" "$file") ]] && \
-         sed -i "/$START_HEADER/,/$END_HEADER/d" "$file"
+        [[ -e "$file" ]] || continue
+        if [[ -n $(sed -n "/$START_HEADER/,/$END_HEADER/p" "$file") ]]; then
+            sed "/$START_HEADER/,/$END_HEADER/d" "$file" > "$file".new
+            cp -f "$file".new "$file" && rm -f "$file".new
+        fi
     done
-    eval "$old_shopt_options" # Restore shopt options
+}
+
+function check_cert_min_validity {
+    # Check if a certificate ($1) is still valid for a given amount of time in seconds ($2).
+    # Returns 0 if the certificate is still valid for this amount of time, 1 otherwise.
+    local cert_path="$1"
+    local min_validity="$(( $(date "+%s") + $2 ))"
+
+    local cert_expiration
+    cert_expiration="$(openssl x509 -noout -enddate -in "$cert_path" | cut -d "=" -f 2)"
+    cert_expiration="$(date --utc --date "${cert_expiration% GMT}" "+%s")"
+
+    [[ $cert_expiration -gt $min_validity ]] || return 1
+}
+
+function get_self_cid {
+    local self_cid=""
+
+    # Try the /proc files methods first then resort to the Docker API.
+    if [[ -f /proc/1/cpuset ]]; then
+        self_cid="$(grep -Eo '[[:alnum:]]{64}' /proc/1/cpuset)"
+    fi
+    if [[ ( ${#self_cid} != 64 ) && ( -f /proc/self/cgroup ) ]]; then
+        self_cid="$(grep -Eo -m 1 '[[:alnum:]]{64}' /proc/self/cgroup)"
+    fi
+    if [[ ( ${#self_cid} != 64 ) ]]; then
+        self_cid="$(docker_api "/containers/$(hostname)/json" | jq -r '.Id')"
+    fi
+
+    # If it's not 64 characters long, then it's probably not a container ID.
+    if [[ ${#self_cid} == 64 ]]; then
+        echo "$self_cid"
+    else
+        echo "$(date "+%Y/%m/%d %T"), Error: can't get my container ID !" >&2
+        return 1
+    fi
 }
 
 ## Docker API
@@ -56,38 +170,188 @@ function docker_exec {
     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
+    if [[ -n "$exec_id" && "$exec_id" != "null" ]]; then
         docker_api /exec/$exec_id/start "POST" '{"Detach": false, "Tty":false}'
+    else
+        echo "$(date "+%Y/%m/%d %T"), Error: can't exec command ${cmd} in container ${id}. Check if the container is running." >&2
+        return 1
     fi
 }
 
+function docker_restart {
+    local id="${1?missing id}"
+    docker_api "/containers/$id/restart" "POST"
+}
+
 function docker_kill {
     local id="${1?missing id}"
     local signal="${2?missing signal}"
     docker_api "/containers/$id/kill?signal=$signal" "POST"
 }
 
+function labeled_cid {
+    docker_api "/containers/json" | jq -r '.[] | select(.Labels["'$1'"])|.Id'
+}
+
+function is_docker_gen_container {
+    local id="${1?missing id}"
+    if [[ $(docker_api "/containers/$id/json" | jq -r '.Config.Env[]' | egrep -c '^DOCKER_GEN_VERSION=') = "1" ]]; then
+        return 0
+    else
+        return 1
+    fi
+}
+
+function get_docker_gen_container {
+    # First try to get the docker-gen container ID from the container label.
+    local docker_gen_cid="$(labeled_cid com.github.jrcs.letsencrypt_nginx_proxy_companion.docker_gen)"
+
+    # If the labeled_cid function dit not return anything and the env var is set, use it.
+    if [[ -z "$docker_gen_cid" ]] && [[ -n "${NGINX_DOCKER_GEN_CONTAINER:-}" ]]; then
+        docker_gen_cid="$NGINX_DOCKER_GEN_CONTAINER"
+    fi
+
+    # If a container ID was found, output it. The function will return 1 otherwise.
+    [[ -n "$docker_gen_cid" ]] && echo "$docker_gen_cid"
+}
+
+function get_nginx_proxy_container {
+    local volumes_from
+    # First try to get the nginx container ID from the container label.
+    local nginx_cid="$(labeled_cid com.github.jrcs.letsencrypt_nginx_proxy_companion.nginx_proxy)"
+
+    # If the labeled_cid function dit not return anything ...
+    if [[ -z "${nginx_cid}" ]]; then
+        # ... and the env var is set, use it ...
+        if [[ -n "${NGINX_PROXY_CONTAINER:-}" ]]; then
+            nginx_cid="$NGINX_PROXY_CONTAINER"
+        # ... else try to get the container ID with the volumes_from method.
+        elif [[ $(get_self_cid) ]]; then
+            volumes_from=$(docker_api "/containers/$(get_self_cid)/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
+                    nginx_cid="$cid"
+                    break
+                fi
+            done
+        fi
+    fi
+
+    # If a container ID was found, output it. The function will return 1 otherwise.
+    [[ -n "$nginx_cid" ]] && echo "$nginx_cid"
+}
+
 ## Nginx
-reload_nginx() {
-    if [[ -n "${NGINX_DOCKER_GEN_CONTAINER:-}" ]]; then
+function reload_nginx {
+    local _docker_gen_container=$(get_docker_gen_container)
+    local _nginx_proxy_container=$(get_nginx_proxy_container)
+
+    if [[ -n "${_docker_gen_container:-}" ]]; then
         # Using docker-gen and nginx in separate container
-        echo "Reloading nginx docker-gen (using separate container ${NGINX_DOCKER_GEN_CONTAINER})..."
-        docker_kill "$NGINX_DOCKER_GEN_CONTAINER" SIGHUP
-        if [[ -n "${NGINX_PROXY_CONTAINER:-}" ]]; then
+        echo "Reloading nginx docker-gen (using separate container ${_docker_gen_container})..."
+        docker_kill "${_docker_gen_container}" SIGHUP
+
+        if [[ -n "${_nginx_proxy_container:-}" ]]; then
             # Reloading nginx in case only certificates had been renewed
-            echo "Reloading nginx (using separate container ${NGINX_PROXY_CONTAINER})..."
-            docker_kill "$NGINX_PROXY_CONTAINER" SIGHUP
+            echo "Reloading nginx (using separate container ${_nginx_proxy_container})..."
+            docker_kill "${_nginx_proxy_container}" SIGHUP
         fi
     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" ]'
+        if [[ -n "${_nginx_proxy_container:-}" ]]; then
+            echo "Reloading nginx proxy (${_nginx_proxy_container})..."
+            docker_exec "${_nginx_proxy_container}" \
+                '[ "sh", "-c", "/app/docker-entrypoint.sh /usr/local/bin/docker-gen /app/nginx.tmpl /etc/nginx/conf.d/default.conf; /usr/sbin/nginx -s reload" ]' \
+                | sed -rn 's/^.*([0-9]{4}\/[0-9]{2}\/[0-9]{2}.*$)/\1/p'
+            [[ ${PIPESTATUS[0]} -eq 1 ]] && echo "$(date "+%Y/%m/%d %T"), Error: can't reload nginx-proxy." >&2
+        fi
+    fi
+}
+
+function set_ownership_and_permissions {
+  local path="${1:?}"
+  # The default ownership is root:root, with 755 permissions for folders and 644 for files.
+  local user="${FILES_UID:-root}"
+  local group="${FILES_GID:-$user}"
+  local f_perms="${FILES_PERMS:-644}"
+  local d_perms="${FOLDERS_PERMS:-755}"
+
+  if [[ ! "$f_perms" =~ ^[0-7]{3,4}$ ]]; then
+    echo "Warning : the provided files permission octal ($f_perms) is incorrect. Skipping ownership and permissions check."
+    return 1
+  fi
+  if [[ ! "$d_perms" =~ ^[0-7]{3,4}$ ]]; then
+    echo "Warning : the provided folders permission octal ($d_perms) is incorrect. Skipping ownership and permissions check."
+    return 1
+  fi
+
+  [[ "$(lc $DEBUG)" == true ]] && echo "Debug: checking $path ownership and permissions."
+
+  # Find the user numeric ID if the FILES_UID environment variable isn't numeric.
+  if [[ "$user" =~ ^[0-9]+$ ]]; then
+    user_num="$user"
+  # Check if this user exist inside the container
+  elif id -u "$user" > /dev/null 2>&1; then
+    # Convert the user name to numeric ID
+    local user_num="$(id -u "$user")"
+    [[ "$(lc $DEBUG)" == true ]] && echo "Debug: numeric ID of user $user is $user_num."
+  else
+    echo "Warning: user $user not found in the container, please use a numeric user ID instead of a user name. Skipping ownership and permissions check."
+    return 1
+  fi
+
+  # Find the group numeric ID if the FILES_GID environment variable isn't numeric.
+  if [[ "$group" =~ ^[0-9]+$ ]]; then
+    group_num="$group"
+  # Check if this group exist inside the container
+  elif getent group "$group" > /dev/null 2>&1; then
+    # Convert the group name to numeric ID
+    local group_num="$(getent group "$group" | awk -F ':' '{print $3}')"
+    [[ "$(lc $DEBUG)" == true ]] && echo "Debug: numeric ID of group $group is $group_num."
+  else
+    echo "Warning: group $group not found in the container, please use a numeric group ID instead of a group name. Skipping ownership and permissions check."
+    return 1
+  fi
+
+  # Check and modify ownership if required.
+  if [[ -e "$path" ]]; then
+    if [[ "$(stat -c %u:%g "$path" )" != "$user_num:$group_num" ]]; then
+      [[ "$(lc $DEBUG)" == true ]] && echo "Debug: setting $path ownership to $user:$group."
+      if [[ -L "$path" ]]; then
+        chown -h "$user_num:$group_num" "$path"
+      else
+        chown "$user_num:$group_num" "$path"
+      fi
+    fi
+    # If the path is a folder, check and modify permissions if required.
+    if [[ -d "$path" ]]; then
+      if [[ "$(stat -c %a "$path")" != "$d_perms" ]]; then
+        [[ "$(lc $DEBUG)" == true ]] && echo "Debug: setting $path permissions to $d_perms."
+        chmod "$d_perms" "$path"
+      fi
+    # If the path is a file, check and modify permissions if required.
+    elif [[ -f "$path" ]]; then
+      # Use different permissions for private files (private keys and ACME account files) ...
+      if [[ "$path" =~ ^.*(default\.key|key\.pem|\.json)$ ]]; then
+        if [[ "$(stat -c %a "$path")" != "$f_perms" ]]; then
+          [[ "$(lc $DEBUG)" == true ]] && echo "Debug: setting $path permissions to $f_perms."
+          chmod "$f_perms" "$path"
         fi
+      # ... and for public files (certificates, chains, fullchains, DH parameters).
+      else
+        if [[ "$(stat -c %a "$path")" != "644" ]]; then
+          [[ "$(lc $DEBUG)" == true ]] && echo "Debug: setting $path permissions to 644."
+          chmod "644" "$path"
+        fi
+      fi
     fi
+  else
+    echo "Warning: $path does not exist. Skipping ownership and permissions check."
+    return 1
+  fi
 }
 
 # Convert argument to lowercase (bash 4 only)
-function lc() {
+function lc {
 	echo "${@,,}"
 }

+ 251 - 71
ngp_le/app/letsencrypt_service

@@ -1,59 +1,155 @@
 #!/bin/bash
+# shellcheck disable=SC2120
 
-DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+source /app/functions.sh
 
 seconds_to_wait=3600
-ACME_CA_URI="${ACME_CA_URI:-https://acme-v01.api.letsencrypt.org/directory}"
-ACME_TOS_HASH="${ACME_TOS_HASH:-6373439b9f29d67a5cd4d18cbc7f264809342dbf21cb2ba2fc7588df987a6221}"
+ACME_CA_URI="${ACME_CA_URI:-https://acme-v02.api.letsencrypt.org/directory}"
 DEFAULT_KEY_SIZE=4096
+REUSE_ACCOUNT_KEYS="$(lc ${REUSE_ACCOUNT_KEYS:-true})"
+REUSE_PRIVATE_KEYS="$(lc ${REUSE_PRIVATE_KEYS:-false})"
+MIN_VALIDITY_CAP=7603200
+DEFAULT_MIN_VALIDITY=2592000
 
-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"
+function create_link {
+    local -r source=${1?missing source argument}
+    local -r target=${2?missing target argument}
+    if [[ -f "$target" ]] && [[ "$(readlink "$target")" == "$source" ]]; then
+      set_ownership_and_permissions "$target"
+      [[ "$(lc $DEBUG)" == true ]] && echo "$target already linked to $source"
+      return 1
+    else
+      ln -sf "$source" "$target" \
+        && set_ownership_and_permissions "$target"
+    fi
 }
 
-create_links() {
-    local readonly base_domain=${1?missing base_domain argument}
-    local readonly domain=${2?missing base_domain argument}
+function create_links {
+    local -r base_domain=${1?missing base_domain argument}
+    local -r domain=${2?missing base_domain argument}
 
-    if [[ ! -f "/etc/nginx/certs/$base_domain"/fullchain.pem || \
-          ! -f "/etc/nginx/certs/$base_domain"/key.pem ]]; then
+    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
+    create_link "./$base_domain/fullchain.pem" "/etc/nginx/certs/$domain.crt"
     return_code=$(( $return_code & $? ))
-    create_link "/etc/nginx/certs/$domain".key "./$base_domain"/key.pem
+    create_link "./$base_domain/key.pem" "/etc/nginx/certs/$domain.key"
     return_code=$(( $return_code & $? ))
     if [[ -f "/etc/nginx/certs/dhparam.pem" ]]; then
-        create_link "/etc/nginx/certs/$domain".dhparam.pem ./dhparam.pem
+        create_link ./dhparam.pem "/etc/nginx/certs/$domain.dhparam.pem"
         return_code=$(( $return_code & $? ))
     fi
-    if [[ -f "/etc/nginx/certs/$base_domain"/chain.pem ]]; then
-        create_link "/etc/nginx/certs/$domain".chain.pem "./$base_domain"/chain.pem
+    if [[ -f "/etc/nginx/certs/$base_domain/chain.pem" ]]; then
+        create_link "./$base_domain/chain.pem" "/etc/nginx/certs/$domain.chain.pem"
         return_code=$(( $return_code & $? ))
     fi
     return $return_code
 }
 
-update_certs() {
-    [[ ! -f "$DIR"/letsencrypt_service_data ]] && return
+function cleanup_links {
+    local -a ENABLED_DOMAINS
+    local -a SYMLINKED_DOMAINS
+    local -a DISABLED_DOMAINS
+
+    # Create an array containing domains for which a
+    # symlinked private key exists in /etc/nginx/certs.
+    for symlinked_domain in /etc/nginx/certs/*.crt; do
+        [[ -L "$symlinked_domain" ]] || continue
+        symlinked_domain="${symlinked_domain##*/}"
+        symlinked_domain="${symlinked_domain%*.crt}"
+        SYMLINKED_DOMAINS+=("$symlinked_domain")
+    done
+    [[ "$(lc $DEBUG)" == true ]] && echo "Symlinked domains: ${SYMLINKED_DOMAINS[*]}"
+
+    # Create an array containing domains that are considered
+    # enabled (ie present on /app/letsencrypt_service_data).
+    # shellcheck source=/dev/null
+    source /app/letsencrypt_service_data
+    for cid in "${LETSENCRYPT_CONTAINERS[@]}"; do
+      host_varname="LETSENCRYPT_${cid}_HOST"
+      hosts_array="${host_varname}[@]"
+      for domain in "${!hosts_array}"; do
+        # Add domain to the array storing currently enabled domains.
+        ENABLED_DOMAINS+=("$domain")
+      done
+    done
+    [[ "$(lc $DEBUG)" == true ]] && echo "Enabled domains: ${ENABLED_DOMAINS[*]}"
+
+    # Create an array containing only domains for which a symlinked private key exists
+    # in /etc/nginx/certs but that no longer have a corresponding LETSENCRYPT_HOST set
+    # on an active container.
+    if [[ ${#SYMLINKED_DOMAINS[@]} -gt 0 ]]; then
+        mapfile -t DISABLED_DOMAINS < <(echo "${SYMLINKED_DOMAINS[@]}" \
+                                             "${ENABLED_DOMAINS[@]}" \
+                                             "${ENABLED_DOMAINS[@]}" \
+                                             | tr ' ' '\n' | sort | uniq -u)
+    fi
+    [[ "$(lc $DEBUG)" == true ]] && echo "Disabled domains: ${DISABLED_DOMAINS[*]}"
+
+
+    # Remove disabled domains symlinks if present.
+    # Return 1 if nothing was removed and 0 otherwise.
+    if [[ ${#DISABLED_DOMAINS[@]} -gt 0 ]]; then
+      [[ "$(lc $DEBUG)" == true ]] && echo "Some domains are disabled :"
+      for disabled_domain in "${DISABLED_DOMAINS[@]}"; do
+          [[ "$(lc $DEBUG)" == true ]] && echo "Checking domain ${disabled_domain}"
+          cert_folder="$(readlink -f /etc/nginx/certs/${disabled_domain}.crt)"
+          # If the dotfile is absent, skip domain.
+          if [[ ! -e "${cert_folder%/*}/.companion" ]]; then
+              [[ "$(lc $DEBUG)" == true ]] && echo "No .companion file found in ${cert_folder}. ${disabled_domain} is not managed by letsencrypt-nginx-proxy-companion. Skipping domain."
+              continue
+          else
+              [[ "$(lc $DEBUG)" == true ]] && echo "${disabled_domain} is managed by letsencrypt-nginx-proxy-companion. Removing unused symlinks."
+          fi
+
+          for extension in .crt .key .dhparam.pem .chain.pem; do
+              file="${disabled_domain}${extension}"
+              if [[ -n "${file// }" ]] && [[ -L "/etc/nginx/certs/${file}" ]]; then
+                  [[ "$(lc $DEBUG)" == true ]] && echo "Removing /etc/nginx/certs/${file}"
+                  rm -f "/etc/nginx/certs/${file}"
+              fi
+          done
+      done
+      return 0
+    else
+      return 1
+    fi
+}
+
+function update_certs {
+
+    check_nginx_proxy_container_run || return
+
+    [[ -f /app/letsencrypt_service_data ]] || return
 
     # Load relevant container settings
     unset LETSENCRYPT_CONTAINERS
-    source "$DIR"/letsencrypt_service_data
+    # shellcheck source=/dev/null
+    source /app/letsencrypt_service_data
 
-    reload_nginx='false'
+    should_reload_nginx='false'
     for cid in "${LETSENCRYPT_CONTAINERS[@]}"; do
+        should_restart_container='false'
         # 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[@]
+        hosts_array="${host_varname}[@]"
+        hosts_array_expanded=("${!hosts_array}")
+        # First domain will be our base domain
+        base_domain="${hosts_array_expanded[0]}"
+
+        params_d_str=""
+
+        # Use container's LETSENCRYPT_EMAIL if set, fallback to DEFAULT_EMAIL
         email_varname="LETSENCRYPT_${cid}_EMAIL"
+        email_address="${!email_varname}"
+        if [[ "$email_address" != "<no value>" ]]; then
+            params_d_str+=" --email $email_address"
+        elif [[ -n "${DEFAULT_EMAIL:-}" ]]; then
+            params_d_str+=" --email $DEFAULT_EMAIL"
+        fi
 
         keysize_varname="LETSENCRYPT_${cid}_KEYSIZE"
         cert_keysize="${!keysize_varname}"
@@ -62,44 +158,55 @@ update_certs() {
         fi
 
         test_certificate_varname="LETSENCRYPT_${cid}_TEST"
-        create_test_certificate=false
-        if [[ $(lc "${!test_certificate_varname:-}") == true ]]; then
-            create_test_certificate=true
+        le_staging_uri="https://acme-staging-v02.api.letsencrypt.org/directory"
+        if [[ $(lc "${!test_certificate_varname:-}") == true ]] || \
+          [[ "$ACME_CA_URI" == "$le_staging_uri" ]]; then
+            # Use staging Let's Encrypt ACME end point
+            acme_ca_uri="$le_staging_uri"
+            # Prefix test certificate directory with _test_
+            certificate_dir="/etc/nginx/certs/_test_$base_domain"
+        else
+            # Use default or user provided ACME end point
+            acme_ca_uri="$ACME_CA_URI"
+            certificate_dir="/etc/nginx/certs/$base_domain"
         fi
 
-        params_d_str=""
-        [[ $DEBUG == true ]] && params_d_str+=" -v"
+        account_varname="LETSENCRYPT_${cid}_ACCOUNT_ALIAS"
+        account_alias="${!account_varname}"
+        if [[ "$account_alias" == "<no value>" ]]; then
+            account_alias=default
+        fi
 
-        hosts_array_expanded=("${!hosts_array}")
-        # First domain will be our base domain
-        base_domain="${hosts_array_expanded[0]}"
+        [[ "$(lc $DEBUG)" == true ]] && params_d_str+=" -v"
+        [[ $REUSE_PRIVATE_KEYS == true ]] && params_d_str+=" --reuse_key"
 
-        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
+        min_validity="LETSENCRYPT_${cid}_MIN_VALIDITY"
+        min_validity="${!min_validity}"
+        if [[ "$min_validity" == "<no value>" ]]; then
+            min_validity=$DEFAULT_MIN_VALIDITY
+        fi
+        # Sanity Check
+        # Upper Bound
+        if [[ $min_validity -gt $MIN_VALIDITY_CAP ]]; then
+            min_validity=$MIN_VALIDITY_CAP
+        fi
+        # Lower Bound
+        if [[ $min_validity -lt $(($seconds_to_wait * 2)) ]]; then
+            min_validity=$(($seconds_to_wait * 2))
+        fi
+
+        if [[ "${1}" == "--force-renew" ]]; then
+            # Manually set to highest certificate lifetime given by LE CA
+            params_d_str+=" --valid_min 7776000"
         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
+            params_d_str+=" --valid_min $min_validity"
         fi
 
-        # Create directory for the first domain
-        mkdir -p /etc/nginx/certs/$base_domain
-        cd /etc/nginx/certs/$base_domain
+        # Create directory for the first domain,
+        # make it root readable only and make it the cwd
+        mkdir -p "$certificate_dir"
+        set_ownership_and_permissions "$certificate_dir"
+        pushd "$certificate_dir" || return
 
         for domain in "${!hosts_array}"; do
             # Add all the domains to certificate
@@ -108,39 +215,112 @@ update_certs() {
             add_location_configuration "$domain" || reload_nginx
         done
 
+        if [[ -e "./account_key.json" ]] && [[ ! -e "./account_reg.json" ]]; then
+          # If there is an account key present without account registration, this is
+          # a leftover from the ACME v1 version of simp_le. Remove this account key.
+          rm -f ./account_key.json
+          [[ "$(lc $DEBUG)" == true ]] \
+            && echo "Debug: removed ACME v1 account key $certificate_dir/account_key.json"
+        fi
+
+        # The ACME account key and registration full path are derived from the
+        # endpoint URI + the account alias (set to 'default' if no alias is provided)
+        account_dir="../accounts/${acme_ca_uri#*://}"
+        if [[ $REUSE_ACCOUNT_KEYS == true ]]; then
+            for type in "key" "reg"; do
+                file_full_path="${account_dir}/${account_alias}_${type}.json"
+                simp_le_file="./account_${type}.json"
+                if [[ -f "$file_full_path" ]]; then
+                    # If there is no symlink to the account file, create it
+                    if [[ ! -L "$simp_le_file" ]]; then
+                        ln -sf "$file_full_path" "$simp_le_file" \
+                          && set_ownership_and_permissions "$simp_le_file"
+                    # If the symlink target the wrong account file, replace it
+                    elif [[ "$(readlink -f "$simp_le_file")" != "$file_full_path" ]]; then
+                        ln -sf "$file_full_path" "$simp_le_file" \
+                          && set_ownership_and_permissions "$simp_le_file"
+                    fi
+                fi
+            done
+        fi
+
         echo "Creating/renewal $base_domain certificates... (${hosts_array_expanded[*]})"
         /usr/bin/simp_le \
-            -f account_key.json -f key.pem -f chain.pem -f fullchain.pem -f cert.pem \
-            --tos_sha256 $ACME_TOS_HASH \
+            -f account_key.json -f account_reg.json \
+            -f key.pem -f chain.pem -f fullchain.pem -f cert.pem \
             $params_d_str \
             --cert_key_size=$cert_keysize \
-            --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
+        if [[ $REUSE_ACCOUNT_KEYS == true ]]; then
+            mkdir -p "$account_dir"
+            for type in "key" "reg"; do
+                file_full_path="${account_dir}/${account_alias}_${type}.json"
+                simp_le_file="./account_${type}.json"
+                # If the account file to be reused does not exist yet, copy it
+                # from the CWD and replace the file in CWD with a symlink
+                if [[ ! -f "$file_full_path" && -f "$simp_le_file" ]]; then
+                    cp "$simp_le_file" "$file_full_path"
+                    ln -sf "$file_full_path" "$simp_le_file"
+                fi
+            done
+        fi
+
+        popd || return
 
-        if [[ -z $base_domain ]]; then
-            echo "inavalid stuff from container: ${cid}"
+        if [[ $simp_le_return -ne 2 ]]; then
+          for domain in "${!hosts_array}"; do
+            if [[ "$acme_ca_uri" == "$le_staging_uri" ]]; then
+              create_links "_test_$base_domain" "$domain" && should_reload_nginx='true' && should_restart_container='true'
+            else
+              create_links "$base_domain" "$domain" && should_reload_nginx='true' && should_restart_container='true'
+            fi
+          done
+          touch "${certificate_dir}/.companion"
+          # Set ownership and permissions of the files inside $certificate_dir
+          for file in .companion cert.pem key.pem chain.pem fullchain.pem account_key.json account_reg.json; do
+            file_path="${certificate_dir}/${file}"
+            [[ -e "$file_path" ]] && set_ownership_and_permissions "$file_path"
+          done
+          account_path="/etc/nginx/certs/accounts/${acme_ca_uri#*://}"
+          account_key_perm_path="${account_path}/${account_alias}_key.json"
+          account_reg_perm_path="${account_path}/${account_alias}_reg.json"
+          # Account key and registration files do not necessarily exists after
+          # simp_le exit code 1. Check if they exist before perm check (#591).
+          [[ -f "$account_key_perm_path" ]] && set_ownership_and_permissions "$account_key_perm_path"
+          [[ -f "$account_reg_perm_path" ]] && set_ownership_and_permissions "$account_reg_perm_path"
+          # Set ownership and permissions of the ACME account folder and its
+          # parent folders (up to /etc/nginx/certs/accounts included)
+          until [[ "$account_path" == /etc/nginx/certs ]]; do
+            set_ownership_and_permissions "$account_path"
+            account_path="$(dirname "$account_path")"
+          done
+          # Queue nginx reload if a certificate was issued or renewed
+          [[ $simp_le_return -eq 0 ]] && should_reload_nginx='true' && should_restart_container='true'
         fi
-        if [[ ! -z $base_domain ]]; then
 
-            for domain in "${!hosts_array}"; do
-                create_links $base_domain $domain && reload_nginx='true'
-                [[ $simp_le_return -eq 0 ]] && reload_nginx='true'
-            done
+        # Restart container if certs are updated and the respective environmental variable is set
+        restart_container_var="LETSENCRYPT_${cid}_RESTART_CONTAINER"
+        if [[ $(lc "${!restart_container_var:-}") == true ]] && [[ "$should_restart_container" == 'true' ]]; then
+            echo "Restarting container (${cid})..."
+            docker_restart "${cid}"
         fi
 
     done
 
-    [[ "$reload_nginx" == 'true' ]] && reload_nginx
+    cleanup_links && should_reload_nginx='true'
+
+    [[ "$should_reload_nginx" == 'true' ]] && reload_nginx
 }
 
+# Allow the script functions to be sourced without starting the Service Loop.
+if [ "${1}" == "--source-only" ]; then
+  return 0
+fi
+
 pid=
 # Service Loop: When this script exits, start it again.
 trap '[[ $pid ]] && kill $pid; exec $0' EXIT

+ 5 - 4
ngp_le/app/letsencrypt_service_data.tmpl

@@ -2,15 +2,16 @@ LETSENCRYPT_CONTAINERS=({{ range $hosts, $containers := groupBy $ "Env.LETSENCRY
 
 {{ range $hosts, $containers := groupBy $ "Env.LETSENCRYPT_HOST" }}
 
-{{ if trim $hosts }}
+{{ $hosts := trimSuffix "," $hosts }}
 
 {{ range $container := $containers }}{{ $cid := printf "%.12s" $container.ID }}
-LETSENCRYPT_{{ $cid }}_HOST=( {{ range $host := split $hosts "," }}'{{ $host }}' {{ end }})
+LETSENCRYPT_{{ $cid }}_HOST=( {{ range $host := split $hosts "," }}{{ $host := trim $host }}'{{ $host }}' {{ end }})
 LETSENCRYPT_{{ $cid }}_EMAIL="{{ $container.Env.LETSENCRYPT_EMAIL }}"
 LETSENCRYPT_{{ $cid }}_KEYSIZE="{{ $container.Env.LETSENCRYPT_KEYSIZE }}"
 LETSENCRYPT_{{ $cid }}_TEST="{{ $container.Env.LETSENCRYPT_TEST }}"
-{{ end }}
-
+LETSENCRYPT_{{ $cid }}_ACCOUNT_ALIAS="{{ $container.Env.LETSENCRYPT_ACCOUNT_ALIAS }}"
+LETSENCRYPT_{{ $cid }}_RESTART_CONTAINER="{{ $container.Env.LETSENCRYPT_RESTART_CONTAINER }}"
+LETSENCRYPT_{{ $cid }}_MIN_VALIDITY="{{ $container.Env.LETSENCRYPT_MIN_VALIDITY }}"
 {{ end }}
 
 {{ end }}

+ 2 - 0
ngp_le/app/nginx_location.conf

@@ -1,4 +1,6 @@
 location ^~ /.well-known/acme-challenge/ {
+    auth_basic off;
+    auth_request off;
     allow all;
     root /usr/share/nginx/html;
     try_files $uri =404;

+ 3 - 3
ngp_le/app/start.sh

@@ -8,15 +8,15 @@ term_handler() {
     source /app/functions.sh
     remove_all_location_configurations
 
-    exit 143; # 128 + 15 -- SIGTERM
+    exit 0
 }
 
-trap 'term_handler' INT QUIT KILL TERM
+trap 'term_handler' INT QUIT 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 -watch -notify '/app/signal_le_service' -wait 15s:60s /app/letsencrypt_service_data.tmpl /app/letsencrypt_service_data &
 docker_gen_pid=$!
 
 # wait "indefinitely"

+ 0 - 3
ngp_le/app/update_certs

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

+ 30 - 0
ngp_le/install_simp_le.sh

@@ -0,0 +1,30 @@
+#!/bin/bash
+
+set -e
+
+# Install python and packages needed to build simp_le
+apk add --update python3 git gcc musl-dev libffi-dev python3-dev openssl-dev
+
+# Create expected symlinks if they don't exist
+[[ -e /usr/bin/pip ]] || ln -sf /usr/bin/pip3 /usr/bin/pip
+[[ -e /usr/bin/python ]] || ln -sf /usr/bin/python3 /usr/bin/python
+
+# Get Let's Encrypt simp_le client source
+branch="0.16.0"
+mkdir -p /src
+git -C /src clone --depth=1 --branch $branch https://github.com/zenhack/simp_le.git
+
+# Install simp_le in /usr/bin
+cd /src/simp_le
+#pip install wheel requests
+for pkg in pip setuptools wheel
+do
+  pip3 install -U "${pkg?}"
+done
+pip3 install .
+
+# Make house cleaning
+cd /
+rm -rf /src
+apk del git gcc musl-dev libffi-dev python3-dev openssl-dev
+rm -rf /var/cache/apk/*