Skip to content

Commit b3f4e40

Browse files
authored
Merge pull request #898 from TreeN0de/TreeN0de-pre/post-hook
Adding pre- and post-hook
2 parents 4b638eb + b4ccbf1 commit b3f4e40

File tree

11 files changed

+250
-9
lines changed

11 files changed

+250
-9
lines changed

.github/workflows/test.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ jobs:
8383
permissions_default,
8484
permissions_custom,
8585
symlinks,
86+
acme_hooks,
8687
]
8788
setup: [2containers, 3containers]
8889
acme-ca: [pebble]

app/letsencrypt_service

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,18 @@ function update_cert {
220220
--fullchain-file "${certificate_dir}/fullchain.pem" \
221221
)
222222

223+
# acme.sh pre and post hooks
224+
local -n acme_pre_hook="ACME_${cid}_PRE_HOOK"
225+
acme_pre_hook=${acme_pre_hook:-$ACME_PRE_HOOK}
226+
if [[ -n "${acme_pre_hook// }" ]]; then
227+
params_issue_arr+=(--pre-hook "$acme_pre_hook")
228+
fi
229+
local -n acme_post_hook="ACME_${cid}_POST_HOOK"
230+
acme_post_hook=${acme_post_hook:-$ACME_POST_HOOK}
231+
if [[ -n "${acme_post_hook// }" ]]; then
232+
params_issue_arr+=(--post-hook "$acme_post_hook")
233+
fi
234+
223235
[[ ! -d "$config_home" ]] && mkdir -p "$config_home"
224236
params_base_arr+=(--config-home "$config_home")
225237
local account_file="${config_home}/ca/${ca_dir}/account.json"

