Hopefully this is useful to someone else. I got confused by the language change from renew
to deploy
hooks and spent some time ripping the code apart to see how the hooks actually work. I’ve broken down where the hooks are defined, their configuration, and how you can modify them.
Notes
When I started this, certbot
was on 0.19
. There’s a really good chance it’s been updated when you’re reading this, especially if you’re trying to update older usage. I’ve tried to mention the versions when talking about things, and I’ve linked the version-tagged files instead of the files from the default branch.
certbot
is solid, organic FOSS written by many capable devs, each with different ideas about how to structure a Python repo. I don’t have a solid grasp on Python yet. I haven’t yet encountered useful tools to break code down and trace execution, so my breakdowns exclusively use the source. My interpretation of the source might be horribly wrong.
When I generalize “hooks” in this post, I’m referring to renew_hook
s and deploy_hook
s. There are also pre_hook
s and post_hook
s that probably shouldn’t get lumped in. For example, when I say “all the hooks are renew_hook
s”, I’m not including (pre|post)_hook
s. I explicitly mention them when they’re pertinent.
Finally, I’m releasing this without all the code I wanted to write. It’s been on the backburner for a couple of weekends. I had originally planned to do a more detailed execution analysis with examples, but I need some of this info in a post I’m working on now. I’m still planning on (eventually) doing that, so stay tuned.
Overview
I had some SSL issues several weekends ago. Something apparently went wrong with my cron
job, and, while trying to verify everything, I realized the CLI flags were different. I was running this command (with absolute paths to mimic cron
):
/usr/bin/certbot renew --quiet --renew-hook "systemctl restart nginx"
However, I couldn’t find it mentioned in the docs:
$ /usr/bin/certbot --help renew | grep -- --renew-hook || (state=$? && echo "whoops" && $(exit $state))
whoops
I did notice --deploy-hook
after reading the entire output:
$ /usr/bin/certbot --help renew | awk 'BEGIN { inside_desired_block = 0; }; /^\s*--deploy-hook/{ inside_desired_block = 1; }; (/^\s*--/ && !/^\s*--deploy-hook/){ inside_desired_block = 0; }; inside_desired_block { print; }'
--deploy-hook DEPLOY_HOOK
Command to be run in a shell once for each
successfully issued certificate. For this command, the
shell variable $RENEWED_LINEAGE will point to the
config live subdirectory (for example,
"/etc/letsencrypt/live/example.com") containing the
new certificates and keys; the shell variable
$RENEWED_DOMAINS will contain a space-delimited list
of renewed certificate domains (for example,
"example.com www.example.com" (default: None)
That sounded exactly like what I remembered reading about --renew-hook
.
Initial Change
The
--renew-hook
flag has been hidden in favor of--deploy-hook
.
renew_hook
was, essentially, hidden behind an adapter, deploy_hook
. My best guess for reasoning is that the functions involved are actually triggered whenever a cert is successfully built, which happens on run
, certonly
, and renew
. Anything passed as a deploy_hook
was duplicated as a renew_hook
, which the major difference being, quite literally, the function called to access them.
(Note: nailing down the precise design pattern used here requires more pedantry than I’m willing to invest right now. deploy_hook
, at inception, was an adapter/wrapper/convenience method that calls renew_hook
when a renew_hook
is called a deploy_hook
instead of a renew_hook
.)
CLI
--renew-hook
was suppressed incli.py
. It’s still parsed, just not exposed in the help.- In its place,
--deploy-hook
was added. - At this point, a
deploy_hook
is arenew_hook
or everything is arenew_hook
.
Hook Definitions
renew_hook
was unchanged inhooks.py
(aside from some output language).deploy_hook
was created as an adapter torenew_hook
. The conditional is deceptive;config.deploy_hook == config.renew_hook
so there’s no reason to not just explicitly callrenew_hook
.
Execution
When a cert is installed for any reason (renew|certonly|run
),
Either
- if the cert already exists and was changed, execute
renew_hook
by callingrenewal.renew_cert
- if the cert is new and was successfully created, execute
deploy_hook
- if the cert already exists and was changed, execute
Current API
This section uses tag v0.19.0
, which was current when I wrote this.
CLI
There weren’t many changes from 0.17
in the CLI setup.
--renew-hook
is suppressed incli.py
. It’s still parsed, just not exposed in the help--deploy-hook
masks--renew-hook
- Either all the
deploy_hook
s are duplicated asrenew_hook
s or there are onlyrenew_hook
s
External Hooks
certbot
can pick up hooks from its configuration file. I can’t find actual documentation of this feature (e.g. ctrl+f renew_hook
), but it’s built by renewal
. Specifically, you can create something like this:
...
[renewalparams]
renew_hook = echo 'do stuff'
...
This will be loaded as a renew_hook
after bootstrapping, so, in theory, it will only be executed by renew_hook
(unlike CLI-created deploy_hook
s, for example), but I haven’t tested that. They’re loaded by restore_required_config_elements
, via _reconstitute
, via handle_renewal_request
, which seems to only appear in main
, where it’s called by renew
.
Similarly, you can define global hooks, placed in (most likely) /etc/letsencrypt/renewal-hooks/deploy
. As explained below, these are not executed by deploy_hook
but rather by renew_hook
, i.e. they’re only going to run with renew
. You can possibly trigger them by explicitly setting a deploy_hook
via the CLI, to force creation of the renew_hook
attribute, but I haven’t tested this.
Hook Definitions
The hooks have been extensively refactored (which kinda happens often at major version 0
). deploy_hook
is similar in name to the primary runner, _run_deploy_hook
. However, the deploy_hook
function is much slimmer than renew_hook
, implying a continued reliance on renew_hook
.
_run_deploy_hook
is a straight-forward runner. I’d argue its primary purpose is to collect command logging and state logic, which it does admirably.deploy_hook
is a simple conditional that executes any defined hooks.renew_hook
is a bit larger. It begins by loading, caching, and executing any hooks inrenewal_deploy_hooks_dir
. The directory is created inconfiguration
as a join onconfig_dir
(which is likely this default), theRENEWAL_HOOKS_DIR
constant, and theRENEWAL_DEPLOY_HOOKS_DIR
constant. Once finished, it executes anyrenew_hook
that wasn’t included inrenewal_deploy_hooks_dir
.
Execution
This breakdown is a bit messier than before, with a few more branches to trace. When a cert is installed for any reason (renew|certonly|run
),
- if renewing and the external
pre_hook
s directory exists, run the globalpre_hook
s. - run any defined
pre_hook
s (if they weren’t already run)
- if renewing and the external
Either
if the cert already exists and was changed, execute
renew_hook
by callingrenewal.renew_cert
if the cert is new and was successfully created, execute
deploy_hook
, which skips the externaldeploy_hook
directory
- if renewing and the external
post_hook
s directory exists, run the globalpost_hook
s. - run any defined
post_hook
s (if they weren’t already run)
- if renewing and the external
So What?
Expect Change
In the couple of weekends it took me to get back to this post, v0.20
was released. I haven’t had a chance to look at it yet. This is great, active FOSS. Don’t expect the minutae to work as intended for awhile yet. To quote semver,
Major version zero (
0.y.z
) is for initial development. Anything may change at any time. The public API should not be considered stable.
Manually Run Hooks After Initial Creation
For some reason there’s a disconnect between deploy_hook
and renew_hook
on installation (confirmed in v0.20
). If you’ve built an external hook in /etc/letsencrypt/renewal-hooks/deploy
that, say, restarts your webserver, it’s not going to get triggered with a brand new cert. However, it will get run every time you renew
afterward.
Create a Generic Server Restart Hook
Something like this in /etc/letsencrypt/renewal-hooks/deploy
will successfully reload any new configuration on any of your new sites (assuming they’re all run by the same webserver). It’s only going to get triggered on a successful update, so it shouldn’t run wild.
Nginx
#!/bin/bash
nginx -t && systemctl restart nginx
Apache
Might have to use apache2ctl
or httpd
.
#!/bin/bash
apachectl -t && systemctl restart apachectl
Pre- and Post-Hooks are Always Run
No matter what, certbot
starts with pre_hook
s and finishes with post_hook
s. I don’t have a good use for either, so I’m a bit short on examples. However, know the hooks are there and can be used.
Final Note
Let’s Encrypt is a fantastic service. If you like what they do, i.e. appreciate how accessible they’ve made secure web traffic, please donate. EFF’s certbot
is what powers my site (and basically anything I work on these days); consider buying them a beer (it’s really just a donate link but you catch my drift).