1
0
docker-letsencrypt-nginx-pr.../app/letsencrypt_service
Nicolas Duchon aa62af0052
Code styling
2020-11-27 14:46:45 +01:00

312 lines
12 KiB
Bash
Executable File

#!/bin/bash
# shellcheck source=functions.sh
source /app/functions.sh
seconds_to_wait=3600
ACME_CA_URI="${ACME_CA_URI:-https://acme-v02.api.letsencrypt.org/directory}"
ACME_CA_TEST_URI="https://acme-staging-v02.api.letsencrypt.org/directory"
DEFAULT_KEY_SIZE="${DEFAULT_KEY_SIZE:-4096}"
RENEW_PRIVATE_KEYS="$(lc "${RENEW_PRIVATE_KEYS:-true}")"
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"
[[ "$DEBUG" == 1 ]] && echo "$target already linked to $source"
return 1
else
ln -sf "$source" "$target" \
&& set_ownership_and_permissions "$target"
fi
}
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
return 1
fi
local return_code=1
create_link "./$base_domain/fullchain.pem" "/etc/nginx/certs/$domain.crt"
return_code=$(( return_code & $? ))
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 ./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 "./$base_domain/chain.pem" "/etc/nginx/certs/$domain.chain.pem"
return_code=$(( return_code & $? ))
fi
return $return_code
}
function cleanup_links {
local -a LETSENCRYPT_CONTAINERS
local -a LETSENCRYPT_STANDALONE_CERTS
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
[[ "$DEBUG" == 1 ]] && echo "Symlinked domains: ${SYMLINKED_DOMAINS[*]}"
# Create an array containing domains that are considered
# enabled (ie present on /app/letsencrypt_service_data or /app/letsencrypt_user_data).
[[ -f /app/letsencrypt_service_data ]] && source /app/letsencrypt_service_data
[[ -f /app/letsencrypt_user_data ]] && source /app/letsencrypt_user_data
LETSENCRYPT_CONTAINERS+=( "${LETSENCRYPT_STANDALONE_CERTS[@]}" )
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
[[ "$DEBUG" == 1 ]] && 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 or on /app/letsencrypt_user_data
if [[ ${#SYMLINKED_DOMAINS[@]} -gt 0 ]]; then
mapfile -t DISABLED_DOMAINS < <(echo "${SYMLINKED_DOMAINS[@]}" \
"${ENABLED_DOMAINS[@]}" \
"${ENABLED_DOMAINS[@]}" \
| tr ' ' '\n' | sort | uniq -u)
fi
[[ "$DEBUG" == 1 ]] && 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
[[ "$DEBUG" == 1 ]] && echo "Some domains are disabled :"
for disabled_domain in "${DISABLED_DOMAINS[@]}"; do
[[ "$DEBUG" == 1 ]] && 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
[[ "$DEBUG" == 1 ]] && echo "No .companion file found in ${cert_folder}. ${disabled_domain} is not managed by letsencrypt-nginx-proxy-companion. Skipping domain."
continue
else
[[ "$DEBUG" == 1 ]] && 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
[[ "$DEBUG" == 1 ]] && echo "Removing /etc/nginx/certs/${file}"
rm -f "/etc/nginx/certs/${file}"
fi
done
done
return 0
else
return 1
fi
}
function update_cert {
local cid="${1:?}"
# Derive host and email variable names
local host_varname="LETSENCRYPT_${cid}_HOST"
# Array variable indirection hack: http://stackoverflow.com/a/25880676/350221
local -a hosts_array="${host_varname}[@]"
local hosts_array_expanded=("${!hosts_array}")
# First domain will be our base domain
local base_domain="${hosts_array_expanded[0]}"
local should_restart_container='false'
local -a params_d_arr
local keysize_varname="LETSENCRYPT_${cid}_KEYSIZE"
local cert_keysize="${!keysize_varname:-"<no value>"}"
if [[ "$cert_keysize" == "<no value>" ]] || \
[[ ! "$cert_keysize" =~ ^(2048|3072|4096|8192|ec-256|ec-384)$ ]]; then
cert_keysize=$DEFAULT_KEY_SIZE
fi
local accountemail_varname="LETSENCRYPT_${cid}_EMAIL"
local accountemail="${!accountemail_varname:-"<no value>"}"
local config_home
# If we don't have a LETSENCRYPT_EMAIL from the proxied container
# and DEFAULT_EMAIL is set to a non empty value, use the latter.
if [[ "$accountemail" == "<no value>" ]]; then
if [[ -n "${DEFAULT_EMAIL// }" ]]; then
accountemail="$DEFAULT_EMAIL"
else
unset accountemail
fi
fi
if [[ -n "${accountemail// }" ]]; then
# If we got an email, use it with the corresponding config home
params_d_arr+=("--accountemail" "$accountemail")
config_home="/etc/acme.sh/$accountemail"
else
# If we did not get any email at all, use the default (empty mail) config
config_home="/etc/acme.sh/default"
fi
[[ ! -d "$config_home" ]] && mkdir -p "$config_home"
local test_certificate_varname="LETSENCRYPT_${cid}_TEST"
local acme_ca_uri certificate_dir
if [[ $(lc "${!test_certificate_varname:-}") == true ]] || \
[[ "$ACME_CA_URI" == "$ACME_CA_TEST_URI" ]]; then
# Use staging Let's Encrypt ACME end point
acme_ca_uri="$ACME_CA_TEST_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
[[ "$DEBUG" == 1 ]] && params_d_arr+=("--debug")
[[ "$RENEW_PRIVATE_KEYS" == true ]] && params_d_arr+=("--always-force-new-domain-key")
[[ "${2:-}" == "--force-renew" ]] && params_d_arr+=("--force")
# Create directory for the first domain
mkdir -p "$certificate_dir"
set_ownership_and_permissions "$certificate_dir"
for domain in "${!hosts_array}"; do
# Add all the domains to certificate
params_d_arr+=("--domain" "$domain")
# Add location configuration for the domain
add_location_configuration "$domain" || reload_nginx
done
echo "Creating/renewal $base_domain certificates... (${hosts_array_expanded[*]})"
acme.sh --issue \
--log /dev/null \
--config-home "$config_home" \
"${params_d_arr[@]}" \
--keylength "$cert_keysize" \
--server "$acme_ca_uri" \
--webroot /usr/share/nginx/html \
--cert-file "${certificate_dir}/cert.pem" \
--key-file "${certificate_dir}/key.pem" \
--ca-file "${certificate_dir}/chain.pem" \
--fullchain-file "${certificate_dir}/fullchain.pem"
local acmesh_return=$?
# 0 = success, 2 = RENEW_SKIP
if [[ $acmesh_return == 0 || $acmesh_return == 2 ]]; then
for domain in "${!hosts_array}"; do
if [[ $acme_ca_uri =~ ^https://acme-staging.* ]]; 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 "${certificate_dir}/.companion"
# Make private key root readable only
for file in cert.pem key.pem chain.pem fullchain.pem; do
local file_path="${certificate_dir}/${file}"
[[ -e "$file_path" ]] && set_ownership_and_permissions "$file_path"
done
# Queue nginx reload if a certificate was issued or renewed
[[ $acmesh_return -eq 0 ]] \
&& should_reload_nginx='true' \
&& should_restart_container='true'
fi
# Restart container if certs are updated and the respective environmental variable is set
local 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
for domain in "${!hosts_array}"; do
if [[ -f "/etc/nginx/conf.d/standalone-cert-$domain.conf" ]]; then
[[ "$DEBUG" == 1 ]] && echo "Debug: removing standalone configuration file /etc/nginx/conf.d/standalone-cert-$domain.conf"
rm -f "/etc/nginx/conf.d/standalone-cert-$domain.conf" && should_reload_nginx='true'
fi
done
}
function update_certs {
local -a LETSENCRYPT_CONTAINERS
local -a LETSENCRYPT_STANDALONE_CERTS
pushd /etc/nginx/certs > /dev/null || return
check_nginx_proxy_container_run || return
# Load relevant container settings
if [[ -f /app/letsencrypt_service_data ]]; then
source /app/letsencrypt_service_data
else
echo "Warning: /app/letsencrypt_service_data not found, skipping data from containers."
fi
# Load settings for standalone certs
if [[ -f /app/letsencrypt_user_data ]]; then
if source /app/letsencrypt_user_data; then
for cid in "${LETSENCRYPT_STANDALONE_CERTS[@]}"; do
host_varname="LETSENCRYPT_${cid}_HOST"
hosts_array="${host_varname}[@]"
for domain in "${!hosts_array}"; do
add_standalone_configuration "$domain"
done
done
reload_nginx
LETSENCRYPT_CONTAINERS+=( "${LETSENCRYPT_STANDALONE_CERTS[@]}" )
else
echo "Warning: could not source /app/letsencrypt_user_data, skipping user data"
fi
fi
should_reload_nginx='false'
for cid in "${LETSENCRYPT_CONTAINERS[@]}"; do
# Pass the eventual --force-renew arg to update_cert() as second arg
update_cert "$cid" "${1:-}"
done
cleanup_links && should_reload_nginx='true'
[[ "$should_reload_nginx" == 'true' ]] && reload_nginx
popd > /dev/null || return
}
# 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
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=