There are as many ways of doing the Let's Encrypt thing as there are site admins on this planet. So here is my way of doing it, mainly as a documentation for myself and as a tutorial for a supervision class I'll be teaching tomorrow morning.
TL;DR;
This blog post describes how to obtain certificates from Let's Encrypt on a production web server in a non-privileged user context. We use the small and well-readable acme-tiny
[1] Python script for it.
Assumptions
- You know how e.g. Apache2 gets configured (in general)
- and you have a host running Apache2 that is reachable on the internet
- and it least has one DNS hostname associated with its public IP address.
- You have an idea about OpenSSL, requesting a signed certificate
- You know what privileges on a *nix system are and why it is bad mostly to run
self-updating scripts under a privileged user account (e.g. root)... (finger pointing at
certbot
development...)
Creating SSL Key and the Certificate Signing Request
For creating an SSL Key and a corresponding Certificate Signing Request, I use this little script, named web_certrequest.sh
:
#!/bin/bash
FQDN="$1"
FQDNunderscores="$(echo $FQDN | sed 's/\./_/g')"
base="$(pwd)"
test -d "$base/private" || mkdir -p "$base/private"
if [ -f "$base/private/${FQDNunderscores}.key" ]; then
openssl req -config "$base/openssl::${FQDNunderscores}.cnf" \
-nodes -new \
-key "$base/private/${FQDNunderscores}.key" \
-out "$base/${FQDNunderscores}.csr"
else
openssl req -config "$base/openssl::${FQDNunderscores}.cnf" \
-nodes -new \
-keyout "$base/private/${FQDNunderscores}.key" \
-out "$base/${FQDNunderscores}.csr"
fi
If you are doing all this for the first time, create an empty folder, put the script in it and make the script executable:
$ chmod u+x web_certrequest.sh
For each host (FQDN) that I need SSL certificates for I have a small openssl::<fqdn-with-underscores-instead-of-dots>.cnf
configuration file:
[ req ]
default_bits = 4096 # Size of keys
distinguished_name = req_distinguished_name
req_extensions = v3_req
x509_extensions = v3_req
[ req_distinguished_name ]
# Variable name Prompt string
#---------------------- ----------------------------------
0.organizationName = Organization Name (company)
organizationalUnitName = Organizational Unit Name (department, division)
emailAddress = Email Address
emailAddress_max = 40
localityName = Locality Name (city, district)
stateOrProvinceName = State or Province Name (full name)
countryName = Country Name (2 letter code)
countryName_min = 2
countryName_max = 2
commonName = Common Name (hostname, IP, or your name)
commonName_max = 64
# Default values for the above, for consistency and less typing.
# Variable name Value
#------------------------------ ------------------------------
0.organizationName_default = My Company # adapt as needed
localityName_default = My City # adapt as needed
stateOrProvinceName_default = My County/State # adapt as needed
countryName_default = C # country code: e.g. DE for Germany
commonName_default = host.example.com # hostname of the webserver
emailAddress_default = hostmasters@example.com # adapt as needed
organizationalUnitName_default = Webmastery # adapt as needed
[ v3_req ]
# Extensions to add to a certificate request
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment, keyEncipherment, dataEncipherment, keyAgreement
# Some CAs do not yet support subjectAltName in CSRs.
# Instead the additional names are form entries on web
# pages where one requests the certificate...
subjectAltName = @alt_names
[alt_names]
### host.example.com is the FQDN
DNS.1 = host.example.com
DNS.2 = www.example.com
DNS.3 = example.com
DNS.4 = wiki.example.com
DNS.5 = www.old-company-name.biz
The above config I place into the same directory as the web_certrequest.sh
script and name it openssl::host_example_com.cnf
.
Note: I will use host.example.com
further down as my host's FQDN. Adapt to your host name, please.
You then run the above script (you need to answer several questions on the way...)...
$ ./web_certrequest.sh host.example.com
and you'll get two files from that:
private/host_example_com.key
: The private key belonging to the to-be-signed certificatehost_example_com.csr
: The file containing the Certificate Signing Request. This file we will send to the Let's Encrypt signing engine...
Note: Once you have the .key
and the .csr
file, you only on rare occasions need to recreate them:
.key
file recreation:- if you need more crypto strengths (e.g. more than 4096 bits)
- except from that, leave it as is...
.csr
file recreation:- only if you recreate the
.key
file - if you need to include another host alias in subjectAltName
- whenever any of the DNS names in the certificate is not in DNS anymore
- if the certificate's meta data is wrong/bad/needs to be adapted (e.g. company names sometimes change)
- only if you recreate the
Next step: The host_example_com.csr
we will soon send to the Let's Encrypt signing engine, but let's prepare the webserver first.
Setting up the Webserver
With Let's Encrypt there are several ways of getting your mastery over a machine on the internet confirmed. One is to request a challenge from the LE API that you will answer via a webserver you control. Another approach is responding to an LE challenge via a DNS server you control. This blog post looks at web server based responses to the Let's Encrypt challenge.
Place the private key on the web server
Copy the private host_example_com.key
file to the web server under /etc/ssl/private
. Make sure, file permissions are 0600
and the file is owned by root:root
.
Create a letsencrypt system user
For the acme-tiny
call we need a non-privileged system user account:
$ sudo adduser --system --home /var/lib/letsencrypt --shell /bin/bash letsencrypt
Setup this letsencrypt
user's home directory in the following way:
(root@host) {/var/lib/letsencrypt} # ls -al
total 40
drwx--x--- 8 letsencrypt www-data 4096 Mar 1 2017 .
drwxr-xr-x 80 root root 4096 Apr 21 08:01 ..
drwx------ 2 letsencrypt nogroup 4096 Mar 6 2017 bin
drwx--x--- 2 letsencrypt www-data 4096 May 1 00:00 challenges
drwx------ 2 letsencrypt nogroup 4096 Mar 6 2017 .letsencrypt
The bin/
subfolder contains this:
(root@host) {/var/lib/letsencrypt/bin} # ls -al
total 12
drwx------ 2 letsencrypt nogroup 4096 Mar 6 2017 .
drwx--x--- 8 letsencrypt www-data 4096 Mar 1 2017 ..
-rwxr-xr-x 1 root root 464 Mar 6 2017 letsencrypt-renew-certs
The letsencrypt-renew-certs
script has this content:
#!/bin/bash
INTERMEDIATE=lets-encrypt-x3-cross-signed.pem
acme-tiny --account-key ~/.letsencrypt/account-host_example_com.key \
--csr ~/.letsencrypt/host_example_com.csr \
--acme-dir ~/challenges/ \
1> ~/.letsencrypt/host_example_com.crt && \
\
cat ~/.letsencrypt/host_example_com.crt \
~/.letsencrypt/${INTERMEDIATE} \
1> ~/.letsencrypt/host_example_com.fullchain.crt
Note: The script above needs the acme-tiny
tool, so don't forget to install it:
$ sudo apt-get install acme-tiny
And the .letsencrypt/
subfolder contains this:
(root@host) {/var/lib/letsencrypt} # ls -al .letsencrypt/
total 32
drwx------ 2 letsencrypt nogroup 4096 Mar 6 2017 .
drwx--x--- 8 letsencrypt www-data 4096 Mar 1 2017 ..
-rw-r--r-- 1 root root 3247 Mar 1 2017 account-host_example_com.key
-rw-r--r-- 1 root root 1984 Mar 1 2017 letsencryptauthorityx3.pem
-rw-r--r-- 1 root root 1647 Nov 16 2016 lets-encrypt-x3-cross-signed.pem
-rw-r--r-- 1 root root 2480 Mar 1 2017 host_example_com.csr
The file host_example_com.csr
needs to be copied over. We just created it above. Remember?
The files letsencryptauthorityx3.pem
[2] and lets-encrypt-x3-cross-signed.pem
[3] are intermediate certificates for the two different certificate chains offered by Let's Encrypt. For more info, see here [4].
The file account-host_example_com.key
is our account key for Let's Encrypt. Personally, I prefer one account key per web server I run, but some people have one account.key
file per admin. The file can be created with this command (do this as the letsencrypt
system user we just created in subdir .letsencrypt/
):
$ openssl genrsa 4096 > account-host_example_com.key
Note: When immitating the above, make sure that the file permissions and file ownerships on your web server system match with what you read above.
Getting started with Apache2
For retrieval of the first certificate, nearly nothing needs to be configured in Apache2, except from it being reachable on port 80 and except from the challenges path being available as URL http://host.example.com/.well-known/acme-challenge
:
Create /etc/apache2/conf-available/acme-tiny.conf
with this content:
Alias /.well-known/acme-challenge/ /var/lib/letsencrypt/challenges/
<Directory /var/lib/letsencrypt/challenges>
Require all granted
Options -Indexes
</Directory>
... and enable it:
$ sudo a2enconf acme-tiny
... and restart Apache2:
$ sudo invoke-rc.d apache2 restart
Obtaining the Certificate from Let's Encrypt
Then, for (regularly) obtaining / updating our certificate, we create a script that can be found via $PATH and should normally be called with root privileges (/usr/local/sbin/letsencrypt-renew-certs
), don't forget making it executable (chmod 0700 /usr/local/sbin/letsencrypt-renew-certs
):
#!/bin/bash
su - letsencrypt -c ~letsencrypt/bin/letsencrypt-renew-certs
invoke-rc.d apache2 restart
This script drops privileges for the certificate file update part and then re-launches Apache2. This is the only part that requires root privileges on the web server. Point is: after the certificate has been updated, we need to restart (or reload?) the web server and this requires root privileges.
If all is correctly in place, you should be able to obtain (and later on update) the web server's SSL certificate from Let's Encrypt:
$ sudo letsencrypt-renew-certs
After the successful acme-tiny
call, the .letsencrypt/
subfolder in /var/lib/letsencrypt
looks like this:
(root@host) {/var/lib/letsencrypt} # ls -al .letsencrypt/
total 32
drwx------ 2 letsencrypt nogroup 4096 Mar 6 2017 .
drwx--x--- 8 letsencrypt www-data 4096 Mar 1 2017 ..
-rw-r--r-- 1 root root 3247 Mar 1 2017 account-host_example_com.key
-rw-r--r-- 1 root root 1984 Mar 1 2017 letsencryptauthorityx3.pem
-rw-r--r-- 1 root root 1647 Nov 16 2016 lets-encrypt-x3-cross-signed.pem
-rw-r--r-- 1 letsencrypt nogroup 0 May 1 00:00 host_example_com.crt
-rw-r--r-- 1 root root 2480 Mar 1 2017 host_example_com.csr
-rw-r--r-- 1 letsencrypt nogroup 4704 Apr 1 00:17 host_example_com.fullchain.crt
Plumbing it all into the Apache2 Config
First, the webserver needs to know where to find the SSL certificate file and the corresponding key file.
Adapting the basic Apache2 SSL Setup
The key file we earlier copied over into /etc/ssl/private/host_example_com.key
.
The certificate file should now exist in /var/lib/letsencrypt/.letsencrypt
. Personally, I prefer having it symlinked to /etc/ssl/certs/host_example_com.fullchain.crt
:
$ sudo ln -s /var/lib/letsencrypt/.letsencrypt/host_example_com.fullchain.crt /etc/ssl/certs/host_example_com.fullchain.crt
Apache2 needs to be pointed to these two files (the .key
and the .crt
file). Enable the SSL module in Apache2, enable the default SSL site and modify the default SSL config like this:
diff --git a/apache2/sites-available/default-ssl.conf b/apache2/sites-available/default-ssl.conf
index 7e37a9c..4e8784a 100644
--- a/apache2/sites-available/default-ssl.conf
+++ b/apache2/sites-available/default-ssl.conf
@@ -29,8 +29,10 @@
# /usr/share/doc/apache2/README.Debian.gz for more info.
# If both key and certificate are stored in the same file, only the
# SSLCertificateFile directive is needed.
- SSLCertificateFile /etc/ssl/certs/ssl-cert-snakeoil.pem
- SSLCertificateKeyFile /etc/ssl/private/ssl-cert-snakeoil.key
+ #SSLCertificateFile /etc/ssl/certs/ssl-cert-snakeoil.pem
+ #SSLCertificateKeyFile /etc/ssl/private/ssl-cert-snakeoil.key
+ SSLCertificateFile /etc/ssl/certs/host_example_com.fullchain.crt
+ SSLCertificateKeyFile /etc/ssl/private/host_example_com.key
# Server Certificate Chain:
# Point SSLCertificateChainFile at a file containing the
Enable the SSL module:
$ sudo a2enmod ssl
Enable the default-ssl site:
$ sudo a2ensite default-ssl
Redirecting all traffic to https://
In the webserver, we now need to make sure that the URL (folder) http://host.example.com/.well-known/acme-challenge/
is always reachable for the Let's Encrypt signing engine on a non-encrypted connection.
Note 1: Once you have Let's Encrypt deployed, you want all requests coming in on http://
be redirected to https://
.
Note 2: The challenges URL should be reachable without encryption (http://
, rather than https://
). Why? If your certificate upgrade fails some day, and you want to obtain a new certificate, you only can do it over a non-encrypted connection (as your https://
certificate has expired).
This is the configuration snippet that you need to place into all VirtualHost definitions (for non-encrypted access to the webserver, so normally under <VirtualHost <address>:80>
) Watch out! Manual adaptation needed in the third line:
RewriteEngine on
RewriteCond %{REQUEST_URI} !/\.well-known/acme-challenge/.*
RewriteRule /(.*) https://vhost-server-name.example.com/$1 [L,NC,NE]
Enable the rewrite module:
a2enmod rewrite
Then restart Apache2:
invoke-rc.d apache2 restart
Testing in a Web Browser
Now open the URL http://host.example.com
in a web browser and it should switch to https://host.example.com
and the certificate should be trustworthy.
Testing another Let's Encrypt Signing Request
If the browser test above works ok, then try the sudo letsencrypt-renew-certs
run again. It should get you another new certificate just fine (like when you ran it just before). You might want to check time stamps under /var/lib/letsencrypt/.letsencrypt
.
If you do this re-test many many times, the Let's Encrypt / ACME site will block you at some point. Don't worry, this is only temporary and a DDoS protection mechanism.
Getting a new Certificate every Month
Every month I run the CRON job below. The CRONTAB entry (use e.g. VISUAL=mcedit crontab -e
) looks like this:
0 0 1 * * /usr/local/sbin/letsencrypt-renew-certs 2>&1 1>/root/letsencrypt-renewal.log