No more certificate fingerprints – only sasl auth instead

Today I gave up on my attempt to allow relay via SSL certificate fingerprints. Instead, I will use sasl auth.

Yesterday I wrote about my SMTP deliver test which broke when an SSL certificate was updated. Later that day, I finished writing scripts which delivered that fingerprint file to all hosts which needed it. Today, I abandoned that approach in favor of sasl. From the time I decided to use sasl to my first successful test was about an hour. I was already using sasl so I just needed to duplicate an existing solution.

But here are the scripts which I was using.

Generate the fingerprints file

This script runs on the same host as security/acme.sh and cert-shifter (part of sysutils/anvil). The file it creates is later rsync‘d to the certificate distribution webserver.

#!/bin/sh

CERT_SRC="/var/db/acme/certs"

CERT_DST_ROOT="/var/db/certs-for-rsync"
FINGERPRINT_DIR="${CERT_DST_ROOT}/cert-fingerprints"
FINGERPRINT_FILE="fingerprints"
FINGERPRINT_ALGORITHM=sha1

TMP="${CERT_DST_ROOT}/tmp"

CERTS=""

# items below here are not usually altered

CONFIG="/usr/local/etc/anvil/cert-fingerprints.conf"

if [ -f ${CONFIG} ]; then
  . ${CONFIG}
fi

BASENAME="/usr/bin/basename"
CHMOD="/bin/chmod"
CP="/bin/cp"
DIFF="/usr/bin/diff"
LOGGER="/usr/bin/logger -t fingerprint-shifter"
OPENSSL="/usr/bin/openssl"
POSTALIAS="/usr/local/sbin/postalias"
SED="/usr/bin/sed"


${LOGGER} starting $0

temp_file=$(mktemp)

for cert in ${CERTS}
do
  # XXX FINGERPRINT_ALGORITHM needs to be validated and the correct 
  fingerprint=`${OPENSSL} x509 -noout -fingerprint -${FINGERPRINT_ALGORITHM} -inform pem -in ${CERT_SRC}/${cert}/${cert}.cer | ${SED} 's/^.* Fingerprint=//g'`
  echo ${fingerprint} ${cert} >> ${temp_file}
done

DST_FILE="${FINGERPRINT_DIR}/${FINGERPRINT_FILE}"
if [ -f ${DST_FILE} ]
then
  if [ -r ${DST_FILE} ]
  then
    diff=`${DIFF} ${temp_file} ${DST_FILE}`
    if [ "${diff}X" != "X" ]
    then
      ${LOGGER} installing ${temp_file} to ${DST_FILE}
      ${CP} ${temp_file} ${DST_FILE}
      ${CHMOD} +r ${DST_FILE}
      ${POSTALIAS} ${DST_FILE}
    else
      ${LOGGER} fingerprints unchanged. No update will occur.
    fi
  else
    ${LOGGER} FINGERPRINT_FILE is not readable: ${DST_FILE}
  fi
else
  ${LOGGER} FINGERPRINT_FILE does not exist, creating it: ${DST_FILE}
  ${CP} ${temp_file} ${DST_FILE}
  ${CHMOD} +r ${DST_FILE}
  ${POSTALIAS} ${DST_FILE}
fi

${LOGGER} stopping $0

This is the corresponding configuration file:

$ cat /usr/local/etc/anvil/cert-fingerprints.conf
FINGERPRINT_DIR="/var/db/certs-for-rsync/cert-fingerprints"
FINGERPRINT_FILE="relay_clientcerts"
FINGERPRINT_ALGORITHM=sha1
CERTS="cliff.int.example.org cliff.example.org"

Pulling the fingerprints

Once the fingerprint file is on the webserver, each client can get access to it.

This script runs on each host which needs the fingerprints file. This is very similar to the cert-puller script which comes with sysutils/anvil.

$ cat /usr/local/bin/finger-puller
#!/bin/sh

# the following variables can be overridden by the config file
FINGERPRINT_DEST="/usr/local/etc/postfix-config"
CERT_SERVER="https://certs.example.org/cert-fingerprints"

# a space separated list of fingerprint files which will be downloaded
MY_FINGERPRINTS="relay_clientcerts"

# a space separated list of services to restart
SERVICES="postfix"

DOWNLOAD_DIR="/var/db/anvil"

# be sure to specify the agument & have no spaces in between the single quotes
USER_AGENT="anvil-finger-puller"

#
# --mirror avoids replacement when identical
# --quiet avoids noise
#
FETCH="/usr/bin/fetch --mirror --quiet --user-agent='${USER_AGENT}'"

CURL="/usr/local/bin/curl --silent --user-agent '${USER_AGENT}' --remote-time"

WGET="/usr/local/bin/wget --quiet --user-agent='${USER_AGENT}'"

# items above can be overridden via the configuration file
# items below here are not usually altered

CONFIG="/usr/local/etc/anvil/finger-puller.conf"

if [ -f ${CONFIG} ]; then
  . ${CONFIG}
fi

# initialize variables used below

SUDO_EXAMPLES=0
NEW_FINGERPRINTS_FOUND=0

# various commands used by this script
BASENAME="/usr/bin/basename"
CP="/bin/cp"
DIFF="/usr/bin/diff"

# These are the downlaoded fingerprints which we will consider for installation
FIND_FINGERPRINT_FILES="/usr/bin/find ${DOWNLOAD_DIR} -type f"

# if you want to disable logging, not recommend, put a hash before /usr
LOGGER="/usr/bin/logger -t finger-puller"
MV="/bin/mv"
SERVICE="/usr/sbin/service"
SUDO="/usr/local/bin/sudo"
TOUCH="/usr/bin/touch"

usage(){
  echo "Usage: $0 [-s] [-h]"
  exit 1
}

sudo_examples(){
  # this function prints out commands which you can use with visudo to copy/paste

  #
  # NOTE: the code here must closely match that within install_new_fingerprints() & restart_services()
  #
  for fingerprint in ${MY_FINGERPRINTS}
  do
    echo "anvil   ALL=(ALL) NOPASSWD:${CP} -a ${DOWNLOAD_DIR}/${fingerprint} ${FINGERPRINT_DEST}/${fingerprint}.tmp"
    echo "anvil   ALL=(ALL) NOPASSWD:${MV} ${FINGERPRINT_DEST}/${fingerprint}.tmp ${FINGERPRINT_DEST}/${fingerprint}"
  done

  for service in ${SERVICES}
  do
    case ${service} in
      "apache24")
        echo "anvil   ALL=(ALL) NOPASSWD:${SERVICE} ${service} graceful"
        ;;

      "dovecot"|"postfix"|"nginx")
        echo "anvil   ALL=(ALL) NOPASSWD:${SERVICE} ${service} restart"
        ;;
    esac
  done
}

sanity_checks(){
  if [ ! -r "${FINGERPRINT_DEST}" -o ! -d "${FINGERPRINT_DEST}" ]; then
    ${LOGGER} "${FINGERPRINT_DEST}" is NOT readable and a directory
    ${LOGGER} $0 exits
    exit 2
  fi
}

fetch_new_fingerprints(){
  # first, we fetch the fingerprints are looking for.
  ${LOGGER} fetching into ${DOWNLOAD_DIR}
  for fingerprint in ${MY_FINGERPRINTS}
  do
    ${LOGGER} checking for ${fingerprint}
    case ${FETCH_TOOL} in
      "wget")
        ${LOGGER} running: ${WGET} --output-document=${DOWNLOAD_DIR}/${fingerprint} ${CERT_SERVER}/${fingerprint}
        ${WGET} --output-document=${DOWNLOAD_DIR}/${fingerprint} ${CERT_SERVER}/${fingerprint}
        ;;
      "curl")
        ${LOGGER} running: ${CURL} -o ${DOWNLOAD_DIR}/${fingerprint} ${CERT_SERVER}/${fingerprint}
        ${CURL} -o ${DOWNLOAD_DIR}/${fingerprint} ${CERT_SERVER}/${fingerprint}
        ;;
      *)
        ${LOGGER} running: ${FETCH} -o ${DOWNLOAD_DIR} ${CERT_SERVER}/${fingerprint}
        ${FETCH} -o ${DOWNLOAD_DIR} ${CERT_SERVER}/${fingerprint}
        ;;
    esac
    RESULT=$?
    if [ "${RESULT}" != "0" ]; then
      ${LOGGER} "error '${RESULT}' on fetch - perhaps the remote file does not exist."
    fi
  done
}

