Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added usage of --challenge-alias, possibility of different DNA modes per domain #82

Open
wants to merge 10 commits into
base: dns-01-challenge
Choose a base branch
from
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,23 @@
[![Docker Image Size](https://img.shields.io/docker/image-size/nginxproxy/acme-companion?sort=semver)](https://hub.docker.com/r/nginxproxy/acme-companion "Click to view the image on Docker Hub")
[![Docker stars](https://img.shields.io/docker/stars/nginxproxy/acme-companion.svg)](https://hub.docker.com/r/nginxproxy/acme-companion "Click to view the image on Docker Hub")
[![Docker pulls](https://img.shields.io/docker/pulls/nginxproxy/acme-companion.svg)](https://hub.docker.com/r/nginxproxy/acme-companion "Click to view the image on Docker Hub")

This fork of [acme-companion](https://github.com/nginx-proxy/acme-companion) brings support for:
* DNS mode challenge
* Wildcard domain certificates

Docker image available on dockerhub as [pinidh/acme-companion](https://hub.docker.com/repository/docker/pinidh/acme-companion).

**acme-companion** is a lightweight companion container for [**nginx-proxy**](https://github.com/nginx-proxy/nginx-proxy).

It handles the automated creation, renewal and use of SSL certificates for proxied Docker containers through the ACME protocol.

### Features:
* Automated creation/renewal of Let's Encrypt (or other ACME CAs) certificates using [**acme.sh**](https://github.com/acmesh-official/acme.sh).
* Let's Encrypt / ACME domain validation through `http-01` challenge only.
* Let's Encrypt / ACME domain validation through `http-01` or dns-01 challenge.
* Automated update and reload of nginx config on certificate creation/renewal.
* Support creation of [Multi-Domain (SAN) Certificates](https://github.com/nginx-proxy/acme-companion/blob/main/docs/Let's-Encrypt-and-ACME.md#multi-domains-certificates).
* Support creation of wildcard certificates.
* Creation of a strong [RFC7919 Diffie-Hellman Group](https://datatracker.ietf.org/doc/html/rfc7919#appendix-A) at startup.
* Work with all versions of docker.

Expand Down
41 changes: 26 additions & 15 deletions app/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -149,25 +149,36 @@ function check_default_account {
fi
}

export NGINX_PROXY_CONTAINER_LABEL="${NGINX_PROXY_CONTAINER_LABEL:-com.github.jrcs.letsencrypt_nginx_proxy_companion.nginx_proxy}"
export DOCKER_GEN_CONTAINER_LABEL="${DOCKER_GEN_CONTAINER_LABEL:-com.github.jrcs.letsencrypt_nginx_proxy_companion.docker_gen}"

if [[ "$*" == "/bin/bash /app/start.sh" ]]; then
print_version
check_docker_socket
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.nginx-proxy.nginx'." >&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.nginx-proxy.docker-gen'." >&2
exit 1
fi
while true; do
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.nginx-proxy.nginx'." >&2
sleep 1
continue
fi
break
done
while true; do
if [[ -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.nginx-proxy.docker-gen'." >&2
sleep 1
continue
fi
break
done
check_writable_directory '/etc/nginx/certs'
parse_true "${ACME_HTTP_CHALLENGE_LOCATION:=false}" && check_writable_directory '/etc/nginx/vhost.d'
check_writable_directory '/etc/acme.sh'
check_writable_directory '/usr/share/nginx/html'
if [[ -f /app/letsencrypt_user_data ]]; then
Expand Down
116 changes: 4 additions & 112 deletions app/functions.sh
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,6 @@ function in_array() {
return 1
}

[[ -z "${VHOST_DIR:-}" ]] && \
declare -r VHOST_DIR=/etc/nginx/vhost.d
[[ -z "${START_HEADER:-}" ]] && \
declare -r START_HEADER='## Start of configuration add by letsencrypt container'
[[ -z "${END_HEADER:-}" ]] && \
declare -r END_HEADER='## End of configuration add by letsencrypt container'

function check_nginx_proxy_container_run {
local _nginx_proxy_container; _nginx_proxy_container=$(get_nginx_proxy_container)
if [[ -n "$_nginx_proxy_container" ]]; then
Expand All @@ -55,96 +48,10 @@ function check_nginx_proxy_container_run {
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
tld_regex="^[[:alpha:]]+$"
regex="^[^.]+\..+$"
while [[ "$domain" =~ $regex ]]; do
first_label="${domain%%.*}"
domain="${domain/#"${first_label}."/}"
if [[ "$domain" == "*" || "$domain" =~ $tld_regex ]]; then
return
else
echo "*.${domain}"
fi
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="^.+\.[^.]+$"
while [[ "$domain" =~ $regex ]]; do
last_label="${domain##*.}"
domain="${domain/%".${last_label}"/}"
if [[ "$domain" == "*" ]]; then
return
else
echo "${domain}.*"
fi
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:-}"
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
while read -r wildcard_domain; do
if [[ -f "${VHOST_DIR}/${wildcard_domain}" ]]; then
domain="$wildcard_domain"
break
fi
domain='default'
done <<< "$(enumerate_wildcard_locations "$domain")"
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
}

function add_standalone_configuration {
local domain="${1:?}"
if grep -q "server_name ${domain};" /etc/nginx/conf.d/*.conf; then
# If the domain is already present in nginx's conf, use the location configuration.
add_location_configuration "$domain"
else
# Else use the standalone configuration.
cat > "/etc/nginx/conf.d/standalone-cert-$domain.conf" << EOF
[[ "$DEBUG" == 1 ]] && echo "Debug: creating standalone configuration file /etc/nginx/conf.d/standalone-cert-$domain.conf"
cat > "/etc/nginx/conf.d/standalone-cert-$domain.conf" << EOF
server {
server_name $domain;
listen 80;
Expand All @@ -159,7 +66,6 @@ server {
}
}
EOF
fi
}

function remove_all_standalone_configurations {
Expand All @@ -171,16 +77,6 @@ function remove_all_standalone_configurations {
eval "$old_shopt_options" # Restore shopt options
}

function remove_all_location_configurations {
for file in "${VHOST_DIR}"/*; do
[[ -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
}

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.
Expand Down Expand Up @@ -283,9 +179,7 @@ function is_docker_gen_container {

function get_docker_gen_container {
# First try to get the docker-gen container ID from the container label.
local legacy_docker_gen_cid; legacy_docker_gen_cid="$(labeled_cid com.github.jrcs.letsencrypt_nginx_proxy_companion.docker_gen)"
local new_docker_gen_cid; new_docker_gen_cid="$(labeled_cid com.github.nginx-proxy.docker-gen)"
local docker_gen_cid; docker_gen_cid="${new_docker_gen_cid:-$legacy_docker_gen_cid}"
local docker_gen_cid; docker_gen_cid="$(labeled_cid "${DOCKER_GEN_CONTAINER_LABEL}")"

# 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
Expand All @@ -299,9 +193,7 @@ function get_docker_gen_container {
function get_nginx_proxy_container {
local volumes_from
# First try to get the nginx container ID from the container label.
local legacy_nginx_cid; legacy_nginx_cid="$(labeled_cid com.github.jrcs.letsencrypt_nginx_proxy_companion.nginx_proxy)"
local new_nginx_cid; new_nginx_cid="$(labeled_cid com.github.nginx-proxy.nginx)"
local nginx_cid; nginx_cid="${new_nginx_cid:-$legacy_nginx_cid}"
local nginx_cid; nginx_cid="$(labeled_cid "${NGINX_PROXY_CONTAINER_LABEL}")"

# If the labeled_cid function dit not return anything ...
if [[ -z "${nginx_cid}" ]]; then
Expand Down
102 changes: 84 additions & 18 deletions app/letsencrypt_service
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ function cleanup_links {
for cid in "${LETSENCRYPT_CONTAINERS[@]}"; do
local -n hosts_array="LETSENCRYPT_${cid}_HOST"
for domain in "${hosts_array[@]}"; do
# Skip wildcard domains
if [[ "${domain:0:2}" == "*." ]]; then
continue
fi
# Add domain to the array storing currently enabled domains.
ENABLED_DOMAINS+=("$domain")
done
Expand Down Expand Up @@ -151,7 +155,6 @@ function update_cert {

# CLI parameters array used for --issue
local -a params_issue_arr
params_issue_arr+=(--webroot /usr/share/nginx/html)

local -n cert_keysize="LETSENCRYPT_${cid}_KEYSIZE"
if [[ -z "$cert_keysize" ]] || \
Expand Down Expand Up @@ -190,6 +193,62 @@ function update_cert {
# Use default or user provided ACME end point
acme_ca_uri="$ACME_CA_URI"
fi

local dns_api_applicable=false

local -n dns_challenge_aliases_array="LETSENCRYPT_${cid}_DNS_CHALLENGE_ALIAS"
local -n dns_modes_array="LETSENCRYPT_${cid}_DNS_MODE"

local dns_mode_settings_varname="LETSENCRYPT_${cid}_DNS_MODE_SETTINGS"
local dns_mode_settings="${!dns_mode_settings_varname:-"<no value>"}"

# Either, we have just one dns mode entry (used for all domains)
# or we need to have one mode entry for each / per domain
# (with LETSENCRYPT_SINGLE_DOMAIN_CERTS set to true,
# both is simultaniously true or false, hence still functions)
if { [ "${#dns_modes_array[*]}" -eq "1" ] || [ "${#dns_modes_array[*]}" -eq "${#hosts_array[*]}" ]; } && [[ "$dns_mode_settings" != "<no value>" ]]; then

[[ "$DEBUG" == 1 ]] && echo "Debug: DNS Mode Settings: ${dns_mode_settings}"
[[ "$DEBUG" == 1 ]] && echo "Debug: DNS Modes: ${dns_modes_array[*]} (${#dns_modes_array[*]})"
[[ "$DEBUG" == 1 ]] && echo "Debug: DNS Challenge Aliases: ${dns_challenge_aliases_array[*]} (${#dns_challenge_aliases_array[*]})"
[[ "$DEBUG" == 1 ]] && echo "Debug: Hosts: ${hosts_array[*]} (${#hosts_array[*]})"

dns_api_applicable=true;

# Apply DNS mode settings
# Setting are in the form "export VAR=value ..."
eval "$dns_mode_settings"

else
[[ "$DEBUG" == 1 ]] && echo "Debug: No valid DNS challenge config given."
[[ "$DEBUG" == 1 ]] && echo "Falling back to webroot mode."

params_issue_arr+=(--webroot /usr/share/nginx/html)
fi

for hosts_index in "${!hosts_array[@]}"; do

local domain="${hosts_array[$hosts_index]}"

# Add all the domains to certificate
params_issue_arr+=(--domain "$domain")

# Use DNS mode if configured
if [ "$dns_api_applicable" = true ]; then

local dns_challenge_alias="${dns_challenge_aliases_array[$hosts_index]}"
local dns_mode="${dns_modes_array[$hosts_index]}"

if [ "${dns_challenge_alias}" ]; then
params_issue_arr+=(--challenge-alias "${dns_challenge_alias}")
fi

if [ "$dns_mode" ]; then
params_issue_arr+=(--dns "${dns_mode}")
fi
fi
done

# LETSENCRYPT_TEST overrides LETSENCRYPT_ACME_CA_URI
local -n test_certificate="LETSENCRYPT_${cid}_TEST"
if [[ $(lc "$test_certificate") == true ]]; then
Expand Down Expand Up @@ -345,15 +404,6 @@ function update_cert {
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")
# If enabled, add location configuration for the domain
if parse_true "${ACME_HTTP_CHALLENGE_LOCATION:=false}"; then
add_location_configuration "$domain" || reload_nginx
fi
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[*]})"
Expand All @@ -364,6 +414,10 @@ function update_cert {
# 0 = success, 2 = RENEW_SKIP
if [[ $acmesh_return == 0 || $acmesh_return == 2 ]]; then
for domain in "${hosts_array[@]}"; do
# Skip wildcard domains
if [[ "${domain:0:2}" == "*." ]]; then
continue
fi
if [[ $acme_ca_uri =~ ^https://acme-staging.* ]]; then
create_links "_test_$base_domain" "$domain" \
&& should_reload_nginx='true' \
Expand Down Expand Up @@ -419,22 +473,34 @@ function update_certs {
echo "Warning: /app/letsencrypt_service_data not found, skipping data from containers."
fi

# Load settings for standalone certs
# Load settings for standalone certs defined into /app/letsencrypt_user_data
if [[ -f /app/letsencrypt_user_data ]]; then
if source /app/letsencrypt_user_data; then
for cid in "${LETSENCRYPT_STANDALONE_CERTS[@]}"; do
local -n hosts_array="LETSENCRYPT_${cid}_HOST"
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

# Configure http-01 challenge for standalone certs
if ! [[ -d /etc/nginx/conf.d ]]; then
echo "Warning: /etc/nginx/conf.d not mounted; skipping standalone configuration"
else
should_reload_nginx='false'
for cid in "${LETSENCRYPT_CONTAINERS[@]}"; do
local -n hosts_array="LETSENCRYPT_${cid}_HOST"
for domain in "${hosts_array[@]}"; do
# Add the standalone configuration if and only if the domain is
# not already present in nginx's conf. If it is present, the location
# configuration is expected to be there.
if ! grep -q "server_name ${domain};" /etc/nginx/conf.d/*.conf; then
add_standalone_configuration "$domain" && should_reload_nginx=true
fi
done
done
[[ "$should_reload_nginx" == 'true' ]] && reload_nginx
fi

should_reload_nginx='false'
for cid in "${LETSENCRYPT_CONTAINERS[@]}"; do
# Pass the eventual --force-renew arg to update_cert() as second arg
Expand Down
Loading