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_hooks and deploy_hooks. There are also pre_hooks and post_hooks that probably shouldn’t get lumped in. For example, when I say “all the hooks are renew_hooks”, I’m not including (pre|post)_hooks. 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-hookflag 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-hookwas suppressed incli.py. It’s still parsed, just not exposed in the help.- In its place,
--deploy-hookwas added. - At this point, a
deploy_hookis arenew_hookor everything is arenew_hook.
Hook Definitions
renew_hookwas unchanged inhooks.py(aside from some output language).deploy_hookwas created as an adapter torenew_hook. The conditional is deceptive;config.deploy_hook == config.renew_hookso 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_hookby 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-hookis suppressed incli.py. It’s still parsed, just not exposed in the help--deploy-hookmasks--renew-hook- Either all the
deploy_hooks are duplicated asrenew_hooks or there are onlyrenew_hooks
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_hooks, 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_hookis a straight-forward runner. I’d argue its primary purpose is to collect command logging and state logic, which it does admirably.deploy_hookis a simple conditional that executes any defined hooks.renew_hookis a bit larger. It begins by loading, caching, and executing any hooks inrenewal_deploy_hooks_dir. The directory is created inconfigurationas a join onconfig_dir(which is likely this default), theRENEWAL_HOOKS_DIRconstant, and theRENEWAL_DEPLOY_HOOKS_DIRconstant. Once finished, it executes anyrenew_hookthat 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_hooks directory exists, run the globalpre_hooks. - run any defined
pre_hooks (if they weren’t already run)
- if renewing and the external
Either
if the cert already exists and was changed, execute
renew_hookby callingrenewal.renew_certif the cert is new and was successfully created, execute
deploy_hook, which skips the externaldeploy_hookdirectory
- if renewing and the external
post_hooks directory exists, run the globalpost_hooks. - run any defined
post_hooks (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_hooks and finishes with post_hooks. 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).
