#!/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