338 lines
12 KiB
Bash
338 lines
12 KiB
Bash
#!/bin/bash
|
|
|
|
# Acme loading script
|
|
# Uses code from:
|
|
# https://github.com/nginx-proxy/acme-companion/blob/main/app/letsencrypt_service
|
|
|
|
# We set a "LOG_LEVEL" that is incompatible with acme.sh. Overwrite it.
|
|
export LOG_LEVEL=1
|
|
export DEBUG=1
|
|
|
|
shopt -s expand_aliases
|
|
. /usr/local/acme.sh/acme.sh.env
|
|
|
|
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="azuracast"
|
|
local group="azuracast"
|
|
local f_perms="644"
|
|
local d_perms="755"
|
|
|
|
[[ "$DEBUG" == 1 ]] && 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; user_num="$(id -u "$user")"
|
|
[[ "$DEBUG" == 1 ]] && 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; group_num="$(getent group "$group" | awk -F ':' '{print $3}')"
|
|
[[ "$DEBUG" == 1 ]] && 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
|
|
[[ "$DEBUG" == 1 ]] && 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
|
|
[[ "$DEBUG" == 1 ]] && 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
|
|
[[ "$DEBUG" == 1 ]] && 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
|
|
[[ "$DEBUG" == 1 ]] && 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() {
|
|
echo "${@,,}"
|
|
}
|
|
|
|
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}
|
|
|
|
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/ssl.crt"
|
|
return_code=$(( return_code & $? ))
|
|
|
|
create_link "./$base_domain/key.pem" "/etc/nginx/certs/ssl.key"
|
|
return_code=$(( return_code & $? ))
|
|
|
|
if [[ -f "/etc/nginx/certs/dhparam.pem" ]]; then
|
|
create_link ./dhparam.pem "/etc/nginx/certs/ssl.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/ssl.chain.pem"
|
|
return_code=$(( return_code & $? ))
|
|
fi
|
|
|
|
return $return_code
|
|
}
|
|
|
|
CERTS_UPDATE_INTERVAL="${CERTS_UPDATE_INTERVAL:-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}")"
|
|
|
|
# Backward compatibility environment variable
|
|
REUSE_PRIVATE_KEYS="$(lc "${REUSE_PRIVATE_KEYS:-false}")"
|
|
|
|
function update_cert {
|
|
local hosts_array
|
|
IFS=',' read -ra hosts_array <<< "$LETSENCRYPT_HOST"
|
|
|
|
local base_domain="${hosts_array[0]}"
|
|
|
|
# Base CLI parameters array, used for both --register-account and --issue
|
|
local -a params_base_arr
|
|
|
|
params_base_arr+=(--log /dev/null)
|
|
[[ "$DEBUG" == 1 ]] && params_base_arr+=(--debug 2)
|
|
|
|
# Alternative trusted root CA path, used for test with Pebble
|
|
if [[ -n "${CA_BUNDLE// }" ]]; then
|
|
if [[ -f "$CA_BUNDLE" ]]; then
|
|
params_base_arr+=(--ca-bundle "$CA_BUNDLE")
|
|
[[ "$DEBUG" == 1 ]] && echo "Debug: acme.sh will use $CA_BUNDLE as trusted root CA."
|
|
else
|
|
echo "Warning: the path to the alternate CA bundle ($CA_BUNDLE) is not valid, using default Alpine trust store."
|
|
fi
|
|
fi
|
|
|
|
# CLI parameters array used for --register-account
|
|
local -a params_register_arr
|
|
|
|
# CLI parameters array used for --issue
|
|
local -a params_issue_arr
|
|
params_issue_arr+=(--webroot /usr/share/nginx/html)
|
|
|
|
local -n cert_keysize="LETSENCRYPT_KEYSIZE"
|
|
if [[ -z "$cert_keysize" ]] || \
|
|
[[ ! "$cert_keysize" =~ ^(2048|3072|4096|ec-256|ec-384)$ ]]; then
|
|
cert_keysize=$DEFAULT_KEY_SIZE
|
|
fi
|
|
params_issue_arr+=(--keylength "$cert_keysize")
|
|
|
|
# OCSP-Must-Staple extension
|
|
local -n ocsp="ACME_OCSP"
|
|
if [[ $(lc "$ocsp") == true ]]; then
|
|
params_issue_arr+=(--ocsp-must-staple)
|
|
fi
|
|
|
|
local -n accountemail="LETSENCRYPT_EMAIL"
|
|
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 [[ -z "$accountemail" ]]; 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
|
|
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
|
|
|
|
local -n acme_ca_uri="ACME_CA_URI"
|
|
if [[ -z "$acme_ca_uri" ]]; then
|
|
# Use default or user provided ACME end point
|
|
acme_ca_uri="$ACME_CA_URI"
|
|
fi
|
|
|
|
# LETSENCRYPT_TEST overrides LETSENCRYPT_ACME_CA_URI
|
|
local -n test_certificate="LETSENCRYPT_TEST"
|
|
if [[ $(lc "$test_certificate") == true ]]; then
|
|
# Use Let's Encrypt ACME V2 staging end point
|
|
acme_ca_uri="$ACME_CA_TEST_URI"
|
|
fi
|
|
|
|
# Set relevant --server parameter and ca folder name
|
|
params_base_arr+=(--server "$acme_ca_uri")
|
|
local ca_dir="${acme_ca_uri##*://}" \
|
|
&& ca_dir="${ca_dir%%:*}"
|
|
|
|
local certificate_dir
|
|
# If we're going to use one of LE stating endpoints ...
|
|
if [[ "$acme_ca_uri" =~ ^https://acme-staging.* ]]; then
|
|
# Unset accountemail
|
|
# force config dir to 'staging'
|
|
unset accountemail
|
|
config_home="/etc/acme.sh/staging"
|
|
# Prefix test certificate directory with _test_
|
|
certificate_dir="/etc/nginx/certs/_test_$base_domain"
|
|
else
|
|
certificate_dir="/etc/nginx/certs/$base_domain"
|
|
fi
|
|
|
|
params_issue_arr+=( \
|
|
--cert-file "${certificate_dir}/cert.pem" \
|
|
--key-file "${certificate_dir}/key.pem" \
|
|
--ca-file "${certificate_dir}/chain.pem" \
|
|
--fullchain-file "${certificate_dir}/fullchain.pem" \
|
|
)
|
|
|
|
[[ ! -d "$config_home" ]] && mkdir -p "$config_home"
|
|
|
|
params_base_arr+=(--config-home "$config_home")
|
|
local account_file="${config_home}/ca/${ca_dir}/account.json"
|
|
|
|
if [[ -n "${accountemail// }" ]]; then
|
|
# We're not using Zero SSL, register the ACME account using the provided email.
|
|
params_register_arr+=(--accountemail "$accountemail")
|
|
fi
|
|
|
|
# Account registration and update if required
|
|
if [[ ! -f "$account_file" ]]; then
|
|
params_register_arr=("${params_base_arr[@]}" "${params_register_arr[@]}")
|
|
[[ "$DEBUG" == 1 ]] && echo "Calling acme.sh --register-account with the following parameters : ${params_register_arr[*]}"
|
|
acme.sh --register-account "${params_register_arr[@]}"
|
|
fi
|
|
|
|
if [[ -n "${accountemail// }" ]] && ! grep -q "mailto:$accountemail" "$account_file"; then
|
|
local -a params_update_arr=("${params_base_arr[@]}" --accountemail "$accountemail")
|
|
[[ "$DEBUG" == 1 ]] && echo "Calling acme.sh --update-account with the following parameters : ${params_update_arr[*]}"
|
|
acme.sh --update-account "${params_update_arr[@]}"
|
|
fi
|
|
|
|
# If we still don't have an account.json file by this point, we've got an issue
|
|
if [[ ! -f "$account_file" ]]; then
|
|
echo "Error: no ACME account was found or registered for $accountemail and $acme_ca_uri, certificate creation aborted."
|
|
return 1
|
|
fi
|
|
|
|
local -n acme_preferred_chain="ACME_PREFERRED_CHAIN"
|
|
if [[ -n "${acme_preferred_chain}" ]]; then
|
|
# Using amce.sh --preferred-chain to select alternate chain.
|
|
params_issue_arr+=(--preferred-chain "$acme_preferred_chain")
|
|
fi
|
|
if [[ "$RENEW_PRIVATE_KEYS" != 'false' && "$REUSE_PRIVATE_KEYS" != 'true' ]]; then
|
|
params_issue_arr+=(--always-force-new-domain-key)
|
|
fi
|
|
|
|
[[ "${2:-}" == "--force-renew" ]] && params_issue_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_issue_arr+=(--domain "$domain")
|
|
done
|
|
|
|
params_issue_arr=("${params_base_arr[@]}" "${params_issue_arr[@]}")
|
|
[[ "$DEBUG" == 1 ]] && echo "Calling acme.sh --issue with the following parameters : ${params_issue_arr[*]}"
|
|
echo "Creating/renewal $base_domain certificates... (${hosts_array[*]})"
|
|
acme.sh --issue "${params_issue_arr[@]}"
|
|
|
|
local acmesh_return=$?
|
|
local should_reload_nginx='false'
|
|
|
|
# 0 = success, 2 = RENEW_SKIP
|
|
if [[ $acmesh_return == 0 || $acmesh_return == 2 ]]; then
|
|
if [[ $acme_ca_uri =~ ^https://acme-staging.* ]]; then
|
|
create_links "_test_$base_domain" \
|
|
&& should_reload_nginx='true'
|
|
else
|
|
create_links "$base_domain" \
|
|
&& should_reload_nginx='true'
|
|
fi
|
|
|
|
# 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
|
|
|
|
[[ $acmesh_return -eq 0 ]] \
|
|
&& should_reload_nginx='true'
|
|
fi
|
|
|
|
if [[ "$should_reload_nginx" == 'true' ]]; then
|
|
echo "Reloading nginx..."
|
|
on_ssl_renewal
|
|
fi
|
|
}
|
|
|
|
if [ ! -z "$VIRTUAL_HOST" ]; then
|
|
echo "Multi-site configuration detected; skipping local ACME setup."
|
|
elif [ ! -z "$LETSENCRYPT_HOST" -a "$LETSENCRYPT_HOST" != " " ]; then
|
|
update_cert "$@"
|
|
fi
|
|
|
|
# Wait some amount of time
|
|
echo "Sleep for ${CERTS_UPDATE_INTERVAL}s"
|
|
sleep $CERTS_UPDATE_INTERVAL
|
|
exit
|