1
0
Fork 0

Merge pull request #898 from TreeN0de/TreeN0de-pre/post-hook

Adding pre- and post-hook
This commit is contained in:
Nicolas Duchon 2022-03-02 19:42:29 +01:00 committed by GitHub
commit b3f4e40c05
Signed by: GitHub
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 250 additions and 9 deletions

View File

@ -83,6 +83,7 @@ jobs:
permissions_default,
permissions_custom,
symlinks,
acme_hooks,
]
setup: [2containers, 3containers]
acme-ca: [pebble]

View File

@ -220,6 +220,18 @@ function update_cert {
--fullchain-file "${certificate_dir}/fullchain.pem" \
)
# acme.sh pre and post hooks
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
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"
params_base_arr+=(--config-home "$config_home")
local account_file="${config_home}/ca/${ca_dir}/account.json"

View File

@ -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 }}

View File

@ -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.
* `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)

70
docs/Hooks.md Normal file
View File

@ -0,0 +1,70 @@
## 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)).
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 \
--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
```
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 \
--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
```
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.
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.
#### Use-cases
* 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.

View File

@ -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

View File

@ -22,6 +22,8 @@
[Zero SSL](./Zero-SSL.md)
[Pre-Hooks and Post-Hooks](./Hooks.md)
#### Troubleshooting:
[Invalid / failing authorizations](./Invalid-authorizations.md)

View File

@ -16,6 +16,7 @@ globalTests+=(
permissions_default
permissions_custom
symlinks
acme_hooks
)
# The ocsp_must_staple test does not work with Pebble

View File

@ -0,0 +1 @@

119
test/tests/acme_hooks/run.sh Executable file
View File

@ -0,0 +1,119 @@
#!/bin/bash
## Test for the hooks of acme.sh
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=$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"
# 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
container_email="contact@${domains[0]}"
# 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"
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
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
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 [[ "$percontainer_pre_hook_command_base64" != "$percontainer_acme_pre_hook" ]]; then
echo "Per-container prehook command not saved properly"
fi
if [[ "$percontainer_post_hook_command_base64" != "$percontainer_acme_post_hook" ]]; then
echo "Per-container posthook command not saved properly"
fi
# 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 "$percontainer_post_hook_file" ]]; then
echo "Per-container posthook action failed"
fi

View File

@ -13,10 +13,30 @@ 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
cli_args_arr+=("${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 +50,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 +59,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" \
@ -75,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
;;