Letsencrypt.org dual certs

We all love Letsencrypt.org don't we? I've been using them for quite a while, but found out that the renew process using DNS was failing. So I decided to finally move it all to the more simple method with a webserver (NGINX of course is the choice).

At the same time I decided to enable dual certificates (ECDSA and RSA for backwards compatibility). Do note this requires the latest nginx mainline on your server.

I created the directory /etc/nginx/globals.d and included the following sniplet in every HTTP/HTTPS vhost: (as the first block, to avoid HTTP->HTTPS redirects taking precedence)
include /etc/nginx/globals.d/*conf;

Created the file /etc/nginx/globals.d/acme.conf with the following content:

location /.well-known/acme-challenge/ {
        alias /var/www/le_root/.well-known/acme-challenge/;
    }

(Make sure to create the directory /var/www/le_root)

Restart nginx for the changes to take effect, and now we can start issuing certificates.

I won't go into details about installing acme.sh (I have mine in ~root/.acme.sh, and have set CERT_HOME='/etc/nginx/certificates' in ~root/.acme.sh/account.conf, which you will need to update further down the guide, including the script, if you want it somewhere else).

I wrote a custom script for handling it all, including spitting out the values for TLSA records (I pin it to the key, and not the certificate, to avoid needing DNS maintenance for the renewals).

For every domain you need to create SAN entries in /etc/ssl/openssl.cnf like so:

[SANPERLPIMPNET]
subjectAltName=DNS:perlpimp.net,DNS:www.perlpimp.net,DNS:perlpimp.dk,DNS:www.perlpimp.dk

The script places CSRs and keys in /var/www/$domain/ssl

To use my script put the following in /usr/local/bin/le-create.sh and make the script executable (remember to update the line starting with -subj "/C=ES/ST=...):

#!/usr/bin/sh
DOMAIN=$1
SANNAME=$2
KEYTYPE=$3

print_help()
{
    echo "Script requires 3 params, DOMAIN, SANNAME, KEYTYPE!"
    exit;
}

if [ -z "$DOMAIN" ]
then
    print_help;
fi

if [ -z $SANNAME ]
then
    print_help;
fi

if [ -z $KEYTYPE ]
then
    print_help;
fi

SSL_PATH="/var/www/$DOMAIN/ssl"

KEYNAME="";
CSRNAME=""
OPENSSL_KEYGEN=""
CRTNAME=""

if [ "$KEYTYPE" == "ecdsa" ]
then
    KEYNAME="$SSL_PATH/$DOMAIN.ecdsa.key";
    CSRNAME="$SSL_PATH/$DOMAIN.ecdsa.csr";
    OPENSSL_KEYGEN="openssl ecparam -genkey -name secp256r1 -out $KEYNAME"
elif [ "$KEYTYPE" == "rsa" ]
then
    KEYNAME="$SSL_PATH/$DOMAIN.rsa.key";
    CSRNAME="$SSL_PATH/$DOMAIN.rsa.csr";
    OPENSSL_KEYGEN="openssl genrsa -out $KEYNAME 4096"
else
    echo "Wrong key type, can be either ecdsa or rsa";
    exit;
fi

if [ ! -d "/var/www/$DOMAIN/ssl" ];
then
    mkdir -p /var/www/$DOMAIN/ssl;
fi

if [ -f $KEYNAME ]
then
    echo "Key exists, aborting"
    exit
fi

echo "Generating $KEYNAME";
echo "Using $OPENSSL_KEYGEN";
$OPENSSL_KEYGEN
echo "Generating CSR"
openssl req \
   -new -key "$KEYNAME" -sha256 -nodes \
   -reqexts "$SANNAME" \
   -out "$CSRNAME" \
   -subj "/C=ES/ST=Malaga/L=Estepona/O=Unixpimps.NET/emailAddress=sysops@unixpimps.net/CN=$DOMAIN" \

/root/.acme.sh/acme.sh --signcsr -w /var/www/le_root \
    --csr $CSRNAME

echo "DANE:"

if [ "$KEYTYPE" == "ecdsa" ]
then
    openssl ec -in $KEYNAME -outform der -pubout |sha256sum
elif [ "$KEYTYPE" == "rsa" ]
then
    openssl rsa -in $KEYNAME -outform der -pubout |sha256sum
fi

To create for example the ECDSA key for the domain perlpimp.net simply run
/usr/local/bin/le-create.sh perlpimp.net SANPERLPIMPNET ecdsa

When the script completes you'll see lines like this in the output:

DANE:
read EC key
writing EC key
5aa4405d5a134f22b84b8de2b2f7b23998f2e80fb3103b259d2f087883740cd2  -

To use DANE you need to add records like this to DNS (adapt depending on need):

_443._tcp    IN TLSA 3 1 1 5aa4405d5a134f22b84b8de2b2f7b23998f2e80fb3103b259d
2f087883740cd2
_443._tcp.www    IN TLSA 3 1 1 5aa4405d5a134f22b84b8de2b2f7b23998f2e80fb3103b259d
2f087883740cd2

Since I have both ECDSA and RSA fallback (acme.sh puts ECDSA in the _ecc suffixed directory in CERTHOME), I've configured the vhost as follows (simply as a refrence):

include /etc/nginx/globals.d/*conf;
    listen       [::]:443 ssl;
    ssl                  on;
    ssl_certificate      /etc/nginx/certificates/perlpimp.net/fullchain.cer;
    ssl_certificate_key  /var/www/perlpimp.net/ssl/perlpimp.net.rsa.key;
    ssl_certificate      /etc/nginx/certificates/perlpimp.net_ecc/fullchain.cer;
    ssl_certificate_key  /var/www/perlpimp.net/ssl/perlpimp.net.ecdsa.key;
    ssl_ciphers "EECDH+ECDSA+AESGCM EECDH+aRSA+AESGCM EECDH+ECDSA+SHA384 EECDH+ECDSA+SHA256 EECDH+aRSA+SHA384 EECDH+aRSA+SHA256 EECDH EDH+aRSA !aNULL !eNULL !LOW !3DES !MD5 !EXP !PSK !SRP !DSS !RC4";
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    ssl_prefer_server_ciphers on;
    ssl_session_cache  builtin:1000  shared:SSL:10m;
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains";
    add_header X-Frame-Options DENY;
    add_header X-Content-Type-Options nosniff;
    ssl_stapling on;
    ssl_dhparam /etc/nginx/dhparam.pem;
    ssl_stapling_verify on;
    resolver_timeout 5s;
    access_log /var/www/perlpimp.net/log/access.log main;
    root /var/www/perlpimp.net/html;
    server_name perlpimp.net www.perlpimp.net;
    client_max_body_size 10M;

Lastly, I use the following crontab entry to keep renewals going all on their own (runs every night at midnight):

0 0 * * * "/root/.acme.sh"/acme.sh --cron --home "/root/.acme.sh" --renew-hook "/usr/bin/systemctl reload nginx" > /dev/null
Show Comments