Generating reusable CSRs for Let’s Encrypt with TLSA

Let’s Encrypt’s standard ACME client, Certbot, allows you to reuse Certificate Signing Requests (CSRs). This is helpful because then you can keep a consistent TLSA record like I talked about in a previous article. DANE with TLSA is becoming extremely popular for authenticating encrypted email services, but they can be written for any TLS service so clients can authenticate the server. CSRs can be generated in a Linux terminal with OpenSSL, which is standard in many distributions of Linux. I wrote a set of scripts which automate the process of generating reusable CSRs, requesting key signing from Let’s Encrypt, calculating the hash of the public key, and creating a symbolic link in a standard location for these certificates. This article focuses on the first script in the series: letsencrypt-generate

Configuration

The first thing each of these scripts does is set up an environment with directory locations and file names. The MAIN_DIR is the first one to get defined because all the other paths are relative to it. The default location is /etc/ssl/letsencrypt. I wanted a safe default, so this places the files in a directory which is unlikely to be disturbed. On my own servers, I actually change this to /etc/letsencrypt because the scripts use subdirectory names which mirror what certbot uses, but the file names of the actual keyfiles are different. The only conflict is with the names of the symbolic links. This set of scripts names the symbolic links the same way that certbot does. That isn’t actually a problem because there really should be only one live certificate for each primary domain name. You can have multiple valid certificates for one domain name, but only one should be considered “live.”

MAIN_DIR=/etc/ssl/letsencrypt

The very next step is to load in the defaults file, if it exists. I added this to make it easier to change the few settings which are duplicated across this set of scripts, like MAIN_DIR. Having that one central location to edit settings just reduces the odds of forgetting to update something (like email address!).

[ -f /etc/default/letsencrypt-tlsa ] && . /etc/default/letsencrypt-tlsa

Input domain names

Now that general settings are out of the way, the script checks to see if you entered a domain name on the command line when you called the script. This particular script needs every Fully Qualified Domain Name (FQDN) which you wish to name in the certificate, starting with the primary domain name. The other scripts don’t strictly need you to name each one, but the primary domain name must be consistent because that’s what it uses to name and locate the certificates.

if [ "$#" -lt 1 ]
then
echo "Usage: $0 domain [space-separated list of alternate domains]" >&2
exit 1
fi

Environment

These scripts use the current system time to create a unique name for each certificate. This prevents file name conflicts even if the scripts share certbot’s namespace.

DATE=$(date +%Y%m%d%H%M%S)

OpenSSL expects alternate names to be specified with a comma separated list of “DNS:” followed by the FQDN. For example: DNS:example.com,DNS:www.example.com. This script creates a environment variable holding the first named domain name with the “DNS:” prefix, and a loop down below will append the rest of the FQDNs.

DOMAINS="DNS:$1"

The COMMON_NAME environment variable holds the first named FQDN for convenience. It is used to name directories and keep CSRs separated from each other.

COMMON_NAME="$1"

Each type of certificate gets its own subdirectory, so these environment variables name the locations where it will place these files.

CSR_DIR=${MAIN_DIR}/csr/${COMMON_NAME}
PRIVKEY_DIR=${MAIN_DIR}/keys/${COMMON_NAME}

This script creates both a CSR and a private key, so they need unique file names, which are generated using the time stamp.

CSR_PATH="${CSR_DIR}/csr-${DATE}.der"
PRIVKEY_PATH="${PRIVKEY_DIR}/privkey-${DATE}.pem"

Now the script appends each alternate FQDN to the list of names for OpenSSL. This is done with a simple BASH loop.

shift
for x in "$@"
do
DOMAINS="$DOMAINS,DNS:$x"
done

A few directories need to exist so OpenSSL can put files there. These directories are created with the -p option to create parent directories and silence errors about existing directories since this may not be the first time the user runs this script. The private key directory is also set to more restrictive permissions.

mkdir -p ${CSR_DIR}
mkdir -p ${PRIVKEY_DIR}
chmod 700 ${PRIVKEY_DIR}

OpenSSL magic

Here is the meat of the script. I’ll break it out into each option to discuss what is happening. Each of these lines end with a backslash character to tell BASH that the line is not over. You can, of course, delete the backslash and put all these lines together in one great big line if you want. I chose to split it apart for readability, both in the script and in the documentation.

OpenSSL is called with the req command, which tells OpenSSL to perform CSR-related actions.

openssl req \

This long beast of a line is what I used to eliminate the need for an OpenSSL configuration file. Normally the -config option is used to specify the file OpenSSL should use to define its behavior, instead we’re using a little BASH-magic to put those options in the command line.

-config <(printf "[req]\ndistinguished_name=req_dn\n[req_dn]\ncommonName=${COMMON_NAME}\n[san]\nsubjectAltName=${DOMAINS}") \

We’re generating a new key pair specifically for this CSR, so specify that on the command line.

-new \

The output of OpenSSL does not need to be encrypted, so we’re disabling that.

-nodes \

Subject should be blank to prevent errors, so we’ll specify a single forward slash for that.

-subj '/' \

OpenSSL needs to be made aware of the configuration option for alternate domain names, so this option does that.

-reqexts san \

The CSR will be saved in the location specified by the -out option.

-out "${CSR_PATH}" \

The private key will be saved in the location specified by the -keyout option. This should be a directory which is not world-readable. The script made sure of that earlier.

-keyout "${PRIVKEY_PATH}" \

Some people debate about the effectiveness of 4096 bit keys over Certbot’s default of 2048, but I prefer the higher bit count. All this means is it takes far more computing power to crack the key. It’s already cost-prohibitive to crack a 2048 bit key, but the cost of computing keeps dropping and I’d rather be prepared for the future. If you want, change this back to 2048. You’ll save a little compute time.

-newkey rsa:4096 \

Certbot seems to be switching over to DER format instead of PEM format for their CSRs, but either one should work. I just use DER here.

-outform DER

Conclusion

That’s all there is to the script. If it executes successfully, you should have a shiny new CSR and private key which are ready to be signed in the next step. These files can be saved and reused each time you renew your certificate so you don’t have to update your TLSA records whenever your certificate expires.


Featured image by Wikipedia user PierreSelim

I hope this helps you too. If it does, please share it.
Share

Leave a Reply

avatar
  Subscribe  
Notify of