LetsEncrypt 23 jan 2016 | Last updated: 16 feb 2016 08:26



This page describes a setup to renew LetsEncrypt certificates without the official Python client that has too many dependencies and needs to run as root.

LetsEncrypt

Started this as I felt that the standard LetsEncrypt client was way too fat and had too many dependencies to be allowed to run as root. Even though this is all pretty basic stuff, I decided to document it here.

Guide changed to use the security/letsencrypt.sh port

The original guide can be found in the lower half of this document.

Some notes on the configuration of my setup

  1. All services accessible from the internet run in jails (all jails reside in /usr/jails by default on FreeBSD)
  2. I use LibreSSL port
  3. I use zsh port

Things that don't need to run as root will be running as an unprivileged user. I will use the user _letsencrypt with group _letsencrypt as the unprivileged user that will perform the certificate renewal process. Deployment of the keys and certificates will have to be executed with a privileged user, this guide uses root.

Install Letsencrypt.sh

The port is available in the ports tree. Install it using the official pkg repository using

pkg install letsencrypt.sh

or alternatively (e.g. when you want to use ZSH) build your own using Poudriere or any of the other building-from-source options and install it. The port works with either zsh or bash.

This will install the actual script into /usr/local/bin. Configuration will land in /usr/local/etc/letsencrypt.sh as will the keys, certificates and certificate-chains. You should want to check that the configuration directory is not world-writable.

Prepare user & directories

To make life easier all of the challenges (!LetsEncrypt as well as keybase etc) will be hosted in a shared dir /usr/local/www/.well-known on the jail running my Apache server. The !LetsEncrypt bits will land in /usr/local/etc/letsencrypt.sh on the host system. There's no requirement to run the interaction with the !LetsEncrypt servers as root, so these will run as a non-privileged user. This unprivileged user will have to write to the acme-challenge and the directory that will contain the keys and certificates.

pw groupadd -n _letsencrypt -g 443
pw useradd  -n _letsencrypt -u 443 -g 443 -d /usr/local/etc/letsencrypt.sh -w no -s /nonexistent
chown root:_letsencrypt /usr/local/etc/letsencrypt.sh
chmod 770               /usr/local/etc/letsencrypt.sh
mkdir -p -m 775    /usr/jails/http/usr/local/www/.well-known/acme-challenge
chgrp _letsencrypt /usr/jails/http/usr/local/www/.well-known/acme-challenge

Adapt the .well-known/acme-challenge directory to your situation.

Modify Apache configuration

The acme validation will GET a uniquely named file from http://<example.org>/.well-known/acme-challenge/

Access to the .well-known directory is granted in my main Apache config file /usr/local/etc/apache24/httpd.conf

<Directory "/usr/local/www/.well-known/">
   Options None
   AllowOverride None
   Require all granted
   Header add Content-Type text/plain
</Directory>

The Content-Type header was in my configs somewhere, shouldn't hurt.<
> If you want to only share the ACME challenges you can suffix .well-known/ with acme-challenge/

Now every (non-ssl) Virtual Host that I have gets a on-line addition

Alias /.well-known/ /usr/local/www/.well-known/

{{{#!wiki caution You need to make sure that all (sub-)domains that you want to sign have access to this directory!<
> That includes rewrites etc.<
> The acme validation is done only using '''plain http''' and will not honour redirects etc. }}}

Letsencrypt configuration

Domains to sign

The script requires a list of domain names you want to have a SAN cert for in the following format:

example.com www.example.com
example.net www.example.net wiki.example.net

Domains and sub-domains that are listed on the ''same line'' will result in SAN-certificates (Subject-Alternative-Name).<
> Store this as /usr/local/etc/letsencrypt.sh/domains.txt

The default configuration file requires some changes, these are stored in /etc/ssl/letsencrypt/config.sh

BASEDIR="/usr/local/etc/letsencrypt.sh"
WELLKNOWN="/usr/jails/http/usr/local/www/.well-known/acme-challenge"
alias openssl='/usr/local/bin/openssl'

To make use of LibreSSL (or OpenSSL from ports) you must add the alias openssl to the config script.

Configure periodic job

