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