#!/usr/bin/env bash # This file contains the default hook functions for dehydrated - these functions will be used when there is no overriding certificate specific hooks file. # All but startup_hook and ext_hook can be overridden by a hooks script on a per certificate basis. # # shellcheck disable=SC2034,SC2317 # Configuration. # Where the copies of the current certificates/keys should be placed. Comment for no copying. CERTSDIR="/etc/certificates" # The syslog facility and tag to use. FACILITY="local3" TAG="dehydrated" # Where from/to to send emails. EMAIL_FROM="\"Server: ${HOSTNAME%%.*}\" " EMAIL_TO=("Systems' Administrator ") # Get the system ID. # shellcheck disable=SC2046 declare SYSTEM_$(grep '^ID=' /etc/os-release 2>/dev/null) # Write a message to syslog, and send a copy via email. notify() { local LOG_PREFIX="${LOG_PREFIX:-Certificate renewal} $1" PRIORITY [[ -z "$1" ]] && return 1 # Select the syslog priority level. case "$1" in 'error') PRIORITY="err" ;; 'warning') PRIORITY="warn" ;; *) PRIORITY="info" ;; esac shift # Log the message to syslog if [[ "$ID" == "alpine" ]]; then # BusyBox logger on Alpine's is missing the --id option. printf "%s\\n" "$LOG_PREFIX:" "$@" "EOX" | logger -p "$FACILITY.$PRIORITY" -t "$TAG" >/dev/null 2>&1 else printf "%s\\n" "$LOG_PREFIX:" "$@" "EOX" | logger --id="$$" -p "$FACILITY.$PRIORITY" -t "$TAG" >/dev/null 2>&1 fi # Email the notification. printf "%s\\n" "$@" | mail -r "$EMAIL_FROM" -s "$LOG_PREFIX" "${EMAIL_TO[@]}" >/dev/null 2>&1 return 0 } # Service configurations (used at startup/shutdown). services() { local DAEMON ERR=0 LOG_PREFIX="Dehydrated configuration" PIDFILE RCFILE SANITY="$1" # Select the service configuration based on the distribution. # RCFILE_ is required for any service. # Either DAEMON_ or PIDFILE_, or both is required for any service. if [[ "$SYSTEM_ID" == "slackware" ]]; then # HTTP daemon selection. if [[ -x "/etc/rc.d/rc.httpd" ]]; then RCFILE_HTTPD="/etc/rc.d/rc.httpd" DAEMON_HTTPD="httpd" PIDFILE_HTTPD="/run/httpd.pid" elif [[ -x "/etc/rc.d/rc.thttpd" ]]; then RCFILE_HTTPD="/etc/rc.d/rc.thttpd" DAEMON_HTTPD="thttpd" PIDFILE_HTTPD="/run/thttpd.pid" fi # FTP daemon selection. if [[ -x "/etc/rc.d/rc.proftpd" ]]; then RCFILE_FTPD="/etc/rc.d/rc.proftpd" DAEMON_FTPD="proftpd" PIDFILE_FTPD="/run/proftpd.pid" fi # SMTP daemon selection. if [[ -x "/etc/rc.d/rc.exim" ]]; then RCFILE_SMTPD="/etc/rc.d/rc.exim" DAEMON_SMTPD="exim" PIDFILE_SMTPD="/run/exim.pid" fi elif [[ "$SYSTEM_ID" == "void" ]]; then # HTTP daemon selection. # thttpd on Void doesn't have a directly callable rc script, so can't be supported. if [[ -x "/usr/sbin/apachectl" ]]; then RCFILE_HTTPD="/usr/sbin/apachectl" DAEMON_HTTPD="httpd" PIDFILE_HTTPD="/run/httpd/httpd.pid" fi elif [[ "$SYSTEM_ID" == "alpine" ]]; then # HTTP daemon selection. if [[ -x "/etc/init.d/apache2" ]]; then RCFILE_HTTPD="/etc/init.d/apache2" DAEMON_HTTPD="httpd" PIDFILE_HTTPD="/run/apache2/httpd.pid" elif [[ -x "/etc/init.d/thttpd" ]]; then RCFILE_HTTPD="/etc/init.d/thttpd" DAEMON_HTTPD="thttpd" PIDFILE_HTTPD="/run/thttpd.pid" fi # Samba daemon selection. if [[ -x "/etc/init.d/samba" ]]; then # FIXME: # RCFILE_SAMBA="/etc/init.d/samba" DAEMON_SAMBA="samba" PIDFILE_SAMBA="/run/samba.pid" fi fi # Sanity check settings. ((SANITY == 1)) && { [[ -z "$RCFILE_HTTPD" ]] && notify "warning" "No configuration settings for an HTTP daemon - no start/restart of HTTP daemon is possible -- check configuration" for RCFILE in "${!RCFILE_@}"; do DAEMON="DAEMON_${RCFILE#RCFILE_}" PIDFILE="PIDFILE_${RCFILE#RCFILE_}" [[ -n "${!RCFILE}" ]] && [[ -z "${!DAEMON}" ]] && [[ -z "${!PIDFILE}" ]] && notify "error" "'$RCFILE' is set, but neither '$DAEMON' nor '$PIDFILE' is set - at least one setting is required -- aborting" && ERR=1 done } ((ERR == 1)) && return 1 return 0 } deploy_challenge() { local DOMAIN="$1" TOKEN_FILENAME="$2" TOKEN_VALUE="$3" # This hook is called once for every domain that needs to be # validated, including any alternative names you may have listed. # Parameters: # DOMAIN - The domain name (CN or subject alternative name) being validated. # TOKEN_FILENAME - The name of the file containing the token to be served for HTTP validation # Should be served by your web server as /.well-known/acme-challenge/${TOKEN_FILENAME}. # TOKEN_VALUE - The token value that needs to be served for validation. # For DNS validation, this is what you want to put in the _acme-challenge TXT record. # For HTTP validation it is the value that is expected be found in the $TOKEN_FILENAME file. # Simple example: Use nsupdate with local named # printf 'server 127.0.0.1\nupdate add _acme-challenge.%s 300 IN TXT "%s"\nsend\n' "$DOMAIN" "$TOKEN_VALUE" | nsupdate -k /var/run/named/session.key return 0 } clean_challenge() { local DOMAIN="$1" TOKEN_FILENAME="$2" TOKEN_VALUE="$3" # This hook is called after attempting to validate each domain, whether or not validation was successful. Here you can delete files or DNS records that are no longer needed. # The parameters are the same as for deploy_challenge. # Simple example: Use nsupdate with local named # printf 'server 127.0.0.1\nupdate delete _acme-challenge.%s TXT "%s"\nsend\n' "$DOMAIN" "$TOKEN_VALUE" | nsupdate -k /var/run/named/session.key return 0 } sync_cert() { local KEYFILE="$1" CERTFILE="$2" FULLCHAINFILE="$3" CHAINFILE="$4" REQUESTFILE="$5" # This hook is called after the certificates have been created but before they are symlinked. # This allows you to sync the files to disk to prevent creating a symlink to empty files on unexpected system crashes. # This hook is not intended to be used for further processing of certificate files; see deploy_cert for that. # Parameters: # KEYFILE - The path of the file containing the private key. # CERTFILE - The path of the file containing the signed certificate. # FULLCHAINFILE - The path of the file containing the full certificate chain. # CHAINFILE - The path of the file containing the intermediate certificate(s). # REQUESTFILE - The path of the file containing the certificate signing request. # Simple example: sync the files before symlinking them # sync "$KEYFILE" "$CERTFILE" "$FULLCHAINFILE" "$CHAINFILE" "$REQUESTFILE" return 0 } deploy_cert() { local DOMAIN="$1" KEYFILE="$2" CERTFILE="$3" FULLCHAINFILE="$4" CHAINFILE="$5" TIMESTAMP="$6" # This hook is called once for each certificate that has been produced. # Here you might, for instance, copy your new certificates to service-specific locations and reload the service. # Parameters: # DOMAIN - The primary domain name, i.e. the certificate common name (CN). # KEYFILE - The path of the file containing the private key. # CERTFILE - The path of the file containing the signed certificate. # FULLCHAINFILE - The path of the file containing the full certificate chain. # CHAINFILE - The path of the file containing the intermediate certificate(s). # TIMESTAMP - Timestamp when the specified certificate was created. local FILE LOG_PREFIX="Certificate deployment" # Only copy the certificate if there's a CERTSDIR setting. [[ -n "$CERTSDIR" ]] && { # If any of the destination files are symlinks, bail out - we don't want to clobber something. for FILE in "$CERTSDIR/${DOMAIN}_"{cert,key,chain,fullchain}.pem; do [[ -e "$FILE" ]] && [[ -L "$FILE" ]] && { notify "error" "Will not copy to symlink '$FILE' during '$DOMAIN' certificate deployment" # Return 0 so that dehydrated doesn't stop - there may be some more certificates to renew. return 0 } done # The first time through this will create the files readable by root only, but better to err on the side of caution. # Subsequent runs will retain whatever permissions were set by the admin after the first run. cmp "$CERTFILE" "$CERTSDIR/${DOMAIN}_cert.pem" >/dev/null 2>&1 || { umask 066 # shellcheck disable=SC2015 cat "$CERTFILE" >"$CERTSDIR/${DOMAIN}_cert.pem" && cat "$KEYFILE" >"$CERTSDIR/${DOMAIN}_key.pem" && cat "$CHAINFILE" >"$CERTSDIR/${DOMAIN}_chain.pem" && cat "$FULLCHAINFILE" >"$CERTSDIR/${DOMAIN}_fullchain.pem" || { notify "error" "Failed to copy certificates/key to '$CERTSDIR' during '$DOMAIN' certificate deployment" # Return 0 so that dehydrated doesn't stop - there may be some more certificates to renew. return 0 } } # Set a marker (used in the exit_hook function) to signal that services should be reloaded at the end of deployments. touch /run/dehydrated-reload-marker || { notify "warning" "Failed to create reload marker during '$DOMAIN' certificate deployment - reloading services manually may be required -- check server" # Return 0 so that dehydrated doesn't stop - there may be some more certificates to renew. return 0 } } # Notify the sysadmin of the sucessful renewal. notify "information" "Sucessful renewal and deployment of certificate/key for '$DOMAIN'" return 0 } deploy_ocsp() { local DOMAIN="$1" OCSPFILE="$2" TIMESTAMP="$3" # This hook is called once for each updated ocsp stapling file that has been produced. # Here you might, for instance, copy your new ocsp stapling files to service-specific locations and reload the service. # Parameters: # DOMAIN - The primary domain name, i.e. the certificate common name (CN). # OCSPFILE - The path of the ocsp stapling file. # TIMESTAMP - Timestamp when the specified ocsp stapling file was created. # Simple example: Copy file to nginx config # cp "$OCSPFILE" /etc/nginx/ssl/; chown -R nginx: /etc/nginx/ssl # systemctl reload nginx return 0 } unchanged_cert() { local DOMAIN="$1" KEYFILE="$2" CERTFILE="$3" FULLCHAINFILE="$4" CHAINFILE="$5" # This hook is called once for each certificate that is still valid and therefore wasn't reissued. # Parameters: # DOMAIN - The primary domain name, i.e. the certificate common name (CN). # KEYFILE - The path of the file containing the private key. # CERTFILE - The path of the file containing the signed certificate. # FULLCHAINFILE - The path of the file containing the full certificate chain. # CHAINFILE - The path of the file containing the intermediate certificate(s). return 0 } invalid_challenge() { local DOMAIN="$1" RESPONSE="$2" # This hook is called if the challenge response has failed, so domain owners can be aware and act accordingly. # Parameters: # DOMAIN - The primary domain name, i.e. the certificate common name (CN). # RESPONSE - The response that the verification server returned # Notify the sysadmin. notify "error" "Validation of '$DOMAIN' failed:" "$RESPONSE" return 0 } request_failure() { local STATUSCODE="$1" REASON="$2" REQTYPE="$3" HEADERS="$4" # This hook is called when an HTTP request fails (e.g., when the ACME server is busy, returns an error, etc). # It will be called upon any response code that does not start with '2'. Useful to alert admins about problems with requests. # Parameters: # STATUSCODE - The HTML status code that originated the error. # REASON - The specified reason for the error. # REQTYPE - The kind of request that was made (GET, POST...) # Notify the sysadmin. notify "error" "HTTP $REQTYPE request failed for '$DOMAIN' with code '$STATUSCODE'" "Reason: $REASON" "Headers:" "$HEADERS" return 0 } generate_csr() { local DOMAIN="$1" CERTDIR="$2" ALTNAMES="$3" # This hook is called before any certificate signing operation takes place. # It can be used to generate or fetch a certificate signing request with external tools. # The output should be just the cerificate signing request formatted as PEM. # Parameters: # DOMAIN - The primary domain as specified in domains.txt. # This does not need to match with the domains in the CSR, it's basically just the directory name. # CERTDIR - Certificate output directory for this particular certificate. # Can be used for storing additional files. # ALTNAMES - All domain names for the current certificate as specified in domains.txt. # Again, this doesn't need to match with the CSR, it's just there for convenience. # Simple example: Look for pre-generated CSRs # if [ -e "$CERTDIR/pre-generated.csr" ]; then # cat "$CERTDIR/pre-generated.csr" # fi return 0 } startup_hook() { # This hook is called before the cron command to do some initial tasks (e.g. starting a webserver). local LOG_PREFIX="Dehydrated startup" # Read services configuration (with sanity check) services 1 || return 1 # Make sure the certificates directory exists. [[ -n "$CERTSDIR" ]] && { umask 022 # shellcheck disable=SC2174 mkdir -p -m 0755 "$CERTSDIR" 2>/dev/null || { notify "error" "Failed to create certificate storage directory -- aborting" return 1 } } # If an HTTP daemon rc script is available and the service is not already running, start it. [[ -n "$RCFILE_HTTPD" ]] && { pgrep -c ${PIDFILE_HTTPD:+-F "$PIDFILE_HTTPD"} "$DAEMON_HTTPD" >/dev/null 2>&1 || { "$RCFILE_HTTPD" start >/dev/null 2>&1 sleep 5 if pgrep -c ${PIDFILE_HTTPD:+-F "$PIDFILE_HTTPD"} "$DAEMON_HTTPD" >/dev/null 2>&1; then # Set a marker (used in exit_hook()) to signal that the HTTP daemon should be stopped at the end of deployments. touch /run/dehydrated-http-daemon-stop-marker 2>/dev/null || notify "warning" "Failed to create HTTP daemon stop marker - HTTP daemon will be left running -- check server" else notify "error" "Failure of '$RCFILE_HTTPD' to start HTTP daemon -- aborting" return 1 fi } } # Add firewall rules to allow HTTP traffic so the nonce can be validated. { iptables -N dehydrated && ip6tables -N dehydrated && iptables -I INPUT 1 -j dehydrated && ip6tables -I INPUT 1 -j dehydrated && iptables -I dehydrated 1 -p tcp --syn --dport 80 -m conntrack --ctstate NEW -j ACCEPT && ip6tables -I dehydrated 1 -p tcp --syn --dport 80 -m conntrack --ctstate NEW -j ACCEPT; } >/dev/null 2>&1 || { notify "error" "Failed to insert firewall rules to allow nonce validation -- aborting" return 1 } return 0 } exit_hook() { local ERROR="$1" # This hook is called at the end of the cron command and can be used to do some final (cleanup or other) tasks. # Parameters: # ERROR - Contains error message if dehydrated exits with error. local DAEMON ERR=0 LOG_PREFIX="Dehydrated shutdown" PIDFILE RCFILE TIMEOUT=30 # Read services configuration (without sanity check - this was already done at startup) services 0 || return 1 # Delete firewall rules that was added to allow HTTP traffic. iptables -C INPUT -j dehydrated >/dev/null 2>&1 && iptables -D INPUT -j dehydrated >/dev/null 2>&1 ip6tables -C INPUT -j dehydrated >/dev/null 2>&1 && ip6tables -D INPUT -j dehydrated >/dev/null 2>&1 iptables -F dehydrated >/dev/null 2>&1 ip6tables -F dehydrated >/dev/null 2>&1 iptables -X dehydrated >/dev/null 2>&1 ip6tables -X dehydrated >/dev/null 2>&1 # If the reload marker was set, restart services. [[ -e /run/dehydrated-reload-marker ]] && { for RCFILE in "${!RCFILE_@}"; do DAEMON="DAEMON_${RCFILE#RCFILE_}" PIDFILE="PIDFILE_${RCFILE#RCFILE_}" # If the HTTP daemon is going to be shut down, there's no need to restart it. [[ "$RCFILE" == "RCFILE_HTTPD" ]] && [[ -e /run/dehydrated-http-daemon-stop-marker ]] && continue # Restart the service. "${!RCFILE}" restart >/dev/null 2>&1 || notify "warning" "Failed to restart service '${!DAEMON}' -- check server" sleep "$TIMEOUT" pgrep -c ${PIDFILE:+-F "${!PIDFILE}"} "${!DAEMON}" >/dev/null 2>&1 || { notify "warning" "Service '${!DAEMON}' exited unexpectedly - trying to start again" "${!RCFILE}" start >/dev/null 2>&1 || notify "warning" "Failed to start service '${!DAEMON}' -- check server" sleep "$TIMEOUT" pgrep -c ${PIDFILE:+-F "${!PIDFILE}"} "${!DAEMON}" >/dev/null 2>&1 || { notify "warning" "Service '${!DAEMON}' failed to restart correctly -- check server" ERR=1 } } done } # Remove the reload marker if all services restarted without issue. Keep the marker if any failed. ((ERR == 0)) && { rm -f /run/dehydrated-reload-marker 2>/dev/null || notify "warning" "Failed to remove services reload marker -- check server"; } # If an HTTP daemon was started by dehydrated, stop it now. ERR=0 [[ -e /run/dehydrated-http-daemon-stop-marker ]] && { pgrep -c ${PIDFILE_HTTPD:+-F "$PIDFILE_HTTPD"} "$DAEMON_HTTPD" >/dev/null 2>&1 && { "$RCFILE_HTTPD" stop >/dev/null 2>&1 || notify "warning" "Failed to gracefully stop service '$DAEMON_HTTPD' -- check server" sleep "$TIMEOUT" pgrep -c ${PIDFILE_HTTPD:+-F "$PIDFILE_HTTPD"} "$DAEMON_HTTPD" >/dev/null 2>&1 && { pkill -TERM ${PIDFILE_HTTPD:+-F "$PIDFILE_HTTPD"} "$DAEMON_HTTPD" >/dev/null 2>&1 || notify "warning" "Failed to -SIGTERM service '$DAEMON_HTTPD' -- check server" sleep "$TIMEOUT" pgrep -c ${PIDFILE_HTTPD:+-F "$PIDFILE_HTTPD"} "$DAEMON_HTTPD" >/dev/null 2>&1 && { pkill -KILL ${PIDFILE_HTTPD:+-F "$PIDFILE_HTTPD"} "$DAEMON_HTTPD" >/dev/null 2>&1 || notify "warning" "Failed to -SIGKILL service '$DAEMON_HTTPD' -- check server" sleep 5 } } pgrep -c ${PIDFILE_HTTPD:+-F "$PIDFILE_HTTPD"} "$DAEMON_HTTPD" >/dev/null 2>&1 && notify "warning" "Failed to stop HTTP daemon that dehydrated started" && ERR=1 } } # If the HTTP daemon was stopped correctly, remove the stop marker. ((ERR == 0)) && { rm -f /run/dehydrated-http-daemon-stop-marker 2>/dev/null || notify "warning" "Failed to remove HTTP daemon stop marker -- check server"; } return 0 } # Run the correct function. HANDLER="$1" shift if declare -pF "$HANDLER" >/dev/null 2>&1; then "$HANDLER" "$@" exit "$?" else exit 0 fi