From 9da38f37b42b60106bc67679aa9dd6eba45248fd Mon Sep 17 00:00:00 2001 From: TreeN0de Date: Fri, 25 Feb 2022 19:07:22 +0100 Subject: [PATCH 1/9] feat: acme.sh pre and post hooks --- app/letsencrypt_service | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/letsencrypt_service b/app/letsencrypt_service index bac881a9..8fd2aa7d 100755 --- a/app/letsencrypt_service +++ b/app/letsencrypt_service @@ -220,6 +220,14 @@ function update_cert { --fullchain-file "${certificate_dir}/fullchain.pem" \ ) + # acme.sh pre and post hooks + if [[ -n "$ACME_PRE_HOOK" ]]; then + params_issue_arr+=(--pre-hook "$ACME_PRE_HOOK") + fi + if [[ -n "$ACME_POST_HOOK" ]]; then + params_issue_arr+=(--post-hook "$ACME_POST_HOOK") + fi + [[ ! -d "$config_home" ]] && mkdir -p "$config_home" params_base_arr+=(--config-home "$config_home") local account_file="${config_home}/ca/${ca_dir}/account.json" From 281aa02052a7ec31dea78b927ee4d26c43dc5fbb Mon Sep 17 00:00:00 2001 From: TreeN0de Date: Fri, 25 Feb 2022 19:22:30 +0100 Subject: [PATCH 2/9] docs: acme.sh pre and post hooks --- docs/Container-configuration.md | 6 ++++- docs/Hooks.md | 42 +++++++++++++++++++++++++++++++++ docs/README.md | 2 ++ 3 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 docs/Hooks.md diff --git a/docs/Container-configuration.md b/docs/Container-configuration.md index 618a7722..dba47e59 100644 --- a/docs/Container-configuration.md +++ b/docs/Container-configuration.md @@ -28,4 +28,8 @@ You can also create test certificates per container (see [Test certificates](./L * `CA_BUNDLE` - This is a test only variable [for use with Pebble](https://github.com/letsencrypt/pebble#avoiding-client-https-errors). It changes the trusted root CA used by `acme.sh`, from the default Alpine trust store to the CA bundle file located at the provided path (inside the container). Do **not** use it in production unless you are running your own ACME CA. -* `CERTS_UPDATE_INTERVAL` - 3600 seconds by default, this defines how often the container will check if the certificates require update. \ No newline at end of file +* `CERTS_UPDATE_INTERVAL` - 3600 seconds by default, this defines how often the container will check if the certificates require update. + +* `ACME_PRE_HOOK` - The provided command will be run before every certificate issuance. The action is limited to the commands available inside the **acme-companion** container. For example `--env "ACME_PRE_HOOK=echo 'start'"`. For more information see [Pre- and Post-Hook](./Hooks.md) + +* `ACME_POST_HOOK` - The provided command will be run after every certificate issuance. The action is limited to the commands available inside the **acme-companion** container. For example `--env "ACME_POST_HOOK=echo 'end'"`. For more information see [Pre- and Post-Hook](./Hooks.md) \ No newline at end of file diff --git a/docs/Hooks.md b/docs/Hooks.md new file mode 100644 index 00000000..34e2e0c7 --- /dev/null +++ b/docs/Hooks.md @@ -0,0 +1,42 @@ +## Pre-Hooks and Post-Hooks + +The Pre- and Post-Hooks of [acme.sh](https://github.com/acmesh-official/acme.sh/) are available through the corresponding environment variables. This allows to trigger actions just before and after certificates are issued (see [acme.sh documentation](https://github.com/acmesh-official/acme.sh/wiki/Using-pre-hook-post-hook-renew-hook-reloadcmd)) + +#### Pre-Hook +This command will be run before certificates are issued. For example `echo 'start'`: +```shell +$ docker run --detach \ + --name nginx-proxy-acme \ + --volumes-from nginx-proxy \ + --volume /var/run/docker.sock:/var/run/docker.sock:ro \ + --volume acme:/etc/acme.sh \ + --env "DEFAULT_EMAIL=mail@yourdomain.tld" \ + --env "ACME_PRE_HOOK=echo 'start'" + nginxproxy/acme-companion +``` + +#### Post-Hook +This command will be run after certificates are issued. For example `echo 'end'`: +```shell +$ docker run --detach \ + --name nginx-proxy-acme \ + --volumes-from nginx-proxy \ + --volume /var/run/docker.sock:/var/run/docker.sock:ro \ + --volume acme:/etc/acme.sh \ + --env "DEFAULT_EMAIL=mail@yourdomain.tld" \ + --env "ACME_POST_HOOK=echo 'end'" + nginxproxy/acme-companion +``` + +#### Verification: +If you want to check wether the hook-command is delivered properly to [acme.sh](https://github.com/acmesh-official/acme.sh/), you should check `/etc/acme.sh/[EMAILADDRESS]/[DOMAIN]/[DOMAIN].conf`. +The variable `Le_PreHook` contains the Pre-Hook-Command base64 encoded. +The variable `Le_PostHook` contains the Pre-Hook-Command base64 encoded. + +#### Limitations +* The commands that can be used in the hooks are limited to the commands available inside the **acme-companion** container. `curl` and `wget` are available, therefore it is possible to communicate with tools outside the container via HTTP, allowing for complex actions to be implemented outside or in other containers. +* The hooks are general options, therefore **the actions for all certificates are the same**. + +#### Use-cases +* Change some firewall rules just for the issuing process of the certificates, so the ports 80 and/or 443 don't have to be publicly reachable at all time. +* Monitoring. \ No newline at end of file diff --git a/docs/README.md b/docs/README.md index e94fabfc..27292b85 100644 --- a/docs/README.md +++ b/docs/README.md @@ -22,6 +22,8 @@ [Zero SSL](./Zero-SSL.md) +[Pre-Hooks and Post-Hooks](./Hooks.md) + #### Troubleshooting: [Invalid / failing authorizations](./Invalid-authorizations.md) From e3419dffb8a15de5451635c17d9c2d6477102842 Mon Sep 17 00:00:00 2001 From: TreeN0de Date: Fri, 25 Feb 2022 19:36:03 +0100 Subject: [PATCH 3/9] tests: change run_le_container() parameter handling --- test/tests/test-functions.sh | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/test/tests/test-functions.sh b/test/tests/test-functions.sh index 1bdfe1e9..5625f842 100755 --- a/test/tests/test-functions.sh +++ b/test/tests/test-functions.sh @@ -13,10 +13,31 @@ export -f get_base_domain function run_le_container { local image="${1:?}" local name="${2:?}" - local cli_args_str="${3:-}" + shift 2 local -a cli_args_arr - for arg in $cli_args_str; do - cli_args_arr+=("$arg") + + while [[ $# -gt 0 ]]; do + local flag="$1" + + case $flag in + -c|--cli-args) #only one value per flag. Multiple args = use flag multiple times + local cli_args_arr_tmp + IFS=' ' read -r -a cli_args_arr_tmp <<< "${2:?}" + cli_args_arr+=("${cli_args_arr_tmp[0]}") #Head + #shellcheck disable=SC2116 + cli_args_arr+=("$(echo "${cli_args_arr_tmp[@]:1}")") #Tail + shift 2 + ;; + + *) #Legacy Option + local cli_args_str="${1:?}" + for arg in $cli_args_str; do + cli_args_arr+=("$arg") + done + shift + ;; + esac + done if [[ "$SETUP" == '3containers' ]]; then @@ -30,6 +51,7 @@ function run_le_container { cli_args_arr+=(--env "ACME_CA_URI=https://pebble:14000/dir") cli_args_arr+=(--env "CA_BUNDLE=/pebble.minica.pem") cli_args_arr+=(--network acme_net) + cli_args_arr+=(--volume "${GITHUB_WORKSPACE}/pebble.minica.pem:/pebble.minica.pem") else return 1 fi @@ -38,7 +60,6 @@ function run_le_container { --name "$name" \ --volumes-from "$NGINX_CONTAINER_NAME" \ --volume /var/run/docker.sock:/var/run/docker.sock:ro \ - --volume "${GITHUB_WORKSPACE}/pebble.minica.pem:/pebble.minica.pem" \ "${cli_args_arr[@]}" \ --env "DOCKER_GEN_WAIT=500ms:2s" \ --env "TEST_MODE=true" \ From c26ee284cd61c266536687d916ce8f3f228e61bd Mon Sep 17 00:00:00 2001 From: TreeN0de Date: Fri, 25 Feb 2022 19:59:29 +0100 Subject: [PATCH 4/9] tests: acme.sh pre and post hooks --- .github/workflows/test.yml | 1 + test/config.sh | 1 + test/tests/acme_hooks/expected-std-out.txt | 1 + test/tests/acme_hooks/run.sh | 71 ++++++++++++++++++++++ 4 files changed, 74 insertions(+) create mode 100644 test/tests/acme_hooks/expected-std-out.txt create mode 100755 test/tests/acme_hooks/run.sh diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 29b5319a..30249c98 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -83,6 +83,7 @@ jobs: permissions_default, permissions_custom, symlinks, + acme_hooks, ] setup: [2containers, 3containers] acme-ca: [pebble] diff --git a/test/config.sh b/test/config.sh index f2b535cf..bb370407 100755 --- a/test/config.sh +++ b/test/config.sh @@ -16,6 +16,7 @@ globalTests+=( permissions_default permissions_custom symlinks + acme_hooks ) # The ocsp_must_staple test does not work with Pebble diff --git a/test/tests/acme_hooks/expected-std-out.txt b/test/tests/acme_hooks/expected-std-out.txt new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/test/tests/acme_hooks/expected-std-out.txt @@ -0,0 +1 @@ + diff --git a/test/tests/acme_hooks/run.sh b/test/tests/acme_hooks/run.sh new file mode 100755 index 00000000..f3412a77 --- /dev/null +++ b/test/tests/acme_hooks/run.sh @@ -0,0 +1,71 @@ +#!/bin/bash + +## Test for the hooks of acme.sh +pre_hook_file="/tmp/prehook" +pre_hook_command="touch $pre_hook_file" +post_hook_file="/tmp/posthook" +post_hook_command="touch $post_hook_file" + + + +if [[ -z $GITHUB_ACTIONS ]]; then + le_container_name="$(basename "${0%/*}")_$(date "+%Y-%m-%d_%H.%M.%S")" +else + le_container_name="$(basename "${0%/*}")" +fi +run_le_container "${1:?}" "$le_container_name" --cli-args "--env ACME_PRE_HOOK=$pre_hook_command" --cli-args "--env ACME_POST_HOOK=$post_hook_command" + +# Create the $domains array from comma separated domains in TEST_DOMAINS. +IFS=',' read -r -a domains <<< "$TEST_DOMAINS" + +# Cleanup function with EXIT trap +function cleanup { + # Remove the Nginx container silently. + docker rm --force "${domains[0]}" &> /dev/null + # Cleanup the files created by this run of the test to avoid foiling following test(s). + docker exec "$le_container_name" /app/cleanup_test_artifacts + # Stop the LE container + docker stop "$le_container_name" > /dev/null +} +trap cleanup EXIT + +# Run an nginx container for ${domains[0]} with LETSENCRYPT_EMAIL set. +container_email="contact@${domains[0]}" +run_nginx_container --hosts "${domains[0]}" --cli-args "--env LETSENCRYPT_EMAIL=${container_email}" + +# Wait for a symlink at /etc/nginx/certs/${domains[0]}.crt +wait_for_symlink "${domains[0]}" "$le_container_name" + +##Check if the command is deliverd properly in /etc/acme.sh +if docker exec "$le_container_name" [[ ! -d "/etc/acme.sh/$container_email" ]]; then + echo "The /etc/acme.sh/$container_email folder does not exist." +elif docker exec "$le_container_name" [[ ! -d "/etc/acme.sh/$container_email/${domains[0]}" ]]; then + echo "The /etc/acme.sh/$container_email/${domains[0]} folder does not exist." +elif docker exec "$le_container_name" [[ ! -f "/etc/acme.sh/$container_email/${domains[0]}/${domains[0]}.conf" ]]; then + echo "The /etc/acme.sh/$container_email/${domains[0]}/${domains[0]}.conf file does not exist." +fi +acme_pre_hook_key="Le_PreHook=" +acme_post_hook_key="Le_PostHook=" +acme_base64_start="'__ACME_BASE64__START_" +acme_base64_end="__ACME_BASE64__END_'" +pre_hook_command_base64=$(echo -n "$pre_hook_command" | base64) +post_hook_command_base64=$(echo -n "$post_hook_command" | base64) + +acme_pre_hook="$(docker exec "$le_container_name" grep "$acme_pre_hook_key" "/etc/acme.sh/$container_email/${domains[0]}/${domains[0]}.conf")" +acme_post_hook="$(docker exec "$le_container_name" grep "$acme_post_hook_key" "/etc/acme.sh/$container_email/${domains[0]}/${domains[0]}.conf")" + +if [[ "$acme_pre_hook_key$acme_base64_start$pre_hook_command_base64$acme_base64_end" != "$acme_pre_hook" ]]; then + echo "Prehook command not saved properly" +fi +if [[ "$acme_post_hook_key$acme_base64_start$post_hook_command_base64$acme_base64_end" != "$acme_post_hook" ]]; then + echo "Posthook command not saved properly" +fi + + +## Check if the action ist performed +if docker exec "$le_container_name" [[ ! -f "$pre_hook_file" ]]; then + echo "Prehook action failed" +fi +if docker exec "$le_container_name" [[ ! -f "$post_hook_file" ]]; then + echo "Posthook action failed" +fi From 312fe57bb892697fc030b08bcdfb33464ec84aad Mon Sep 17 00:00:00 2001 From: Nicolas Duchon Date: Wed, 2 Mar 2022 17:42:54 +0100 Subject: [PATCH 5/9] fix: remove unnecessary echo triggering SC2116 --- test/tests/test-functions.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/tests/test-functions.sh b/test/tests/test-functions.sh index 5625f842..4934f78a 100755 --- a/test/tests/test-functions.sh +++ b/test/tests/test-functions.sh @@ -24,8 +24,7 @@ function run_le_container { local cli_args_arr_tmp IFS=' ' read -r -a cli_args_arr_tmp <<< "${2:?}" cli_args_arr+=("${cli_args_arr_tmp[0]}") #Head - #shellcheck disable=SC2116 - cli_args_arr+=("$(echo "${cli_args_arr_tmp[@]:1}")") #Tail + cli_args_arr+=("${cli_args_arr_tmp[*]:1}") #Tail shift 2 ;; From b9e7d59bed138555e478b301416915a33f405967 Mon Sep 17 00:00:00 2001 From: Nicolas Duchon Date: Wed, 2 Mar 2022 18:42:07 +0100 Subject: [PATCH 6/9] feat: per-container Pre-Hooks and Post-Hooks --- app/letsencrypt_service | 12 ++++++++---- app/letsencrypt_service_data.tmpl | 6 ++++++ 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/app/letsencrypt_service b/app/letsencrypt_service index 8fd2aa7d..c94c2e58 100755 --- a/app/letsencrypt_service +++ b/app/letsencrypt_service @@ -221,11 +221,15 @@ function update_cert { ) # acme.sh pre and post hooks - if [[ -n "$ACME_PRE_HOOK" ]]; then - params_issue_arr+=(--pre-hook "$ACME_PRE_HOOK") + local -n acme_pre_hook="ACME_${cid}_PRE_HOOK" + acme_pre_hook=${acme_pre_hook:-$ACME_PRE_HOOK} + if [[ -n "${acme_pre_hook// }" ]]; then + params_issue_arr+=(--pre-hook "$acme_pre_hook") fi - if [[ -n "$ACME_POST_HOOK" ]]; then - params_issue_arr+=(--post-hook "$ACME_POST_HOOK") + local -n acme_post_hook="ACME_${cid}_POST_HOOK" + acme_post_hook=${acme_post_hook:-$ACME_POST_HOOK} + if [[ -n "${acme_post_hook// }" ]]; then + params_issue_arr+=(--post-hook "$acme_post_hook") fi [[ ! -d "$config_home" ]] && mkdir -p "$config_home" diff --git a/app/letsencrypt_service_data.tmpl b/app/letsencrypt_service_data.tmpl index 456e6ad7..b7077ff2 100644 --- a/app/letsencrypt_service_data.tmpl +++ b/app/letsencrypt_service_data.tmpl @@ -31,6 +31,8 @@ LETSENCRYPT_CONTAINERS=( {{ $EAB_HMAC_KEY := trim (coalesce $container.Env.ACME_EAB_HMAC_KEY "") }} {{ $ZEROSSL_API_KEY := trim (coalesce $container.Env.ZEROSSL_API_KEY "") }} {{ $RESTART_CONTAINER := trim (coalesce $container.Env.LETSENCRYPT_RESTART_CONTAINER "") }} + {{ $PRE_HOOK := trim (coalesce $container.Env.ACME_PRE_HOOK "") }} + {{ $POST_HOOK := trim (coalesce $container.Env.ACME_POST_HOOK "") }} {{ $cid := printf "%.12s" $container.ID }} {{ if parseBool (coalesce $container.Env.LETSENCRYPT_SINGLE_DOMAIN_CERTS "false") }} {{/* Explicit per-domain splitting of the certificate */}} @@ -49,6 +51,8 @@ LETSENCRYPT_CONTAINERS=( {{- "\n" }}ACME_{{ $cid }}_{{ $hostHash }}_EAB_HMAC_KEY="{{ $EAB_HMAC_KEY }}" {{- "\n" }}ZEROSSL_{{ $cid }}_{{ $hostHash }}_API_KEY="{{ $ZEROSSL_API_KEY }}" {{- "\n" }}LETSENCRYPT_{{ $cid }}_{{ $hostHash }}_RESTART_CONTAINER="{{ $RESTART_CONTAINER }}" + {{- "\n" }}ACME_{{ $cid }}_{{ $hostHash }}_PRE_HOOK="{{ $PRE_HOOK }}" + {{- "\n" }}ACME_{{ $cid }}_{{ $hostHash }}_POST_HOOK="{{ $POST_HOOK }}" {{ end }} {{ else }} {{/* Default: multi-domain (SAN) certificate */}} @@ -69,6 +73,8 @@ LETSENCRYPT_CONTAINERS=( {{- "\n" }}ACME_{{ $cid }}_EAB_HMAC_KEY="{{ $EAB_HMAC_KEY }}" {{- "\n" }}ZEROSSL_{{ $cid }}_API_KEY="{{ $ZEROSSL_API_KEY }}" {{- "\n" }}LETSENCRYPT_{{ $cid }}_RESTART_CONTAINER="{{ $RESTART_CONTAINER }}" + {{- "\n" }}ACME_{{ $cid }}_PRE_HOOK="{{ $PRE_HOOK }}" + {{- "\n" }}ACME_{{ $cid }}_POST_HOOK="{{ $POST_HOOK }}" {{ end }} {{ end }} {{ end }} From 084766d0eb93df2f075c2376793090f319c7d50b Mon Sep 17 00:00:00 2001 From: Nicolas Duchon Date: Wed, 2 Mar 2022 18:55:04 +0100 Subject: [PATCH 7/9] tests: change run_nginx_container() parameter handling --- test/tests/test-functions.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/tests/test-functions.sh b/test/tests/test-functions.sh index 4934f78a..120067d5 100755 --- a/test/tests/test-functions.sh +++ b/test/tests/test-functions.sh @@ -95,10 +95,10 @@ function run_nginx_container { ;; -c|--cli-args) - local cli_args_str="${2:?}" - for arg in $cli_args_str; do - cli_args_arr+=("$arg") - done + local cli_args_arr_tmp + IFS=' ' read -r -a cli_args_arr_tmp <<< "${2:?}" + cli_args_arr+=("${cli_args_arr_tmp[0]}") #Head + cli_args_arr+=("${cli_args_arr_tmp[*]:1}") #Tail shift 2 ;; From 16f7e198df62a0564365622c1882ca6ce08df806 Mon Sep 17 00:00:00 2001 From: Nicolas Duchon Date: Wed, 2 Mar 2022 18:49:00 +0100 Subject: [PATCH 8/9] tests: per-container Pre-Hooks and Post-Hooks --- test/tests/acme_hooks/run.sh | 98 +++++++++++++++++++++++++++--------- 1 file changed, 73 insertions(+), 25 deletions(-) diff --git a/test/tests/acme_hooks/run.sh b/test/tests/acme_hooks/run.sh index f3412a77..1d231538 100755 --- a/test/tests/acme_hooks/run.sh +++ b/test/tests/acme_hooks/run.sh @@ -1,19 +1,25 @@ #!/bin/bash ## Test for the hooks of acme.sh -pre_hook_file="/tmp/prehook" -pre_hook_command="touch $pre_hook_file" -post_hook_file="/tmp/posthook" -post_hook_command="touch $post_hook_file" +default_pre_hook_file="/tmp/default_prehook" +default_pre_hook_command="touch $default_pre_hook_file" +default_post_hook_file="/tmp/default_posthook" +default_post_hook_ommand="touch $default_post_hook_file" +percontainer_pre_hook_file="/tmp/percontainer_prehook" +percontainer_pre_hook_command="touch $percontainer_pre_hook_file" +percontainer_post_hook_file="/tmp/percontainer_posthook" +percontainer_post_hook_command="touch $percontainer_post_hook_file" if [[ -z $GITHUB_ACTIONS ]]; then le_container_name="$(basename "${0%/*}")_$(date "+%Y-%m-%d_%H.%M.%S")" else le_container_name="$(basename "${0%/*}")" fi -run_le_container "${1:?}" "$le_container_name" --cli-args "--env ACME_PRE_HOOK=$pre_hook_command" --cli-args "--env ACME_POST_HOOK=$post_hook_command" +run_le_container "${1:?}" "$le_container_name" \ + --cli-args "--env ACME_PRE_HOOK=$default_pre_hook_command" \ + --cli-args "--env ACME_POST_HOOK=$default_post_hook_ommand" # Create the $domains array from comma separated domains in TEST_DOMAINS. IFS=',' read -r -a domains <<< "$TEST_DOMAINS" @@ -29,14 +35,27 @@ function cleanup { } trap cleanup EXIT -# Run an nginx container for ${domains[0]} with LETSENCRYPT_EMAIL set. container_email="contact@${domains[0]}" -run_nginx_container --hosts "${domains[0]}" --cli-args "--env LETSENCRYPT_EMAIL=${container_email}" + +# Run an nginx container for ${domains[0]} with LETSENCRYPT_EMAIL set. +run_nginx_container --hosts "${domains[0]}" \ + --cli-args "--env LETSENCRYPT_EMAIL=${container_email}" + +# Run an nginx container for ${domains[1]} with LETSENCRYPT_EMAIL, ACME_PRE_HOOK and ACME_POST_HOOK set. +run_nginx_container --hosts "${domains[1]}" \ + --cli-args "--env LETSENCRYPT_EMAIL=${container_email}" \ + --cli-args "--env ACME_PRE_HOOK=$percontainer_pre_hook_command" \ + --cli-args "--env ACME_POST_HOOK=$percontainer_post_hook_command" # Wait for a symlink at /etc/nginx/certs/${domains[0]}.crt wait_for_symlink "${domains[0]}" "$le_container_name" -##Check if the command is deliverd properly in /etc/acme.sh +acme_pre_hook_key="Le_PreHook=" +acme_post_hook_key="Le_PostHook=" +acme_base64_start="'__ACME_BASE64__START_" +acme_base64_end="__ACME_BASE64__END_'" + +# Check if the default command is deliverd properly in /etc/acme.sh if docker exec "$le_container_name" [[ ! -d "/etc/acme.sh/$container_email" ]]; then echo "The /etc/acme.sh/$container_email folder does not exist." elif docker exec "$le_container_name" [[ ! -d "/etc/acme.sh/$container_email/${domains[0]}" ]]; then @@ -44,28 +63,57 @@ elif docker exec "$le_container_name" [[ ! -d "/etc/acme.sh/$container_email/${d elif docker exec "$le_container_name" [[ ! -f "/etc/acme.sh/$container_email/${domains[0]}/${domains[0]}.conf" ]]; then echo "The /etc/acme.sh/$container_email/${domains[0]}/${domains[0]}.conf file does not exist." fi -acme_pre_hook_key="Le_PreHook=" -acme_post_hook_key="Le_PostHook=" -acme_base64_start="'__ACME_BASE64__START_" -acme_base64_end="__ACME_BASE64__END_'" -pre_hook_command_base64=$(echo -n "$pre_hook_command" | base64) -post_hook_command_base64=$(echo -n "$post_hook_command" | base64) -acme_pre_hook="$(docker exec "$le_container_name" grep "$acme_pre_hook_key" "/etc/acme.sh/$container_email/${domains[0]}/${domains[0]}.conf")" -acme_post_hook="$(docker exec "$le_container_name" grep "$acme_post_hook_key" "/etc/acme.sh/$container_email/${domains[0]}/${domains[0]}.conf")" +default_pre_hook_command_base64="${acme_pre_hook_key}${acme_base64_start}$(echo -n "$default_pre_hook_command" | base64)${acme_base64_end}" +default_post_hook_command_base64="${acme_post_hook_key}${acme_base64_start}$(echo -n "$default_post_hook_ommand" | base64)${acme_base64_end}" + +default_acme_pre_hook="$(docker exec "$le_container_name" grep "$acme_pre_hook_key" "/etc/acme.sh/$container_email/${domains[0]}/${domains[0]}.conf")" +default_acme_post_hook="$(docker exec "$le_container_name" grep "$acme_post_hook_key" "/etc/acme.sh/$container_email/${domains[0]}/${domains[0]}.conf")" + +if [[ "$default_pre_hook_command_base64" != "$default_acme_pre_hook" ]]; then + echo "Default prehook command not saved properly" +fi +if [[ "$default_post_hook_command_base64" != "$default_acme_post_hook" ]]; then + echo "Default posthook command not saved properly" +fi + + +# Check if the default action is performed +if docker exec "$le_container_name" [[ ! -f "$default_pre_hook_file" ]]; then + echo "Default prehook action failed" +fi +if docker exec "$le_container_name" [[ ! -f "$default_post_hook_file" ]]; then + echo "Default posthook action failed" +fi + +# Wait for a symlink at /etc/nginx/certs/${domains[1]}.crt +wait_for_symlink "${domains[1]}" "$le_container_name" + +# Check if the per-container command is deliverd properly in /etc/acme.sh +if docker exec "$le_container_name" [[ ! -d "/etc/acme.sh/$container_email/${domains[1]}" ]]; then + echo "The /etc/acme.sh/$container_email/${domains[1]} folder does not exist." +elif docker exec "$le_container_name" [[ ! -f "/etc/acme.sh/$container_email/${domains[1]}/${domains[1]}.conf" ]]; then + echo "The /etc/acme.sh/$container_email/${domains[1]}/${domains[1]}.conf file does not exist." +fi + +percontainer_pre_hook_command_base64="${acme_pre_hook_key}${acme_base64_start}$(echo -n "$percontainer_pre_hook_command" | base64)${acme_base64_end}" +percontainer_post_hook_command_base64="${acme_post_hook_key}${acme_base64_start}$(echo -n "$percontainer_post_hook_command" | base64)${acme_base64_end}" + +percontainer_acme_pre_hook="$(docker exec "$le_container_name" grep "$acme_pre_hook_key" "/etc/acme.sh/$container_email/${domains[1]}/${domains[1]}.conf")" +percontainer_acme_post_hook="$(docker exec "$le_container_name" grep "$acme_post_hook_key" "/etc/acme.sh/$container_email/${domains[1]}/${domains[1]}.conf")" -if [[ "$acme_pre_hook_key$acme_base64_start$pre_hook_command_base64$acme_base64_end" != "$acme_pre_hook" ]]; then - echo "Prehook command not saved properly" +if [[ "$percontainer_pre_hook_command_base64" != "$percontainer_acme_pre_hook" ]]; then + echo "Per-container prehook command not saved properly" fi -if [[ "$acme_post_hook_key$acme_base64_start$post_hook_command_base64$acme_base64_end" != "$acme_post_hook" ]]; then - echo "Posthook command not saved properly" +if [[ "$percontainer_post_hook_command_base64" != "$percontainer_acme_post_hook" ]]; then + echo "Per-container posthook command not saved properly" fi -## Check if the action ist performed -if docker exec "$le_container_name" [[ ! -f "$pre_hook_file" ]]; then - echo "Prehook action failed" +# Check if the percontainer action is performed +if docker exec "$le_container_name" [[ ! -f "$percontainer_pre_hook_file" ]]; then + echo "Per-container prehook action failed" fi -if docker exec "$le_container_name" [[ ! -f "$post_hook_file" ]]; then - echo "Posthook action failed" +if docker exec "$le_container_name" [[ ! -f "$percontainer_post_hook_file" ]]; then + echo "Per-container posthook action failed" fi From b4ccbf1e8f2ca9a25dbac2e471546a8dad12f9b3 Mon Sep 17 00:00:00 2001 From: Nicolas Duchon Date: Wed, 2 Mar 2022 19:28:16 +0100 Subject: [PATCH 9/9] docs: per-container Pre-Hooks and Post-Hooks --- docs/Hooks.md | 46 +++++++++++++++++++++++++++------- docs/Let's-Encrypt-and-ACME.md | 5 ++++ 2 files changed, 42 insertions(+), 9 deletions(-) diff --git a/docs/Hooks.md b/docs/Hooks.md index 34e2e0c7..53ded99b 100644 --- a/docs/Hooks.md +++ b/docs/Hooks.md @@ -1,9 +1,15 @@ ## Pre-Hooks and Post-Hooks -The Pre- and Post-Hooks of [acme.sh](https://github.com/acmesh-official/acme.sh/) are available through the corresponding environment variables. This allows to trigger actions just before and after certificates are issued (see [acme.sh documentation](https://github.com/acmesh-official/acme.sh/wiki/Using-pre-hook-post-hook-renew-hook-reloadcmd)) +The Pre- and Post-Hooks of [acme.sh](https://github.com/acmesh-official/acme.sh/) are available through the corresponding environment variables. This allows to trigger actions just before and after certificates are issued (see [acme.sh documentation](https://github.com/acmesh-official/acme.sh/wiki/Using-pre-hook-post-hook-renew-hook-reloadcmd)). -#### Pre-Hook -This command will be run before certificates are issued. For example `echo 'start'`: +If you set `ACME_PRE_HOOK` and/or `ACME_POST_HOOK` on the **acme-companion** container, **the actions for all certificates will be the same**. If you want specific actions to be run for specific certificates, set the `ACME_PRE_HOOK` / `ACME_POST_HOOK` environment variable(s) on the proxied container(s) instead. Default (on the **acme-companion** container) and per-container `ACME_PRE_HOOK` / `ACME_POST_HOOK` environment variables aren't combined : if both default and per-container variables are set for a given proxied container, the per-container variables will take precedence over the default. + +If you want to run the same default hooks for most containers but not for some of them, you can set the `ACME_PRE_HOOK` / `ACME_POST_HOOK` environment variables to the Bash noop operator (ie, `ACME_PRE_HOOK=:`) on those containers. + +#### Pre-Hook: `ACME_PRE_HOOK` +This command will be run before certificates are issued. + +For example `echo 'start'` on the **acme-companion** container (setting a default Pre-Hook): ```shell $ docker run --detach \ --name nginx-proxy-acme \ @@ -11,12 +17,24 @@ $ docker run --detach \ --volume /var/run/docker.sock:/var/run/docker.sock:ro \ --volume acme:/etc/acme.sh \ --env "DEFAULT_EMAIL=mail@yourdomain.tld" \ - --env "ACME_PRE_HOOK=echo 'start'" + --env "ACME_PRE_HOOK=echo 'start'" \ nginxproxy/acme-companion ``` -#### Post-Hook -This command will be run after certificates are issued. For example `echo 'end'`: +And on a proxied container (setting a per-container Pre-Hook): +```shell +$ docker run --detach \ + --name your-proxyed-app \ + --env "VIRTUAL_HOST=yourdomain.tld" \ + --env "LETSENCRYPT_HOST=yourdomain.tld" \ + --env "ACME_PRE_HOOK=echo 'start'" \ + nginx +``` + +#### Post-Hook: `ACME_POST_HOOK` +This command will be run after certificates are issued. + +For example `echo 'end'` on the **acme-companion** container (setting a default Post-Hook): ```shell $ docker run --detach \ --name nginx-proxy-acme \ @@ -24,10 +42,20 @@ $ docker run --detach \ --volume /var/run/docker.sock:/var/run/docker.sock:ro \ --volume acme:/etc/acme.sh \ --env "DEFAULT_EMAIL=mail@yourdomain.tld" \ - --env "ACME_POST_HOOK=echo 'end'" + --env "ACME_POST_HOOK=echo 'end'" \ nginxproxy/acme-companion ``` +And on a proxied container (setting a per-container Post-Hook): +```shell +$ docker run --detach \ + --name your-proxyed-app \ + --env "VIRTUAL_HOST=yourdomain.tld" \ + --env "LETSENCRYPT_HOST=yourdomain.tld" \ + --env "ACME_POST_HOOK=echo 'start'" \ + nginx +``` + #### Verification: If you want to check wether the hook-command is delivered properly to [acme.sh](https://github.com/acmesh-official/acme.sh/), you should check `/etc/acme.sh/[EMAILADDRESS]/[DOMAIN]/[DOMAIN].conf`. The variable `Le_PreHook` contains the Pre-Hook-Command base64 encoded. @@ -35,8 +63,8 @@ The variable `Le_PostHook` contains the Pre-Hook-Command base64 encoded. #### Limitations * The commands that can be used in the hooks are limited to the commands available inside the **acme-companion** container. `curl` and `wget` are available, therefore it is possible to communicate with tools outside the container via HTTP, allowing for complex actions to be implemented outside or in other containers. -* The hooks are general options, therefore **the actions for all certificates are the same**. #### Use-cases -* Change some firewall rules just for the issuing process of the certificates, so the ports 80 and/or 443 don't have to be publicly reachable at all time. +* Changing some firewall rules just for the ACME authorization, so the ports 80 and/or 443 don't have to be publicly reachable at all time. +* Certificate "post processing" / conversion to another format. * Monitoring. \ No newline at end of file diff --git a/docs/Let's-Encrypt-and-ACME.md b/docs/Let's-Encrypt-and-ACME.md index 5cb59840..8633a69b 100644 --- a/docs/Let's-Encrypt-and-ACME.md +++ b/docs/Let's-Encrypt-and-ACME.md @@ -75,6 +75,11 @@ If the ACME CA provides multiple cert chain, you can use the `ACME_PREFERRED_CHA The `LETSENCRYPT_RESTART_CONTAINER` environment variable, when set to `true` on an application container, will restart this container whenever the corresponding cert (`LETSENCRYPT_HOST`) is renewed. This is useful when certificates are directly used inside a container for other purposes than HTTPS (e.g. an FTPS server), to make sure those containers always use an up to date certificate. +#### Pre-Hook and Post-Hook + +The `ACME_PRE_HOOK` and `ACME_POST_HOOK` let you use the [`acme.sh` Pre- and Post-Hooks feature](https://github.com/acmesh-official/acme.sh/wiki/Using-pre-hook-post-hook-renew-hook-reloadcmd) to run commands respectively before and after the container's certificate has been issued. For more information see [Pre- and Post-Hook](./Hooks.md) + + ### global (set on acme-companion container) #### Default contact address