Skip to content

Instantly share code, notes, and snippets.

@MagnusOxlund
Last active December 6, 2024 05:59
Show Gist options
  • Save MagnusOxlund/72bed00bda6e7ab6a8132b9ae839aafc to your computer and use it in GitHub Desktop.
Save MagnusOxlund/72bed00bda6e7ab6a8132b9ae839aafc to your computer and use it in GitHub Desktop.
Certbot renewal hook priority
# Script that tests the order and priority of Certbot's four renewal hook types:
# * Command line argument hooks
# * Certificate renewal configuration hooks
# * Global configuration hooks
# * Renewal directory hooks
# The script has not been verified to be safe. Use at your own risk.
# --------------
# WARNING PROMPT
# --------------
echo "Warning: this test will erase your global Certbot config file at /etc/letsencrypt/cli.ini as well as files in /etc/letsencrypt/renewal-hooks/"
echo "Do you wish to continue?(yes/no)"
read input
if [ "$input" != "yes" ]; then
exit 1
fi
# ------------------
# CERT SELECT PROMPT
# ------------------
ls /etc/letsencrypt/renewal/ | grep -oP '.*\.conf' | sed 's/\.conf$//'
echo "-----"
read -p "Select a certificate from the list above to use for testing: " certName
# -----------
# DEFINITIONS
# -----------
# Test environment
basicSafetyCheck() {
if [ "$certName" == "" ]; then
echo "No test certificate set. Aborting."
exit 1
fi
if [ ! -f "$localConfig" ]; then
echo "Certificate doesn't exist. Aborting."
exit 1
fi
checkLocalConfig | grep -q "no hooks in local config"
if [ $? == 1 ]; then
echo "Test certificate's renewal config may not contain hooks. Aborting"
exit 1
fi
echo -e "commencing test using $certName for" $(certbot --version)
}
prepareTestEnv() {
echo "-- preparing test environment --"
checkLocalConfig
createLocalConfigBackup
checkGlobalConfig
checkDirHooks
echo "-- environment ready -- "
echo -e "\n\n\n"
}
resetTestEnv() {
echo "-- resetting test environment --"
removeGlobalConfig
removeDirHooks
restoreLocalConfig
checkGlobalConfig
checkDirHooks
checkLocalConfig
removeLocalConfigBackup
echo "-- environment reset --"
}
# CLI hooks
renewSansHooks() {
echo "running renew without hooks"
sudo certbot renew --cert-name $certName --dry-run --run-deploy-hooks --non-interactive
}
renewWithCLIHooks() {
echo "running renew with CLI hooks"
sudo certbot renew --cert-name $certName --dry-run --run-deploy-hooks --non-interactive --pre-hook "echo CLI pre" --post-hook "echo CLI post" --deploy-hook "echo CLI deploy"
}
renewWithCLIHooksEmptyStr() {
echo "running renew with CLI hooks set to empty strings"
sudo certbot renew --cert-name $certName --dry-run --run-deploy-hooks --non-interactive --pre-hook "" --post-hook "" --deploy-hook ""
}
renewWithCLIHooksDirHooks() {
echo "running renew with CLI hooks set to directory hooks"
sudo certbot renew --cert-name $certName --dry-run --run-deploy-hooks --non-interactive --pre-hook "$dirHookPre" --post-hook "$dirHookPost" --deploy-hook "$dirHookDeploy"
}
# Local/global config hook keys
configHooks=(
"pre_hook"
"post_hook"
"deploy_hook"
"renew_hook"
"pre-hook"
"post-hook"
"deploy-hook"
"renew-hook"
)
# Local config hooks
localConfig="/etc/letsencrypt/renewal/$certName.conf"
createLocalConfigBackup() {
echo "creating local config backup"
sudo cp $localConfig $localConfig.backup
}
restoreLocalConfig() {
echo "restoring from local config"
sudo cp $localConfig.backup $localConfig
}
removeLocalConfigBackup() {
echo "removing local config backup"
[ -f $localConfig.backup ] && sudo rm $localConfig.backup
}
checkLocalConfig() {
echo "checking local config"
local foundHooks=0
for hook in "${configHooks[@]}"; do
if grep -q "$hook" "$localConfig"; then
foundHooks=$((foundHooks + 1))
fi
done
if [ $foundHooks -eq 0 ]; then
echo "no hooks in local config"
else
echo "hooks in local config:"
for hook in "${configHooks[@]}"; do
grep --color=never "$hook" "$localConfig" | sed -e 's/^/ /';
done
fi
}
setLocalConfigHooks() {
echo "setting local config hooks"
sudo certbot reconfigure --cert-name $certName --run-deploy-hooks --non-interactive --pre-hook "echo local pre" --post-hook "echo local post" --deploy-hook "echo local deploy"
}
setLocalConfigHooksEmptyStr() {
echo "setting local config hooks to empty strings"
sudo certbot reconfigure --cert-name $certName --run-deploy-hooks --non-interactive --pre-hook "" --post-hook "" --deploy-hook ""
}
setLocalConfigHooksDirHooks() {
echo "setting local config hooks"
sudo certbot reconfigure --cert-name $certName --run-deploy-hooks --non-interactive --pre-hook "$dirHookPre" --post-hook "$dirHookPost" --deploy-hook "$dirHookDeploy"
}
# Global config hooks
globalConfig="/etc/letsencrypt/cli.ini"
setGlobalConfigHooks() {
echo "creating global config"
sudo touch "$globalConfig"
sudo chmod 777 "$globalConfig"
sudo echo "pre-hook = \"echo global pre\"" | sudo tee -a "$globalConfig" 1> /dev/null
sudo echo "post-hook = \"echo global post\"" | sudo tee -a "$globalConfig" 1> /dev/null
sudo echo "deploy-hook = \"echo global deploy\"" | sudo tee -a "$globalConfig" 1> /dev/null
}
setGlobalConfigHooksDirHooks() {
echo "creating global config with hooks pointing to directory hooks"
sudo touch "$globalConfig"
sudo chmod 777 "$globalConfig"
sudo echo "pre-hook = \"$dirHookPre\"" | sudo tee -a "$globalConfig" 1> /dev/null
sudo echo "post-hook = \"$dirHookPost\"" | sudo tee -a "$globalConfig" 1> /dev/null
sudo echo "deploy-hook = \"$dirHookDeploy\"" | sudo tee -a "$globalConfig" 1> /dev/null
}
checkGlobalConfig() {
echo "checking global config"
if [ -f "$globalConfig" ]; then
local foundHooks=0
for hook in "${configHooks[@]}"; do
if grep -q "$hook" "$globalConfig"; then
foundHooks=$((foundHooks + 1))
fi
done
if [ $foundHooks -eq 0 ]; then
echo "no hooks in global config"
else
echo "hooks in global config:"
for hook in "${configHooks[@]}"; do
grep --color=never "$hook" "$globalConfig" | sed -e 's/^/ /';
done
fi
else
echo "global config doesn't exist"
fi
}
removeGlobalConfig() {
echo "removing global config"
[ -f "$globalConfig" ] && sudo rm "$globalConfig"
}
# Directory hooks
dirHookPre="/etc/letsencrypt/renewal-hooks/pre/pre.sh"
dirHookPost="/etc/letsencrypt/renewal-hooks/post/post.sh"
dirHookDeploy="/etc/letsencrypt/renewal-hooks/deploy/deploy.sh"
dirHookPreContent="echo dir pre;"
dirHookPostContent="echo dir post;"
dirHookDeployContent="echo dir deploy;"
setDirHooks() {
echo "setting dir hooks"
sudo touch "$dirHookPre"
sudo touch "$dirHookPost"
sudo touch "$dirHookDeploy"
sudo chmod 777 "$dirHookPre"
sudo chmod 777 "$dirHookPost"
sudo chmod 777 "$dirHookDeploy"
sudo echo "$dirHookPreContent" | sudo tee "$dirHookPre" 1> /dev/null
sudo echo "$dirHookPostContent" | sudo tee "$dirHookPost" 1> /dev/null
sudo echo "$dirHookDeployContent" | sudo tee "$dirHookDeploy" 1> /dev/null
}
checkDirHooks() {
echo "checking dir hooks"
if [ -f "$dirHookPre" -a -f "$dirHookPost" -a -f "$dirHookDeploy" ]; then
if grep -q "$dirHookPreContent" "$dirHookPre" && grep -q "$dirHookPostContent" "$dirHookPost" && grep -q "$dirHookDeployContent" "$dirHookDeploy"; then
echo "all dir hooks are in place"
else
grep -q "$dirHookPreContent" "$dirHookPre" || echo "dir pre-hook corrupt or empty"
grep -q "$dirHookPostContent" "$dirHookPost" || echo "dir post-hook corrupt or empty"
grep -q "$dirHookDeployContent" "$dirHookDeploy" || echo "dir deploy-hook corrupt or empty"
fi
elif [ ! -f "$dirHookPre" -a ! -f "$dirHookPost" -a ! -f "$dirHookDeploy" ]; then
echo "no dir hooks exist";
else
[ -f "$dirHookPre" ] || sudo echo "pre.sh is missing"
[ -f "$dirHookPost" ] || sudo echo "post.sh is missing"
[ -f "$dirHookDeploy" ] || sudo echo "deploy.sh is missing"
fi
}
removeDirHooks() {
echo "removing dir hooks"
[ -f "$dirHookPre" ] && sudo rm "$dirHookPre"
[ -f "$dirHookPost" ] && sudo rm "$dirHookPost"
[ -f "$dirHookDeploy" ] && sudo rm "$dirHookDeploy"
}
# -----------------
# TEST DEFINITIONS
# -----------------
testBaseline() {
echo "-- testing baseline renew command without any hooks --"
checkLocalConfig
checkGlobalConfig
checkDirHooks
echo -e "\n========== test core begin =========="
renewSansHooks
echo -e "========== test core end ==========\n"
checkLocalConfig
checkGlobalConfig
checkDirHooks
echo -e "-- baseline test complete --\n\n\n"
}
# CLI test
testCLI() {
echo "-- testing CLI hooks --"
echo -e "\n========== test core begin =========="
renewWithCLIHooks
echo -e "========== test core end ==========\n"
checkLocalConfig
echo -e "-- CLI test complete --\n\n\n"
}
# Local config tests
testLocal() {
echo "-- testing local config hooks --"
setLocalConfigHooks
checkLocalConfig
echo -e "\n========== test core begin =========="
renewSansHooks
echo -e "========== test core end ==========\n"
restoreLocalConfig
checkLocalConfig
echo -e "-- local config test complete --\n\n\n"
}
testCLIvsLocal() {
echo "-- testing CLI vs local config hooks --"
setLocalConfigHooks
checkLocalConfig
echo -e "\n========== test core begin =========="
renewWithCLIHooks
echo -e "========== test core end ==========\n"
checkLocalConfig
restoreLocalConfig
checkLocalConfig
echo -e "-- CLI vs local config test complete --\n\n\n"
}
# Global config tests
testGlobal() {
echo "-- testing global config hooks --"
setGlobalConfigHooks
checkGlobalConfig
echo -e "\n========== test core begin =========="
renewSansHooks
echo -e "========== test core end ==========\n"
removeGlobalConfig
checkGlobalConfig
echo -e "-- global config test complete --\n\n\n"
}
testClIvsGlobal() {
echo "-- testing CLI vs global config hooks --"
setGlobalConfigHooks
checkGlobalConfig
echo -e "\n========== test core begin =========="
renewWithCLIHooks
echo -e "========== test core end ==========\n"
checkLocalConfig
removeGlobalConfig
checkGlobalConfig
echo -e "-- CLI vs global config test complete --\n\n\n"
}
testLocalvsGlobal() {
echo "-- testing local config vs global config hooks --"
setGlobalConfigHooks
checkGlobalConfig
setLocalConfigHooks
checkLocalConfig
echo -e "\n========== test core begin =========="
renewSansHooks
echo -e "========== test core end ==========\n"
removeGlobalConfig
checkGlobalConfig
restoreLocalConfig
checkLocalConfig
echo -e "-- local config vs global config test complete --\n\n\n"
}
testCLIvsLocalvsGlobal() {
echo "-- testing CLI vs local config vs global config hooks --"
setGlobalConfigHooks
checkGlobalConfig
setLocalConfigHooks
checkLocalConfig
echo -e "\n========== test core begin =========="
renewWithCLIHooks
echo -e "========== test core end ==========\n"
checkLocalConfig
removeGlobalConfig
checkGlobalConfig
restoreLocalConfig
checkLocalConfig
echo -e "-- CLI vs local config vs global config test complete --\n\n\n"
}
# Directory hook tests
testDir() {
echo "-- testing dir hooks --"
setDirHooks
checkDirHooks
echo -e "\n========== test core begin =========="
renewSansHooks
echo -e "========== test core end ==========\n"
removeDirHooks
checkDirHooks
echo -e "-- dir test complete --\n\n\n"
}
testCLIvsDir() {
echo "-- testing CLI vs dir hooks --"
setDirHooks
checkDirHooks
echo -e "\n========== test core begin =========="
renewWithCLIHooks
echo -e "========== test core end ==========\n"
checkLocalConfig
removeDirHooks
checkDirHooks
echo -e "-- CLI vs dir test complete --\n\n\n"
}
testLocalVsDir() {
echo "-- testing local config vs dir hooks --"
setDirHooks
checkDirHooks
setLocalConfigHooks
checkLocalConfig
echo -e "\n========== test core begin =========="
renewSansHooks
echo -e "========== test core end ==========\n"
restoreLocalConfig
checkLocalConfig
removeDirHooks
checkDirHooks
echo -e "-- local config vs dir test complete --\n\n\n"
}
testGlobalVsDir() {
echo "-- testing global config vs dir hooks --"
setDirHooks
checkDirHooks
setGlobalConfigHooks
checkGlobalConfig
echo -e "\n========== test core begin =========="
renewSansHooks
echo -e "========== test core end ==========\n"
removeGlobalConfig
checkGlobalConfig
removeDirHooks
checkDirHooks
echo -e "-- global config vs dir test complete --\n\n\n"
}
testCLIvsLocalVsGlobalVsDir() {
echo "-- testing CLI vs local config vs global config vs dir hooks --"
setDirHooks
checkDirHooks
setGlobalConfigHooks
checkGlobalConfig
setLocalConfigHooks
checkLocalConfig
echo -e "\n========== test core begin =========="
renewWithCLIHooks
echo -e "========== test core end ==========\n"
checkLocalConfig
restoreLocalConfig
checkLocalConfig
removeGlobalConfig
checkGlobalConfig
removeDirHooks
checkDirHooks
echo -e "-- CLI vs local config vs global config vs dir test complete --\n\n\n"
}
testDirNoRepeatCLI() {
echo "-- testing directory hook "no repeat" policy against CLI hooks--"
setDirHooks
checkDirHooks
echo -e "\n========== test core begin =========="
renewWithCLIHooksDirHooks
echo -e "========== test core end ==========\n"
removeDirHooks
checkDirHooks
echo -e "-- dir "no repeat" against CLI test complete --\n\n\n"
}
testDirNoRepeatLocal() {
echo "-- testing directory hook "no repeat" policy against local config hooks--"
setDirHooks
checkDirHooks
setLocalConfigHooksDirHooks
checkLocalConfig
echo -e "\n========== test core begin =========="
renewSansHooks
echo -e "========== test core end ==========\n"
restoreLocalConfig
checkLocalConfig
removeDirHooks
checkDirHooks
echo -e "-- dir "no repeat" against local test complete --\n\n\n"
}
testDirNoRepeatGlobal() {
echo "-- testing directory hook "no repeat" policy against global config hooks--"
setDirHooks
checkDirHooks
setGlobalConfigHooksDirHooks
checkGlobalConfig
echo -e "\n========== test core begin =========="
renewSansHooks
echo -e "========== test core end ==========\n"
removeGlobalConfig
checkGlobalConfig
removeDirHooks
checkDirHooks
echo -e "-- dir "no repeat" against global test complete --\n\n\n"
}
# -----------
# TESTING
# -----------
basicSafetyCheck
prepareTestEnv
testBaseline
testCLI
testLocal
testCLIvsLocal
testGlobal
testClIvsGlobal
testLocalvsGlobal
testCLIvsLocalvsGlobal
testDir
testCLIvsDir
testLocalVsDir
testGlobalVsDir
testCLIvsLocalVsGlobalVsDir
testDirNoRepeatCLI
testDirNoRepeatLocal
testDirNoRepeatGlobal
resetTestEnv
@MagnusOxlund
Copy link
Author

MagnusOxlund commented Dec 5, 2024

Here is the test result for Certbot 3.0.1:

As documented in the Certbot manual, directory hooks always run and always run first.

The override priority among the remaining three hook methods is:

command line argument hooks (renew --pre-hook <...>) > global config hooks (cli.ini) > certificate configuration hooks (<cert name>.conf)

Surprisingly, hooks defined in the global Certbot config file (cli.ini) override local certificate renewal config hooks. This means that specificity (defined as CLI > local > global) does not determine the override priority.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment