Automating Keychain import on macOS

This is a thread for discussing how to import Let’s Encrypt certificates into Keychain on macOS automatically. Apparently this is necessary for some server environments, so it will be helpful to have a post-renewal script that can be run automatically to perform this step.

What does the array passed via --renew-hook look like?

There should be two, called RENEWED_DOMAINS (a space-separated list of the domains in the renewed certificate) and RENEWED_LINEAGE (the location of the certificate that was renewed). I think the latter is the renewal configuration file (in /etc/letsencrypt/renewal) but we can double-check that.

Okay, here is what I have so far. Will test, but if anyone tries first, let me know.

#!/bin/bash
PATHTOLOG='/Library/Logs/LetsEncrypt/'
LOGFILE=$PATHTOLOG'macOS_Keychain_Import_Log.txt'

RED='\033[0;31m'
YELLOW='\033[0;33m'
NC='\033[0m' # No Color
CURRENTDATE=$(date +"%a %b %d, %Y at %r")

if [ ! -d "$PATHTOLOG" ]; then
	mkdir $PATHTOLOG
	touch $LOGFILE
fi

echo "---Let's Encrypt Auto Renewal ${CURRENTDATE} ---" >> $LOGFILE
set -e

#RENEWED_DOMAINS='example.com apple.com test.com whatever.us'

for domain in $RENEWED_DOMAINS; do
	echo "Processing: ${domain}" >> $LOGFILE

	# bash generate random 32 character alphanumeric string (upper and lowercase)
	TEMP_PASS=$(cat /dev/urandom | env LC_CTYPE=C tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1)


	# Export the $domain LetsEncrypt Cert to Keychain Access Format (.p12)
	echo "Converting Cert to Keychain Access Format (.p12)" >> $LOGFILE
	eval openssl pkcs12 -export -inkey /etc/letsencrypt/live/$domain/privkey.pem -in /etc/letsencrypt/live/$domain/cert.pem -certfile /etc/letsencrypt/live/$domain/fullchain.pem -out /etc/letsencrypt/live/$domain/letsencrypt_sslcert.p12 -passout pass:$TEMP_PASS

	# Import the $domain LetsEncrypt Cert to Keychain Access
	echo "Importing Cert to Keychain Access" >> $LOGFILE
	eval security import /etc/letsencrypt/live/$domain/letsencrypt_sslcert.p12 -f pkcs12 -k /Library/Keychains/System.keychain -P $TEMP_PASS -T /Applications/Server.app/Contents/ServerRoot/System/Library/CoreServices/ServerManagerDaemon.bundle/Contents/MacOS/servermgrd

done

Two things about this:

First, eval isn't necessary in order to run external commands in shell scripts! You can just give the command line and it will be run (including variable substitutions with $).

Second, I'm afraid this isn't what RENEWED_DOMAINS means. In Certbot, each certificate is named with a "lineage name" (more recently often called "certname" or "certificate name"), which is originally the first domain name specified on the command line when the certificate was originally created. However, a single certificate can cover up to 100 domain names. When the certificate is renewed, all of these domain names will be listed in the RENEWED_DOMAINS variable, but that doesn't mean that the various PEM files will exist for each domain name within /etc/letsencrypt/live; rather, only one single PEM file exists covering all of them, whose name we have to reconstruct from RENEWED_LINEAGE — I'll double check how if you like.

You can have multiple certificates installed on the system, but that should result in the renew hook being run once per renewed certificate.

That makes sense. I was hoping this worked, rather than building the logic to see which certs are renewed.

The reason for eval, is it outputs errors to the console in macOS for troubleshooting.

I can try to figure out a way of doing that. This is the exact kind of scenario for which the renewal hook feature exists, so we should be able to make it work. :slight_smile:

Huh, I wonder why that is.

Great, I’ll wait before I create a sub-routine to scan the certs folder for changes.

I believe it is because the eval command always has output, while most other commands may only have output on failure .

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.