|
|
@@ -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
|