Let's Encrypt with Pound on Debian
Wed, 04 Nov 2015 13:26 categories: debianTLDR: mister-muffin.de (and all its subdomains), bootstrap.debian.net and binarycontrol.debian.net are now finally signed by "Let's Encrypt Authority X1" \o/
EDIT2: I created this post when Let's Encrypt was still in beta. For a recipe of how to use letsencrypt with pound and without super user privileges read the very last section at the bottom.
I just tried out the letsencrypt client Debian packages prepared by Harlan Lieberman-Berg which can be found here:
My server setup uses Pound as a reverse proxy in front of a number of LXC based containers running the actual services. Furthermore, letsencrypt only supports Nginx and Apache for now, so I had to manually setup things anyways. Here is how.
After installing the Debian packages I built from above git repositories, I ran the following commands:
$ mkdir -p letsencrypt/etc letsencrypt/lib letsencrypt/log
$ letsencrypt certonly --authenticator manual --agree-dev-preview \
--server https://acme-v01.api.letsencrypt.org/directory --text \
--config-dir letsencrypt/etc --logs-dir letsencrypt/log \
--work-dir letsencrypt/lib --email josch@mister-muffin.de \
--domains mister-muffin.de --domains blog.mister-muffin.de \
--domains [...]
I created the letsencrypt
directory structure to be able to run letsencrypt
as a normal user. Otherwise, running this command would require access to
/etc/letsencrypt
and others. Having to set this up and pass all these
parameters is a bit bothersome but there is an upstream
issue about making this
easier when using the "certonly" option which in princible should not require
superuser privileges.
The --server
option is necessary for now because "Let's Encrypt" is still in
beta and one needs to register for
it.
Without the --server
option one will get an untrusted certificate from the
"happy hacker fake CA".
The letsencrypt
program will then ask me for my agreement to the Terms of
Service and then, for each domain I specified with the --domains
option
present me the token content and the location under each domain where it
expects to find this content, respectively. This looks like this each time:
-------------------------------------------------------------------------------
NOTE: The IP of this machine will be publicly logged as having requested this
certificate. If you're running letsencrypt in manual mode on a machine that is
not your server, please ensure you're okay with that.
Are you OK with your IP being logged?
-------------------------------------------------------------------------------
(Y)es/(N)o: Y
Make sure your web server displays the following content at
http://mister-muffin.de/.well-known/acme-challenge/XXXX before continuing:
{"header": {"alg": "RS256", "jwk": {"e": "AQAB", "kty": "RSA", "n": "YYYY"}}, "payload": "ZZZZ", "signature": "QQQQ"}
Content-Type header MUST be set to application/jose+json.
If you don't have HTTP server configured, you can run the following
command on the target server (as root):
mkdir -p /tmp/letsencrypt/public_html/.well-known/acme-challenge
cd /tmp/letsencrypt/public_html
echo -n '{"header": {"alg": "RS256", "jwk": {"e": "AQAB", "kty": "RSA", "n": "YYYY"}}, "payload": "ZZZZ", "signature": "QQQQ"}' > .well-known/acme-challenge/XXXX
# run only once per server:
$(command -v python2 || command -v python2.7 || command -v python2.6) -c \
"import BaseHTTPServer, SimpleHTTPServer; \
SimpleHTTPServer.SimpleHTTPRequestHandler.extensions_map = {'': 'application/jose+json'}; \
s = BaseHTTPServer.HTTPServer(('', 80), SimpleHTTPServer.SimpleHTTPRequestHandler); \
s.serve_forever()"
Press ENTER to continue
For brevity I replaced any large base64 encoded chunks of the messages with
YYYY
, ZZZZ
and QQQQ
. The token location is abbreviated with XXXX
.
After temporarily stopping Pound on my webserver I created the directory
/tmp/letsencrypt/public_html/.well-known/acme-challenge
and then opened two
shells on my server, both at /tmp/letsencrypt/public_html
. In one, I kept a
tiny HTTP server running (like the suggested Python SimpleHTTPServer which will
also work if one has Python installed). In the other I copy pasted the echo
line that the letsencrypt
program suggested me to run.
I had to copypaste that echo
command for each domain I wanted to verify. This
could easily be automated, so I filed an issue about
this with upstream.
It seems that the letsencrypt servers query each of these tokens twice: once directly each time after having hit enter after seeing the message above and another time once all tokens are in place.
At the end of this ordeal I get:
2015-11-04 11:12:18,409:WARNING:letsencrypt.client:Non-standard path(s), might not work with crontab installed by your operating system package manager
IMPORTANT NOTES:
- If you lose your account credentials, you can recover through
e-mails sent to josch@mister-muffin.de.
- Congratulations! Your certificate and chain have been saved at
letsencrypt/etc/live/mister-muffin.de/fullchain.pem. Your cert will
expire on 2016-02-02. To obtain a new version of the certificate in
the future, simply run Let's Encrypt again.
- Your account credentials have been saved in your Let's Encrypt
configuration directory at letsencrypt/etc. You should make a
secure backup of this folder now. This configuration directory will
also contain certificates and private keys obtained by Let's
Encrypt so making regular backups of this folder is ideal.
I can now scp the content of letsencrypt/etc/live/mister-muffin.de/*
to my
server. Unfortunately, Pound (and also my ejabberd XMPP server) requires the
private key to be in the same file as the certificate and the chain, so on the
server I also had to do:
cat /etc/ssl/private/privkey.pem /etc/ssl/private/fullchain.pem > /etc/ssl/private/private_fullchain.pem
And edit the Pound config to use /etc/ssl/private/private_fullchain.pem
. But
that's all, folks!
EDIT
It seems that manually copying over the echo commands as I described above is
not necessary. Instead of using the certonly
plugin, I can use the webroot
plugin. That plugin takes the --webroot-path
option and will copy the tokens
to there. Since my webroot is on a remote machine, I could just mount it
locally via sshfs and pass the mountpoint as --webroot-path
.
That I didn't realize that the webroot plugin does what I want (and not the certonly plugin) can easily be explained by the only documentation of the webroot plugin in the help output and the man page generated from it being "Webroot Authenticator" which is not very helpful.
Another user seems to have run into similar problems. Better documenting the plugins so that these situations can be prevented in the future is tracked in this upstream bug.
EDIT2
Now that letsencrypt is out for everybody, lets update the instructions with
what I learned. Firstly, since we don't want a long downtime, we add the
following section to /etc/pound/pound.cfg
:
Service
URL "^/.well-known/acme-challenge/"
BackEnd
Address 127.0.0.1
Port 8000
End
End
This will make sure that all requests to /.well-known/acme-challenge/
and
below are redirected to a server running on port 8000. That service will be a
temporary webserver which we will only switch on for the purpose of retrieving
new certificates. So on my server I run:
$ mkdir ~/letsencrypt
$ (cd ~/letsencrypt && python3 -m http.server 8000)
Now on my laptop I mount that directory via sshfs locally:
$ sshfs fulda:/root/letsencrypt ~/letsencrypt/fulda
And finally I use the webroot
authenticator to automatically retrieve and
validate all my certificates. No manual intervention needed anymore:
$ letsencrypt certonly --authenticator webroot --text \
--config-dir letsencrypt/etc --logs-dir letsencrypt/log \
--work-dir letsencrypt/lib --email josch@mister-muffin.de \
--webroot-path ~/letsencrypt/fulda --domains mister-muffin.de \
--domains [...]
Now I can quit the python webserver running on my server and copy the generated certificates into their right locations.