The certbot Hook API

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):

Initial Change

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.)

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.

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).