The security/letsencrypt.sh port includes a periodic job that makes automation very simple. To automatically renew certificates, add the following to your /etc/periodic.conf

weekly_letsencrypt_enable="YES"
weekly_letsencrypt_user="_letsencrypt"
weekly_letsencrypt_deployscript="/usr/local/etc/letsencrypt.sh/deploy.sh"

First run

You will probably want to run !LetsEncrypt manually the first time

cd /etc/ssl/letsencrypt
su -m _letsencrypt -c 'zsh ./letsencrypt.sh --cron'

(Replace zsh with bash if that's the shell you have installed)

You will end up with a sub-directory certs that contains your domains as directories with the Subject-Alternative-Names certs and the corresponding private keys. The symbolic links are useful for deployment.

example.com/
   cert-1453573903.csr
   cert-1453573903.pem
   cert.csr -> cert-1453573903.csr
   cert.pem -> cert-1453573903.pem
   chain-1453573903.pem
   chain.pem -> chain-1453573903.pem
   fullchain-1453573903.pem
   fullchain.pem -> fullchain-1453573903.pem
   privkey-1453573903.pem
   privkey.pem -> privkey-1453573903.pem
example.net/
   cert-1453576309.csr
   cert-1453576309.pem
   cert.csr -> cert-1453576309.csr
   cert.pem -> cert-1453576309.pem
   chain-1453576309.pem
   chain.pem -> chain-1453576309.pem
   fullchain-1453576309.pem
   fullchain.pem -> fullchain-1453576309.pem
   privkey-1453576309.pem
   privkey.pem -> privkey-1453576309.pem

Deploy new certs

We've run the certificate request process as a restricted user but you'll need to run the deploy script as root.

Here you'll probably need to get creative with scripting. In the host environment, your now have

/usr/local/etc/ssl/letsencrypt.sh/certs/example.net/privkey.pem
/usr/local/etc/ssl/letsencrypt.sh/certs/example.net/fullchain.pem

NB: These are symlinks!!!<
>

Example (jailed) applications

Your Apache server may (should?) run in the http jail and you've setup an Apache Virtual Host with

SSLCertificateFile /etc/ssl/certs/example.net.pem
SSLCertificateKeyFile /etc/ssl/priv/example.net.pem

and your OpenSMTPd mailserver for example.net in the mail jail

pki example.net certificate "/etc/ssl/certs/example.net.pem"
pki example.net key         "/etc/ssl/priv/example.net.pem"
listen on $lan_addr port 587 tls-require \
       pki example.net hostname example.net auth

Seen from the host environment your certificates actually need to end up in

/usr/jails/```http```/etc/ssl
/usr/jails/```mail```/etc/ssl

Example deployment script

You could use the following script to deploy new certs

#!/bin/sh
domain="example.net"
letsencryptdir="/usr/local/etc/letsencrypt.sh"
targets="mail http"
for jail in ${targets}; do
  targetdir="/usr/jails/${jail}/etc/ssl"
  # Check if the certificate has changed
  [[ -z "`diff -rq ${letsencryptdir}/certs/${domain}/fullchain.pem ${targetdir}/certs/${domain}.pem`" ]] && continue
  cp -L "${letsencryptdir}/certs/${domain}/privkey.pem"   "${targetdir}/priv/${domain}.pem"
  cp -L "${letsencryptdir}/certs/${domain}/fullchain.pem" "${targetdir}/certs/${domain}.pem"
  chmod 400 "${targetdir}/priv/${domain}.pem"
  chmod 644 "${targetdir}/certs/${domain}.pem"
  # Restart/-load relevant services
  [[ "${jail}" = "http" ]] && jexec ${jail} service apache24 restart
  [[ "${jail}" = "mail" ]] && jexec ${jail} service smtpd    restart
done
# Clean up old keys and certs
/usr/local/bin/letsencrypt.sh --cleanup

Store this as /usr/local/etc/letsencrypt.sh/deploy.sh and make sure the execute bit is set.<
> '''NB:''' Some applications want a private key, certificate and separate chain instead. If this is the case you'll need to copy cert.pem and chain.pem to the appropriate location in stead.

Old method (not using ports)

This would also be usable on non-FreeBSD systems.

You will want to check the earlier information for more detail.

Prepare user & directories

To make life easier all of the challenges (!LetsEncrypt as well as keybase etc) will be hosted in a shared dir /usr/local/www/.well-known on the jail running my Apache server. The !LetsEncrypt bits will land in /etc/ssl/letsencrypt on the host system. There's no requirement to run the interaction with the !LetsEncrypt servers as root, so these will run as a non-privileged user. This unprivileged user will have to write to the acme-challenge and the directory that will contain the keys and certificates.

pw groupadd -n _letsencrypt -g 443
pw useradd  -n _letsencrypt -u 443 -g 443 -d /etc/ssl/letsencrypt -w no -s /nonexistent
mkdir -m 770            /etc/ssl/letsencrypt
chown root:_letsencrypt /etc/ssl/letsencrypt
mkdir -p -m 775    /usr/jails/http/usr/local/www/.well-known/acme-challenge
chgrp _letsencrypt /usr/jails/http/usr/local/www/.well-known/acme-challenge

Adapt the .well-known/acme-challenge directory to your situation. It is assumed that a webserver is already installed and /usr/jails/http/usr/local/www already exists, otherwise the modes on the path elements to acme-challenge will be way to wide.

LetsEncrypt Script

This method uses the script from Lukas Schauer which he hosts on GitHub. This script does not require Python and doesn't have any requirements besides bash or zsh. Download the script and default config from GitHub

fetch -o /etc/ssl/letsencrypt/letsencrypt.sh https://github.com/lukas2511/letsencrypt.sh/raw/master/letsencrypt.sh
fetch -o /etc/ssl/letsencrypt/config.sh https://github.com/lukas2511/letsencrypt.sh/raw/master/config.sh.example
chgrp _letsencrypt /etc/ssl/letsencrypt/{letsencrypt,config}.sh

The latest version of the script supports zsh but you'll have to invoke it as zsh letsencrypt.sh or alternatively modify the !#shebangs in the scripts.

Letsencrypt configuration

The default configuration file requires some changes, these are stored in /etc/ssl/letsencrypt/config.sh

BASEDIR="/etc/ssl/letsencrypt"
WELLKNOWN="/usr/jails/http/usr/local/www/.well-known/acme-challenge"
alias openssl='/usr/local/bin/openssl'

To make use of LibreSSL (or OpenSSL from ports) you must add the alias openssl to the config script.

First run

You are now setup to actually run !LetsEncrypt

cd /etc/ssl/letsencrypt
su -m _letsencrypt -c 'zsh ./letsencrypt.sh --cron'

(Replace zsh with bash if that's the shell you have installed)

Automate

Now you can schedule your certificate renewal job. By default the script will only renew if the remaining validity of your certificates is less than a month. A weekly job would thus be adequate.

As root

#!/bin/sh

# Run script to renew certs
cd /etc/ssl/letsencrypt
su -m _letsencrypt -c 'zsh ./letsencrypt.sh --cron'

# Deploy certs if they've been renewed
domain="example.net"
targets="mail http"
for jail in ${targets}; do
  [[ -z "`diff -rq /etc/ssl/letsencrypt/certs/${domain}/fullchain.pem /usr/jails/${jail}/etc/ssl/certs/${domain}.pem`" ]] && continue
  cp -L "/etc/ssl/letsencrypt/certs/${domain}/privkey.pem"   "/usr/jails/${jail}/etc/ssl/priv/${domain}.pem"
  cp -L "/etc/ssl/letsencrypt/certs/${domain}/fullchain.pem" "/usr/jails/${jail}/etc/ssl/certs/${domain}.pem"
  chmod 400 "/usr/jails/${jail}/etc/ssl/priv/${domain}.pem"
  chmod 644 "/usr/jails/${jail}/etc/ssl/certs/${domain}.pem"
  # Restart/-load relevant services
  [[ "${jail}" = "http" ]] && jexec ${jail} service apache24 restart
  [[ "${jail}" = "mail" ]] && jexec ${jail} service smtpd    restart
done

You'll need to adapt your script for your requirements, this is merely an example of how it ''could'' be done.