2016-05-31 letsencrypt.sh instead

Update: 2019-10-20 Dehydrated and Let's Encrypt.


I’ve been using the official Let’s Encrypt client, now Certbot on GitHub, using the webroot plugin, but then I saw Andreas Gohr’s blog post Simple Let's Encrypt on Debian/Apache, based on the German post letsencrypt.sh, based on Lukas Schauer’s letsencrypt.sh on GitHub.

Setting it up required a bit of fiddling.

I created the so-called BASEDIR /etc/letsencrypt.sh, with an empty domains.txt file, the suggested hook.sh and a config (no longer config.sh!) as follows:

hook.sh:

#!/bin/bash

if [ ${1} == "deploy_cert" ]; then
    echo " + Hook: Restarting Apache..."
    service apache2 reload
else
    echo " + Hook: Nothing to do..."
fi

config:

BASEDIR="/etc/letsencrypt.sh/"
WELLKNOWN="/var/www/letsencrypt.sh/"
PRIVATE_KEY="${BASEDIR}/private_key.pem"
HOOK="${BASEDIR}/hook.sh"
CONTACT_EMAIL="kensanata@gmail.com"

I also created /var/www/letsencrypt.sh/. These are the permissions and ownership I’m looking for:

drwxr-xr-x 2 www-data www-data 4096 May 31 12:50 letsencrypt.sh

I followed the Import from official letsencrypt client instructions. In the bash file, I had to set BASEDIR="/etc/letsencrypt.sh" or it wouldn’t work, and I had to run it as root. This created the certs subdirectory in my BASEDIR with all the certificates and it populated the domains.txt file. I had to look over the list of domains because five out of six domains didn’t have the www subdomain I wanted. Probably because the very first time I made those calls I had forgotten about them.

The perl file just required me to cpanm --sudo Crypt::OpenSSL::RSA and cpanm --sudo Crypt::OpenSSL::Bignum. The result of calling it I piped into private_key.pem in my BASEDIR.

For all my sites, I replaced all the .pem locations.

Old:

SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/alexschroeder.ch/cert.pem
SSLCertificateKeyFile /etc/letsencrypt/live/alexschroeder.ch/privkey.pem
SSLCertificateChainFile /etc/letsencrypt/live/alexschroeder.ch/chain.pem
SSLVerifyClient None

New:

SSLEngine on
SSLCertificateFile /etc/letsencrypt.sh/certs/alexschroeder.ch/cert.pem
SSLCertificateKeyFile /etc/letsencrypt.sh/certs/alexschroeder.ch/privkey.pem
SSLCertificateChainFile /etc/letsencrypt.sh/certs/alexschroeder.ch/chain.pem
SSLVerifyClient None

Do this for all sites in sites-enabled.

Don’t forget to place the following in /etc/apache2/conf.d/letsencrypt:

Alias /.well-known/acme-challenge /var/www/letsencrypt.sh/

<Directory /var/www/letsencrypt.sh/>
        Options None
        AllowOverride None
        Order allow,deny
        Allow from all
</Directory>

When I ran ~/src/letsencrypt.sh/letsencrypt.sh -c the first time, I got an error, presumably because I had forgotten to restart Apache and so the challenge wasn’t being served:

alex@kallobombus:~/src/letsencrypt.sh$ sudo ~/src/letsencrypt.sh/letsencrypt.sh -c
1. INFO: Using main config file /etc/letsencrypt.sh/config
Processing alexschroeder.ch with alternative names: www.alexschroeder.ch
 + Checking domain name(s) of existing cert... changed!
 + Domain name(s) are not matching!
 + Names in old certificate: alexschroeder.ch
 + Configured names: alexschroeder.ch www.alexschroeder.ch
 + Forcing renew.
 + Checking expire date of existing cert...
 + Valid till Jun 10 09:16:00 2016 GMT (Less than 30 days). Renewing!
 + Signing domains...
 + Generating private key...
 + Generating signing request...
 + Requesting challenge for alexschroeder.ch...
 + Requesting challenge for www.alexschroeder.ch...
 + Hook: Nothing to do...
 + Responding to challenge for alexschroeder.ch...
 + Hook: Nothing to do...
