letsencrypt.sh

So, while this site cannot be equipped with any kind of TLS certificates (don't ask), I'm using Let's Encrypt certificates for some other web sites. But as much as I like everything the EFF does, I despise their official LE client, for apparent reasons:

$ sudo apt-get install certbot
[....]
The following NEW packages will be installed:
  certbot python-acme python-certbot python-cffi-backend python-configargparse python-configobj python-cryptography python-enum34 python-funcsigs python-idna python-ipaddress
  python-mock python-openssl python-parsedatetime python-pbr python-pyasn1 python-requests python-rfc3339 python-six python-tz python-urllib3 python-zope.component
  python-zope.event python-zope.hookable python-zope.interface
0 upgraded, 25 newly installed, 0 to remove and 3 not upgraded.
No, thank you :-\ Luckily, their ACME protocol allows for many more client options to choose from. After some experiments, I almost settled for letsencrypt.sh, but ran into unfixed bugs and ended up with a fork of the same. With that, I wanted to cover two use cases:

Local server

In this scenario, the letsencrypt.sh client is requesting (and validating) certificates for the same machine it's running on. This is also the machine where our account key resides. The work flow is basically:
## Needs to be done only once:
$ letsencrypt.sh register -a letsencrypt-account-key.pem \
      -e webmaster@example.org

$ umask 0022
$ letsencrypt.sh sign -a letsencrypt-account-key.pem \
      -k letsencrypt-example-key.pem \
      -w /var/www/.well-known/acme-challenge/ \
      -c letsencrypt-$(date -I)-example-cert.pem www.example.org mail.example.org
The well-known path should be writeable by the user executing the letsencrypt.sh script and readable by the webserver. That way, we don't have to play funky games with our webserver configuration, trying to generate the responses dynamically but instead we (temporarily) create actual files in that directory to be validated in the process. This may not work when renewing certificates for different domains, though.

We may need the full certificate chain (and some DH parameters too), so let's concatenate them altogether:
$ cat letsencrypt-$(date -I)-example-cert.pem \
      letsencrypt-$(date -I)-example-cert.pem_chain \
      dhparams-2048.pem \
      > letsencrypt-$(date -I)-example-cert-combined.pem
The resulting file can then be installed as SSLCertificateFile or ssl_certificate or ssl_cert or whatever service is in use here.

Remote server

While the letsencrypt.sh client seems small enough, we still don't want to install it on every server that needs a certificate. Instead, we'll use letsencrypt.sh to issue certificates for a remote machine. However, as the certificates are domain-validated, we need a way to transfer the validation token to the remote server. Luckily, this version of letsencrypt.sh is able to do just that:
$ letsencrypt.sh sign -a letsencrypt-account-key.pem \
      -k letsencrypt-foobar-key.pem \
      -P /usr/local/bin/push-response-ssh \
      -c letsencrypt-$(date -I)-foobar-cert.pem foobar.net www.foobar.net
Here, a hook script is transferring the token to the remote server (configured in the same script). On the remote side, another hook will read the validation token from stdin and install it in its own well-known location (TOKEN_DIR). This can all be configured with SSH key authentication:
foobar$ cat ~www-data/.ssh/authorized_keys 
command="/usr/local/sbin/push-response-ssh-remote" ssh-ed25519 AAAAC3[...] admin@local

foobar$ grep ^TOKEN_DIR /usr/local/sbin/push-response-ssh-remote 
TOKEN_DIR="/var/www/.well-known/acme-challenge"
The resulting certificate is still installed locally and needs to be transferred to the remote side. Since we configured the remote www-data account to only allow the hook script to execute, we have adjusted the same somewhat to allow the certificate to be installed by the same user. Since we're now abusing the original hook script (and multiple command directives for a single key are not supported), our deployment command looks somewhat convoluted:
$ cat letsencrypt-foobar-key.pem \
      letsencrypt-$(date -I)-foobar-cert.pem{,_chain} | \
      ssh -i ~/.ssh/letsencrypt-key www-data@foobar.net \
      installkey \
      aol.com \
      $(openssl rand -hex 32 | cut -c-43) \
      $(letsencrypt.sh thumbprint -a letsencrypt-account-key.pem | awk '{print $NF}')
So, the new installkey parameter tells the remote hook script what to do. The aol.com and the random value are just place holders for a valid domain name resp. something that looks like a validation token. This is all because push-response-ssh-remote expects all these things. The deployment would be much easier if we 1) used a different user or key or 2) re-write the remote hook to allow for a simpler deployment :-)

With that in place, the certificate for the remote side has been saved to whatever is configured in push-response-ssh-remote and can now be used in the respective services. Yay! \o/