app/letsencrypt_service_data.tmpl

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ LETSENCRYPT_CONTAINERS=(
3131
{{ $EAB_HMAC_KEY := trim (coalesce $container.Env.ACME_EAB_HMAC_KEY "") }}
3232
{{ $ZEROSSL_API_KEY := trim (coalesce $container.Env.ZEROSSL_API_KEY "") }}
3333
{{ $RESTART_CONTAINER := trim (coalesce $container.Env.LETSENCRYPT_RESTART_CONTAINER "") }}
34+
{{ $PRE_HOOK := trim (coalesce $container.Env.ACME_PRE_HOOK "") }}
35+
{{ $POST_HOOK := trim (coalesce $container.Env.ACME_POST_HOOK "") }}
3436
{{ $cid := printf "%.12s" $container.ID }}
3537
{{ if parseBool (coalesce $container.Env.LETSENCRYPT_SINGLE_DOMAIN_CERTS "false") }}
3638
{{/* Explicit per-domain splitting of the certificate */}}
@@ -49,6 +51,8 @@ LETSENCRYPT_CONTAINERS=(
4951
{{- "\n" }}ACME_{{ $cid }}_{{ $hostHash }}_EAB_HMAC_KEY="{{ $EAB_HMAC_KEY }}"
5052
{{- "\n" }}ZEROSSL_{{ $cid }}_{{ $hostHash }}_API_KEY="{{ $ZEROSSL_API_KEY }}"
5153
{{- "\n" }}LETSENCRYPT_{{ $cid }}_{{ $hostHash }}_RESTART_CONTAINER="{{ $RESTART_CONTAINER }}"
54+
{{- "\n" }}ACME_{{ $cid }}_{{ $hostHash }}_PRE_HOOK="{{ $PRE_HOOK }}"
55+
{{- "\n" }}ACME_{{ $cid }}_{{ $hostHash }}_POST_HOOK="{{ $POST_HOOK }}"
5256
{{ end }}
5357
{{ else }}
5458
{{/* Default: multi-domain (SAN) certificate */}}
@@ -69,6 +73,8 @@ LETSENCRYPT_CONTAINERS=(
6973
{{- "\n" }}ACME_{{ $cid }}_EAB_HMAC_KEY="{{ $EAB_HMAC_KEY }}"
7074
{{- "\n" }}ZEROSSL_{{ $cid }}_API_KEY="{{ $ZEROSSL_API_KEY }}"
7175
{{- "\n" }}LETSENCRYPT_{{ $cid }}_RESTART_CONTAINER="{{ $RESTART_CONTAINER }}"
76+
{{- "\n" }}ACME_{{ $cid }}_PRE_HOOK="{{ $PRE_HOOK }}"
77+
{{- "\n" }}ACME_{{ $cid }}_POST_HOOK="{{ $POST_HOOK }}"
7278
{{ end }}
7379
{{ end }}
7480
{{ end }}

docs/Container-configuration.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,8 @@ You can also create test certificates per container (see [Test certificates](./L
2828

2929
* `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.
3030

31-
* `CERTS_UPDATE_INTERVAL` - 3600 seconds by default, this defines how often the container will check if the certificates require update.
31+
* `CERTS_UPDATE_INTERVAL` - 3600 seconds by default, this defines how often the container will check if the certificates require update.
32+
33+
* `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)
34+
35+
* `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)

docs/Hooks.md

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
## Pre-Hooks and Post-Hooks
2+
3+
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)).
4+
5+
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.
6+
7+
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.
8+
9+
#### Pre-Hook: `ACME_PRE_HOOK`
10+
This command will be run before certificates are issued.
11+
12+
For example `echo 'start'` on the **acme-companion** container (setting a default Pre-Hook):
13+
```shell
14+
$ docker run --detach \
15+
--name nginx-proxy-acme \
16+
--volumes-from nginx-proxy \
17+
--volume /var/run/docker.sock:/var/run/docker.sock:ro \
18+
--volume acme:/etc/acme.sh \
19+
--env "DEFAULT_EMAIL=mail@yourdomain.tld" \
20+
--env "ACME_PRE_HOOK=echo 'start'" \
21+
nginxproxy/acme-companion
22+
```
23+
24+
And on a proxied container (setting a per-container Pre-Hook):
25+
```shell
26+
$ docker run --detach \
27+
--name your-proxyed-app \
28+
--env "VIRTUAL_HOST=yourdomain.tld" \
29+
--env "LETSENCRYPT_HOST=yourdomain.tld" \
30+
--env "ACME_PRE_HOOK=echo 'start'" \
31+
nginx
32+
```
33+
34+
#### Post-Hook: `ACME_POST_HOOK`
35+
This command will be run after certificates are issued.
36+
37+
For example `echo 'end'` on the **acme-companion** container (setting a default Post-Hook):
38+
```shell
39+
$ docker run --detach \
40+
--name nginx-proxy-acme \
41+
--volumes-from nginx-proxy \
42+
--volume /var/run/docker.sock:/var/run/docker.sock:ro \
43+
--volume acme:/etc/acme.sh \
44+
--env "DEFAULT_EMAIL=mail@yourdomain.tld" \
45+
--env "ACME_POST_HOOK=echo 'end'" \
46+
nginxproxy/acme-companion
47+
```
48+
49+
And on a proxied container (setting a per-container Post-Hook):
50+
```shell
51+
$ docker run --detach \
52+
--name your-proxyed-app \
53+
--env "VIRTUAL_HOST=yourdomain.tld" \
54+
--env "LETSENCRYPT_HOST=yourdomain.tld" \
55+
--env "ACME_POST_HOOK=echo 'start'" \
56+
nginx
57+
```
58+
59+
#### Verification:
60+
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`.
61+
The variable `Le_PreHook` contains the Pre-Hook-Command base64 encoded.
62+
The variable `Le_PostHook` contains the Pre-Hook-Command base64 encoded.
63+
64+
#### Limitations
65+
* 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.
66+
67+
#### Use-cases
68+
* 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.
69+
* Certificate "post processing" / conversion to another format.
70+
* Monitoring.

docs/Let's-Encrypt-and-ACME.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,11 @@ If the ACME CA provides multiple cert chain, you can use the `ACME_PREFERRED_CHA
7575

7676
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.
7777

78+
#### Pre-Hook and Post-Hook
79+
80+
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)
81+
82+
7883
### global (set on acme-companion container)
7984

8085
#### Default contact address

docs/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222

2323
[Zero SSL](./Zero-SSL.md)
2424

25+
[Pre-Hooks and Post-Hooks](./Hooks.md)
26+
2527
#### Troubleshooting:
2628

2729
[Invalid / failing authorizations](./Invalid-authorizations.md)

test/config.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ globalTests+=(
1616
permissions_default
1717
permissions_custom
1818
symlinks
19+
acme_hooks
1920
)
2021

2122
# The ocsp_must_staple test does not work with Pebble
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+

test/tests/acme_hooks/run.sh

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
#!/bin/bash
2+
3+
## Test for the hooks of acme.sh
4+
5+
default_pre_hook_file="/tmp/default_prehook"
6+
default_pre_hook_command="touch $default_pre_hook_file"
7+
default_post_hook_file="/tmp/default_posthook"
8+
default_post_hook_ommand="touch $default_post_hook_file"
9+
10+
percontainer_pre_hook_file="/tmp/percontainer_prehook"
11+
percontainer_pre_hook_command="touch $percontainer_pre_hook_file"
12+
percontainer_post_hook_file="/tmp/percontainer_posthook"
13+
percontainer_post_hook_command="touch $percontainer_post_hook_file"
14+
15+
if [[ -z $GITHUB_ACTIONS ]]; then
16+
le_container_name="$(basename "${0%/*}")_$(date "+%Y-%m-%d_%H.%M.%S")"
17+
else
18+
le_container_name="$(basename "${0%/*}")"
19+
fi
20+
run_le_container "${1:?}" "$le_container_name" \
21+
--cli-args "--env ACME_PRE_HOOK=$default_pre_hook_command" \
22+
--cli-args "--env ACME_POST_HOOK=$default_post_hook_ommand"
23+
24+
# Create the $domains array from comma separated domains in TEST_DOMAINS.
25+
IFS=',' read -r -a domains <<< "$TEST_DOMAINS"
26+
27+
# Cleanup function with EXIT trap
28+
function cleanup {
29+
# Remove the Nginx container silently.
30+
docker rm --force "${domains[0]}" &> /dev/null
31+
# Cleanup the files created by this run of the test to avoid foiling following test(s).
32+
docker exec "$le_container_name" /app/cleanup_test_artifacts
33+
# Stop the LE container
34+
docker stop "$le_container_name" > /dev/null
35+
}
36+
trap cleanup EXIT
37+
38+
container_email="contact@${domains[0]}"
39+
40+
# Run an nginx container for ${domains[0]} with LETSENCRYPT_EMAIL set.
41+
run_nginx_container --hosts "${domains[0]}" \
42+
--cli-args "--env LETSENCRYPT_EMAIL=${container_email}"
43+
44+
# Run an nginx container for ${domains[1]} with LETSENCRYPT_EMAIL, ACME_PRE_HOOK and ACME_POST_HOOK set.
45+
run_nginx_container --hosts "${domains[1]}" \
46+
--cli-args "--env LETSENCRYPT_EMAIL=${container_email}" \
47+
--cli-args "--env ACME_PRE_HOOK=$percontainer_pre_hook_command" \
48+
--cli-args "--env ACME_POST_HOOK=$percontainer_post_hook_command"
49+
50+
# Wait for a symlink at /etc/nginx/certs/${domains[0]}.crt
51+
wait_for_symlink "${domains[0]}" "$le_container_name"
52+
53+
acme_pre_hook_key="Le_PreHook="
54+
acme_post_hook_key="Le_PostHook="
55+
acme_base64_start="'__ACME_BASE64__START_"
56+
acme_base64_end="__ACME_BASE64__END_'"
57+
58+
# Check if the default command is deliverd properly in /etc/acme.sh
59+
if docker exec "$le_container_name" [[ ! -d "/etc/acme.sh/$container_email" ]]; then
60+
echo "The /etc/acme.sh/$container_email folder does not exist."
61+
elif docker exec "$le_container_name" [[ ! -d "/etc/acme.sh/$container_email/${domains[0]}" ]]; then
62+
echo "The /etc/acme.sh/$container_email/${domains[0]} folder does not exist."
63+
elif docker exec "$le_container_name" [[ ! -f "/etc/acme.sh/$container_email/${domains[0]}/${domains[0]}.conf" ]]; then
64+
echo "The /etc/acme.sh/$container_email/${domains[0]}/${domains[0]}.conf file does not exist."
65+
fi
66+
67+
default_pre_hook_command_base64="${acme_pre_hook_key}${acme_base64_start}$(echo -n "$default_pre_hook_command" | base64)${acme_base64_end}"
68+
default_post_hook_command_base64="${acme_post_hook_key}${acme_base64_start}$(echo -n "$default_post_hook_ommand" | base64)${acme_base64_end}"
69+
70+
default_acme_pre_hook="$(docker exec "$le_container_name" grep "$acme_pre_hook_key" "/etc/acme.sh/$container_email/${domains[0]}/${domains[0]}.conf")"
71+
default_acme_post_hook="$(docker exec "$le_container_name" grep "$acme_post_hook_key" "/etc/acme.sh/$container_email/${domains[0]}/${domains[0]}.conf")"
72+
73+
if [[ "$default_pre_hook_command_base64" != "$default_acme_pre_hook" ]]; then
74+
echo "Default prehook command not saved properly"
75+
fi
76+
if [[ "$default_post_hook_command_base64" != "$default_acme_post_hook" ]]; then
77+
echo "Default posthook command not saved properly"
78+
fi
79+
80+
81+
# Check if the default action is performed
82+
if docker exec "$le_container_name" [[ ! -f "$default_pre_hook_file" ]]; then
83+
echo "Default prehook action failed"
84+
fi
85+
if docker exec "$le_container_name" [[ ! -f "$default_post_hook_file" ]]; then
86+
echo "Default posthook action failed"
87+
fi
88+
89+
# Wait for a symlink at /etc/nginx/certs/${domains[1]}.crt
90+
wait_for_symlink "${domains[1]}" "$le_container_name"
91+
92+
# Check if the per-container command is deliverd properly in /etc/acme.sh
93+
if docker exec "$le_container_name" [[ ! -d "/etc/acme.sh/$container_email/${domains[1]}" ]]; then
94+
echo "The /etc/acme.sh/$container_email/${domains[1]} folder does not exist."
95+
elif docker exec "$le_container_name" [[ ! -f "/etc/acme.sh/$container_email/${domains[1]}/${domains[1]}.conf" ]]; then
96+
echo "The /etc/acme.sh/$container_email/${domains[1]}/${domains[1]}.conf file does not exist."
97+
fi
98+
99+
percontainer_pre_hook_command_base64="${acme_pre_hook_key}${acme_base64_start}$(echo -n "$percontainer_pre_hook_command" | base64)${acme_base64_end}"
100+
percontainer_post_hook_command_base64="${acme_post_hook_key}${acme_base64_start}$(echo -n "$percontainer_post_hook_command" | base64)${acme_base64_end}"
101+
102+
percontainer_acme_pre_hook="$(docker exec "$le_container_name" grep "$acme_pre_hook_key" "/etc/acme.sh/$container_email/${domains[1]}/${domains[1]}.conf")"
103+
percontainer_acme_post_hook="$(docker exec "$le_container_name" grep "$acme_post_hook_key" "/etc/acme.sh/$container_email/${domains[1]}/${domains[1]}.conf")"
104+
105+
if [[ "$percontainer_pre_hook_command_base64" != "$percontainer_acme_pre_hook" ]]; then
106+
echo "Per-container prehook command not saved properly"
107+
fi
108+
if [[ "$percontainer_post_hook_command_base64" != "$percontainer_acme_post_hook" ]]; then
109+
echo "Per-container posthook command not saved properly"
110+
fi
111+
112+
113+
# Check if the percontainer action is performed
114+
if docker exec "$le_container_name" [[ ! -f "$percontainer_pre_hook_file" ]]; then
115+
echo "Per-container prehook action failed"
116+
fi
117+
if docker exec "$le_container_name" [[ ! -f "$percontainer_post_hook_file" ]]; then
118+
echo "Per-container posthook action failed"
119+
fi

0 commit comments

Comments
 (0)