ERROR: Challenge is invalid! (returned: invalid) (result: {
...
})

When I tried to sudo service apache2 reload I got an error in my config. The problem was that my config was referring to the chain.pem but the import script hadn’t copied it over!

I was forced to run something like the following: cd /etc/letsencrypt.sh/certs; for d in *; do echo $d; cp -i /etc/letsencrypt/live/$d/chain.pem /etc/letsencrypt.sh/certs/$d/chain-1464687792.pem; cd /etc/letsencrypt.sh/certs/$d; ln -s chain-1464687792.pem chain.pem; done (I had picked the timestamp by looking at the other files).

Finally reloading Apache worked.

Just to be sure, I created a little text file as /var/www/letsencrypt.sh/test and tried to read it using https://alexschroeder.ch/.well-known/acme-challenge/test. That seemed to work.

And now it worked!

alex@kallobombus:~$ sudo ~/src/letsencrypt.sh/letsencrypt.sh -c
1. INFO: Using main config file /etc/letsencrypt.sh/config
Processing alexschroeder.ch with alternative names: www.alexschroeder.ch
 + Checking domain name(s) of existing cert... changed!
 + Domain name(s) are not matching!
 + Names in old certificate: alexschroeder.ch
 + Configured names: alexschroeder.ch www.alexschroeder.ch
 + Forcing renew.
 + Checking expire date of existing cert...
 + Valid till Jun 10 09:16:00 2016 GMT (Less than 30 days). Renewing!
 + Signing domains...
 + Generating private key...
 + Generating signing request...
 + Requesting challenge for alexschroeder.ch...
 + Requesting challenge for www.alexschroeder.ch...
 + Hook: Nothing to do...
 + Responding to challenge for alexschroeder.ch...
 + Hook: Nothing to do...
 + Challenge is valid!
 + Hook: Nothing to do...
 + Responding to challenge for www.alexschroeder.ch...
 + Hook: Nothing to do...
 + Challenge is valid!
 + Requesting certificate...
 + Checking certificate...
 + Done!
 + Creating fullchain.pem...
 + Hook: Restarting Apache...
[ ok ] Reloading web server config: apache2.
 + Done!
Processing arabisch-lernen.org with alternative names: www.arabisch-lernen.org
 + Checking domain name(s) of existing cert... unchanged.
 + Checking expire date of existing cert...
 + Valid till Aug  6 17:27:00 2016 GMT (Longer than 30 days). Skipping renew!
 + Hook: Nothing to do...
Processing campaignwiki.org with alternative names: www.campaignwiki.org
 + Checking domain name(s) of existing cert... changed!
 + Domain name(s) are not matching!
 + Names in old certificate: campaignwiki.org
 + Configured names: campaignwiki.org www.campaignwiki.org
 + Forcing renew.
 + Checking expire date of existing cert...
 + Valid till Jun 10 09:17:00 2016 GMT (Less than 30 days). Renewing!
 + Signing domains...
 + Generating private key...
 + Generating signing request...
 + Requesting challenge for campaignwiki.org...
 + Requesting challenge for www.campaignwiki.org...
 + Hook: Nothing to do...
 + Responding to challenge for campaignwiki.org...
 + Hook: Nothing to do...
 + Challenge is valid!
 + Hook: Nothing to do...
 + Responding to challenge for www.campaignwiki.org...
 + Hook: Nothing to do...
 + Challenge is valid!
 + Requesting certificate...
 + Checking certificate...
 + Done!
 + Creating fullchain.pem...
 + Hook: Restarting Apache...
[ ok ] Reloading web server config: apache2.
 + Done!