install_new_fingerprints(){
  ${LOGGER} looking for fingerprint files: ${FIND_FINGERPRINT_FILES}
  # for each new thing we find, move it over to the right place
  NEW_FILES=`${FIND_FINGERPRINT_FILES}`
  for new_file in ${NEW_FILES}
  do
    filename=`${BASENAME} ${new_file}`
    ${LOGGER} validating ${filename}

    # that fingerprint may not be installed
    if [ -f ${FINGERPRINT_DEST}/${filename} ]; then
      diff=`${DIFF} ${new_file} ${FINGERPRINT_DEST}/${filename}`
    else
      diff="x";
      ${LOGGER} ${filename} does not exist and will be installed
    fi

    # only install if the fingerprints are different.
    if [ "${diff}X" != "X" ]; then
      ${LOGGER} installing ${filename}
      #
      # NOTE: the code here must closely match that within sudo_examples() & restart_services()
      #
      ${SUDO} ${CP} -a ${new_file} ${FINGERPRINT_DEST}/${filename}.tmp
      ${SUDO} ${MV} ${FINGERPRINT_DEST}/${filename}.tmp ${FINGERPRINT_DEST}/${filename}
      NEW_FINGERPRINTS_FOUND=1
    fi
  done
}


restart_services(){
  #
  # NOTE: the code here must closely match that within sudo_examples() & install_new_fingerprints()
  #
  for service in ${SERVICES}
  do
    case ${service} in
      "apache24")
        ${LOGGER} doing a graceful on ${service}
        ${SUDO} ${SERVICE} ${service} graceful
        ;;

      # it might be better if we do a reload.
      # will that be sufficient?
      "dovecot"|"postfix"|"nginx")
        ${LOGGER} restarting ${service}
        ${SUDO} ${SERVICE} ${service} restart
        ;;
      
      *)
        ${LOGGER} "Unknown service requested in $0: ${service}"
        ;;
    esac
  done
}

#
# main code starts here
#

while getopts "hs" opt; do
  case $opt in
    s)
      SUDO_EXAMPLES=1
      shift
      ;;
    h)
      usage
      shift
      ;;
    * )
      usage
      ;;
  esac
done

#
# give them samples for visudo
#
if [ ${SUDO_EXAMPLES} == "1" ]; then
  sudo_examples
  exit 1
fi

${LOGGER} starting $0

sanity_checks

fetch_new_fingerprints

install_new_fingerprints

if [ ${NEW_FINGERPRINTS_FOUND} == "1" ]; then
  restart_services
else
  ${LOGGER} no new fingerprints found
fi

${LOGGER} stopping $0

This is the corresponding configuration file:

$ cat /usr/local/etc/anvil/finger-puller.conf
DOWNLOAD_DIR="/var/db/anvil/fingerprints"
CERT_SERVER="https://certs.example.org/cert-fingerprints"
MY_FINGERPRINTS="relay_clientcerts relay_clientcerts.db"
SERVICES="postfix"

What am I doing instead?

Instead of this approach, I have each sending Postfix server doing something like this.

Client side

On the sending servers, I have this in main.cf:

smtp_sasl_auth_enable = yes
smtp_sasl_password_maps = hash:/usr/local/etc/postfix/sasl_passwd
smtp_sasl_type = cyrus

sasl_passwd contains lines like this:

[receivinghost.example.org]:25     username:password

After amending that file, be sure to run postmap sasl_passwd.

See the Postfix SASL Howto for more detail.

NOTE: I have cyrus-sasl installed, but there is nothing cyrus-specific running.

Server side

On the receiving server, I have this:

smtpd_sasl_auth_enable = yes
smtpd_sasl_local_domain = $myhostname
smtpd_sasl_path = private/auth
smtpd_sasl_type = dovecot

Configuring dovecot is outside scope for this post but here are two things I found:

* A post I made to the Postfix Users mailing list.
* An old FreeBSDDiary article from 2007 when I was more concerned about self-generated certs. Today, we have Let’s Encrypt.

Hope that helps.

Website Pin Facebook Twitter Myspace Friendfeed Technorati del.icio.us Digg Google StumbleUpon Premium Responsive

Leave a Comment

Scroll to Top