dehydrated on FreeBSD

This describes how to setup dehydrated on a FreeBSD system to have your Let's Encrypt certificates updated automatically. It works for single hostname and multidomain certificates only. For verification, the http-01 protocol is used. Apache serves both the live websites, as well as the verification challenges. With slight modifications, one can adopt this for other webservers as well.

Installation

Either install the package:

# pkg install dehydrated
Or install the port, which I prefer since it allows you to deselect Build and/or install documentation, which I barely need on a server:
# cd /usr/ports/security/dehydrated
# make install clean

Configuration

  1. Place your domain names for which you want to receive an SSL certificate in /usr/local/etc/dehydrated/domains.txt, e.g.:
    www.your-domain.invalid
    www.your-other-domain.invalid webmail-your-other-domain.invalid
    
    The first line denotes a single hostname certificate, the second entry a multidomain certificate.
  2. Edit /usr/local/etc/dehydrated/config, uncomment and set HOOK to $BASEDIR/hook.sh, as well as AUTO_CLEANUP to yes:
    HOOK="$BASEDIR/hook.sh"
    AUTO_CLEANUP="yes"
    
  3. Edit /usr/local/etc/dehydrated/hook.sh, and add and extend the following functions:
    #!/usr/local/bin/bash
    
    RESTART_FLAG="$BASEDIR/restart_apache"
    
    sendemail() {
        subject="$1"
    
        (cat <<EOF; cat) | sendmail -t
    From: root
    To: root
    Subject: $subject
    
    EOF
    }
    
    lookup() {
        local domain="$1"
        local domains_txt="$BASEDIR/domains.txt"
        local error
        local ssldir
        local tokendir
        local tokenfile
    
        local mainname=`awk '{if (/(^| )'"$domain"'($| )/) { print $1; }}' "$domains_txt"`
        if [ -z "$mainname" ]; then
            error="Domain $domain not found in $domains_txt"
            (cat <<EOF; cat "$domains_txt") | sendemail "$error"
    Content of $domains_txt:
    EOF
            echo "$error" >&2
            exit 1
        fi
    
        tokendir="/www/$mainname/htdocs/.well-known/acme-challenge"
    
        if [ -z "$3" ]; then
            # clean challenge
            rm "$tokendir/$2"
        elif [ -z "$4" ]; then
            # deploy challenge
            tokenfile="$tokendir/$2"
            mkdir -p "$tokendir"
            echo "$3" > "$tokenfile"
            chmod 0644 "$tokenfile"
        else
            # deploy cert
            ssldir="/www/$mainname/ssl"
            cp "$2" "$ssldir/$mainname.key"
            cp "$3" "$ssldir/$mainname.crt"
            cp "$4" "$ssldir/$mainname.chain"
            touch "$RESTART_FLAG"
        fi
    }
    
    deploy_challenge() {
        local DOMAIN="${1}" TOKEN_FILENAME="${2}" TOKEN_VALUE="${3}"
    
        lookup "$DOMAIN" "$TOKEN_FILENAME" "$TOKEN_VALUE"
    }
    
    clean_challenge() {
        local DOMAIN="${1}" TOKEN_FILENAME="${2}" TOKEN_VALUE="${3}"
    
        lookup "$DOMAIN" "$TOKEN_FILENAME"
    }
    
    deploy_cert() {
        local DOMAIN="${1}" KEYFILE="${2}" CERTFILE="${3}" FULLCHAINFILE="${4}" CHAINFILE="${5}" TIMESTAMP="${6}"
    
        lookup "$DOMAIN" "$KEYFILE" "$CERTFILE" "$CHAINFILE"
    }
    
    invalid_challenge() {
        local DOMAIN="${1}" RESPONSE="${2}"
    
        cat <<EOF | sendemail "Validation of $DOMAIN failed"
    Response:
    $RESPONSE
    EOF
    }
    
    request_failure() {
        local STATUSCODE="${1}" REASON="${2}" REQTYPE="${3}" HEADERS="${4}"
    
        cat <<EOF | sendemail "Request of $DOMAIN failed"
    Status code:
    $STATUSCODE
    
    Reason:
    $REASON
    
    Request type:
    $REQTYPE
    
    Headers:
    $HEADERS
    EOF
    }
    
    exit_hook() {
      if [ -f "$RESTART_FLAG" ]; then
        service apache24 graceful
        rm "$RESTART_FLAG"
      fi
    }
    
    This assumes that For example, if your domains.txt contains
    www.your-other-domain.invalid webmail-your-other-domain.invalid
    
    then challenges will be placed as text files in the directory /www/www.your-other-domain.invalid/htdocs/.well-known/acme-challenge. Afterwards, you will get three files
    1. /www/www.your-other-domain.invalid/ssl/www.your-other-domain.invalid.key
    2. /www/www.your-other-domain.invalid/ssl/www.your-other-domain.invalid.crt
    3. /www/www.your-other-domain.invalid/ssl/www.your-other-domain.invalid.chain
    with www.your-other-domain.invalid.crt being a multidomain certificate, and www.your-other-domain.invalid.chain containing the CA and intermediate certificates.
  4. Make /usr/local/etc/dehydrated/hook.sh executable:
    # chmod 0755 /usr/local/etc/dehydrated/hook.sh
    
  5. Add this to /etc/periodic.conf:
    weekly_dehydrated_enable="YES"
    
  6. Once register your account:
    # /usr/local/bin/dehydrated --register --accept-terms
    
  7. You can now wait until the next weekly cycle of periodic, or request the certificates manually:
    # /usr/local/etc/periodic/weekly/000.dehydrated