Processing communitywiki.org with alternative names: www.communitywiki.org
 + Checking domain name(s) of existing cert... changed!
 + Domain name(s) are not matching!
 + Names in old certificate: communitywiki.org
 + Configured names: communitywiki.org www.communitywiki.org
 + Forcing renew.
 + Checking expire date of existing cert...
 + Valid till Jun 10 09:17:00 2016 GMT (Less than 30 days). Renewing!
 + Signing domains...
 + Generating private key...
 + Generating signing request...
 + Requesting challenge for communitywiki.org...
 + Requesting challenge for www.communitywiki.org...
 + Hook: Nothing to do...
 + Responding to challenge for communitywiki.org...
 + Hook: Nothing to do...
 + Challenge is valid!
 + Hook: Nothing to do...
 + Responding to challenge for www.communitywiki.org...
 + Hook: Nothing to do...
 + Challenge is valid!
 + Requesting certificate...
 + Checking certificate...
 + Done!
 + Creating fullchain.pem...
 + Hook: Restarting Apache...
[ ok ] Reloading web server config: apache2.
 + Done!
Processing korero.org with alternative names: www.korero.org
 + Checking domain name(s) of existing cert... changed!
 + Domain name(s) are not matching!
 + Names in old certificate: korero.org
 + Configured names: korero.org www.korero.org
 + Forcing renew.
 + Checking expire date of existing cert...
 + Valid till Jun 10 09:18:00 2016 GMT (Less than 30 days). Renewing!
 + Signing domains...
 + Generating private key...
 + Generating signing request...
 + Requesting challenge for korero.org...
 + Requesting challenge for www.korero.org...
 + Hook: Nothing to do...
 + Responding to challenge for korero.org...
 + Hook: Nothing to do...
 + Challenge is valid!
 + Hook: Nothing to do...
 + Responding to challenge for www.korero.org...
 + Hook: Nothing to do...
 + Challenge is valid!
 + Requesting certificate...
 + Checking certificate...
 + Done!
 + Creating fullchain.pem...
 + Hook: Restarting Apache...
[ ok ] Reloading web server config: apache2.
 + Done!
Processing oddmuse.org with alternative names: www.oddmuse.org
 + Checking domain name(s) of existing cert... changed!
 + Domain name(s) are not matching!
 + Names in old certificate: oddmuse.org
 + Configured names: oddmuse.org www.oddmuse.org
 + Forcing renew.
 + Checking expire date of existing cert...
 + Valid till Jun 10 09:18:00 2016 GMT (Less than 30 days). Renewing!
 + Signing domains...
 + Generating private key...
 + Generating signing request...
 + Requesting challenge for oddmuse.org...
 + Requesting challenge for www.oddmuse.org...
 + Hook: Nothing to do...
 + Responding to challenge for oddmuse.org...
 + Hook: Nothing to do...
 + Challenge is valid!
 + Hook: Nothing to do...
 + Responding to challenge for www.oddmuse.org...
 + Hook: Nothing to do...
 + Challenge is valid!
 + Requesting certificate...
 + Checking certificate...
 + Done!
 + Creating fullchain.pem...
 + Hook: Restarting Apache...
[ ok ] Reloading web server config: apache2.
 + Done!
Processing orientalisch.info with alternative names: www.orientalisch.info
 + Checking domain name(s) of existing cert... changed!
 + Domain name(s) are not matching!
 + Names in old certificate: orientalisch.info
 + Configured names: orientalisch.info www.orientalisch.info
 + Forcing renew.
 + Checking expire date of existing cert...
 + Valid till Jun 10 09:18:00 2016 GMT (Less than 30 days). Renewing!
 + Signing domains...
 + Generating private key...
 + Generating signing request...
 + Requesting challenge for orientalisch.info...
 + Requesting challenge for www.orientalisch.info...
 + Hook: Nothing to do...
 + Responding to challenge for orientalisch.info...
 + Hook: Nothing to do...
 + Challenge is valid!
 + Hook: Nothing to do...
 + Responding to challenge for www.orientalisch.info...
 + Hook: Nothing to do...
 + Challenge is valid!
 + Requesting certificate...
 + Checking certificate...
 + Done!
 + Creating fullchain.pem...
 + Hook: Restarting Apache...
[ ok ] Reloading web server config: apache2.
 + Done!

Note the message saying that Apache is reloading.

And to verify it, I use curl and look for the line saying “expire date”:

alex@kallobombus:~$ curl --verbose --head --silent https://alexschroeder.ch/
* About to connect() to alexschroeder.ch port 443 (#0)
*   Trying 127.0.0.1...
* connected
* Connected to alexschroeder.ch (127.0.0.1) port 443 (#0)
* successfully set certificate verify locations:
*   CAfile: none
  CApath: /etc/ssl/certs
* SSLv3, TLS handshake, Client hello (1):
* SSLv3, TLS handshake, Server hello (2):
* SSLv3, TLS handshake, CERT (11):
* SSLv3, TLS handshake, Server key exchange (12):
* SSLv3, TLS handshake, Server finished (14):
* SSLv3, TLS handshake, Client key exchange (16):
* SSLv3, TLS change cipher, Client hello (1):
* SSLv3, TLS handshake, Finished (20):
* SSLv3, TLS change cipher, Client hello (1):
* SSLv3, TLS handshake, Finished (20):
* SSL connection using ECDHE-RSA-AES128-GCM-SHA256
* Server certificate:
*        subject: CN=alexschroeder.ch
*        start date: 2016-05-31 09:32:00 GMT
*        expire date: 2016-08-29 09:32:00 GMT
*        subjectAltName: alexschroeder.ch matched
*        issuer: C=US; O=Let's Encrypt; CN=Let's Encrypt Authority X3
*        SSL certificate verify ok.
...

Yay!

Now all I have to do is install this.

I copied /home/alex/src/letsencrypt.sh/letsencrypt.sh to /etc/letsencrypt.sh/letsencrypt.sh and created the file /etc/cron.weekly/letsencrypt.sh which calls it:

#!/bin/sh
exec /etc/letsencrypt.sh/letsencrypt.sh -c

I made sure to grant it +x permissions, too. 🙂

And now, just to test things, let’s run the cron job:

alex@kallobombus:~$ sudo /etc/cron.weekly/letsencrypt.sh
1. INFO: Using main config file /etc/letsencrypt.sh/config
Processing alexschroeder.ch with alternative names: www.alexschroeder.ch
 + Checking domain name(s) of existing cert... unchanged.
 + Checking expire date of existing cert...
 + Valid till Aug 29 09:32:00 2016 GMT (Longer than 30 days). Skipping renew!
 + Hook: Nothing to do...
Processing arabisch-lernen.org with alternative names: www.arabisch-lernen.org
 + Checking domain name(s) of existing cert... unchanged.
 + Checking expire date of existing cert...
 + Valid till Aug  6 17:27:00 2016 GMT (Longer than 30 days). Skipping renew!
 + Hook: Nothing to do...
Processing campaignwiki.org with alternative names: www.campaignwiki.org
 + Checking domain name(s) of existing cert... unchanged.
 + Checking expire date of existing cert...
 + Valid till Aug 29 09:32:00 2016 GMT (Longer than 30 days). Skipping renew!
 + Hook: Nothing to do...
Processing communitywiki.org with alternative names: www.communitywiki.org
 + Checking domain name(s) of existing cert... unchanged.
 + Checking expire date of existing cert...
 + Valid till Aug 29 09:32:00 2016 GMT (Longer than 30 days). Skipping renew!
 + Hook: Nothing to do...
Processing korero.org with alternative names: www.korero.org
 + Checking domain name(s) of existing cert... unchanged.
 + Checking expire date of existing cert...
 + Valid till Aug 29 09:32:00 2016 GMT (Longer than 30 days). Skipping renew!
 + Hook: Nothing to do...
Processing oddmuse.org with alternative names: www.oddmuse.org
 + Checking domain name(s) of existing cert... unchanged.
 + Checking expire date of existing cert...
 + Valid till Aug 29 09:33:00 2016 GMT (Longer than 30 days). Skipping renew!
 + Hook: Nothing to do...
Processing orientalisch.info with alternative names: www.orientalisch.info
 + Checking domain name(s) of existing cert... unchanged.
 + Checking expire date of existing cert...
 + Valid till Aug 29 09:33:00 2016 GMT (Longer than 30 days). Skipping renew!
 + Hook: Nothing to do...

Very nice.

Tags:

Comments

I really should read the draft for the ACME protocol (via their GitHub).

– Alex Schroeder 2016-05-31 13:56 UTC


I did the same for Emacs Wiki. Since I’m using nginx as the front-end, there are small differences. Here’s the excerpt from /etc/nginx/sites-enabled/emacswiki:

server {
        listen 443 ssl;
        server_name www.emacswiki.org po6.ferrier.me.uk;

        # https://mozilla.github.io/server-side-tls/ssl-config-generator/?server=nginx-1.4.6&openssl=1.0.1f&hsts=yes&profile=modern
        # using Nginx, Modern, Server Version 1.4.6 and OpenSSL Version 1.0.1f with HSTS
        ssl_certificate     /etc/letsencrypt.sh/certs/www.emacswiki.org/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt.sh/certs/www.emacswiki.org/privkey.pem;
        ssl_session_timeout 1d;
        ssl_session_cache shared:SSL:50m;

        # Diffie-Hellman parameter for DHE ciphersuites, recommended 2048 bits
        # using openssl dhparam -out dhparam.pem 2048
        ssl_dhparam         /etc/nginx/dhparam.pem;

        # modern configuration. tweak to your needs.
        ssl_protocols TLSv1.1 TLSv1.2;
        ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!3DES:!MD5:!PSK';
        ssl_prefer_server_ciphers on;

        # HSTS (ngx_http_headers_module is required) (15768000 seconds = 6 months)
        # are we ready?
        # add_header Strict-Transport-Security max-age=15768000;

        # OCSP Stapling ---
        # fetch OCSP records from URL in ssl_certificate and cache them
        ssl_stapling on;
        ssl_stapling_verify on;

        # verify chain of trust of OCSP response using Root CA and Intermediate certs
        # https://community.letsencrypt.org/t/will-does-the-letsencrypt-client-create-a-cert-chain-usable-with-ocsp-stapling/2072/15
        ssl_trusted_certificate /etc/letsencrypt/live/www.emacswiki.org/chain.pem;

        ...

        location /.well-known/acme-challenge {
                 alias /var/www/letsencrypt.sh/;
        }
}

And /etc/letsencrypt.sh/hook.sh is also different, of course:

#!/bin/bash
if [ ${1} == "deploy_cert" ]; then
    echo " + Hook: Reloading Nginx..."
    service nginx reload
else
    echo " + Hook: Nothing to do..."
fi

And the output:

1. INFO: Using main config file /etc/letsencrypt.sh/config
Processing www.emacswiki.org with alternative names: emacswiki.org
 + Checking domain name(s) of existing cert... unchanged.
 + Checking expire date of existing cert...
 + Valid till Aug 14 06:27:00 2016 GMT (Longer than 30 days). Skipping renew!
 + Hook: Nothing to do...

– Alex Schroeder 2016-06-10 09:16 UTC


Oh, the project renamed itself to dehydrated.

– Alex Schroeder 2017-03-14 06:21 UTC


Please make sure you contribute only your own work, or work licensed under the GNU Free Documentation License. Note: in order to facilitate peer review and fight vandalism, we will store your IP number for a number of days. See Privacy Policy for more information. See Info for text formatting rules. You can edit the comment page if you need to fix typos. You can subscribe to new comments by email without leaving a comment.

To save this page you must answer this question:

Please say HELLO.

Referrers: Blog articles and HowTos