Gehe zu deutscher Webseite

ViaThinkSoft CodeLib

This article is in:
CodeLibHow-TosApache

Revision: 27 April 2019

This example shows, how you can implement a custom automatic which renews Let's Encrypt certificates for your website. The custom automatic may be useful in case you don't trust an 100% automatic and are concerned that complex configurations might break, or in case you just want to have more control about the renewals.
With this custom automatic, it is easy to let your other services (MySQL, FTP, IMAP, SMTP etc) use the same certificates as your HTTP service.

This tutorial requires that you have an already configured Apache installation and have some basic knowledge about SSL. This tutorial especially is for webmasters who want to change to Let's Encrypt or begin using HTTPS.

In our example, we want to save our SSL relevant data in /data/ssl/letsencrypt . All directory names in this tutorial are only examples, of course and should be adapted to your individual machine configuration.

Step 1 (only required once): Installing of Certbot, Apache and the Cronjob

1. Create the following directories:

sudo mkdir /data
sudo mkdir /data/ssl
sudo mkdir /data/ssl/letsencrypt

2. Install and enable the required Apache2 modules by executing following commands:

sudo aptitude update
sudo aptitude install libapache2-mod-macro
sudo a2enmod macro
sudo a2enmod proxy
sudo a2enmod proxy_http
sudo a2enmod ssl

3. Now we are creating some macros in Apache. Please create /etc/apache2/sites-available/000--macros.conf

Attention: The file name contains two hyphens, because "000--macros.conf" must be loaded/sorted before "000-default.conf".

<Macro LetsEncryptProxy>
        <IfModule mod_proxy.c>
                ProxyPass "/.well-known/acme-challenge/" "http://127.0.0.1:999/.well-known/acme-challenge/" retry=1
                ProxyPassReverse "/.well-known/acme-challenge/" "http://127.0.0.1:999/.well-known/acme-challenge/"
                <Location "/.well-known/acme-challenge/">
                        ProxyPreserveHost On
                        Order allow,deny
                        Allow from all
                        Require all granted
                </Location>
        </IfModule>
</Macro>

<Macro LetsEncryptSSL $sitedirname $ssl_log>
        SSLEngine on
        SSLCertificateFile "/data/ssl/letsencrypt/$sitedirname/certificate.pem"
        SSLCertificateKeyFile "/data/ssl/letsencrypt/$sitedirname/private.key"
        SSLCertificateChainFile "/data/ssl/letsencrypt/$sitedirname/intermediate_ca.pem"
        SetEnvIf User-Agent ".*MSIE.*" nokeepalive ssl-unclean-shutdown
        CustomLog "$ssl_log" "%t %h %{SSL_PROTOCOL}x %{SSL_CIPHER}x \"%r\" %b"
</Macro>

4. Activate the configuration file by creating a symlink:

cd /etc/apache2/sites-enabled/
ln -s ../sites-available/000--macros.conf

5. Add OCSP-Stapling to Apache

Edit /etc/apache2/mods-enabled/ssl.conf and add following at the end:


SSLUseStapling          on
SSLStaplingResponderTimeout 5
SSLStaplingReturnResponderErrors off
SSLStaplingCache        shmcb:/var/run/ocsp(128000)

6. Add the following line to each <VirtualHost> block in your website configuration files (/etc/apache2/sites-available/*.conf) :

Use LetsEncryptProxy

In case the domain validation fails in the later procedure, the reason might be a Rewrite-Rule. In this case, you have to add following line to the Rewrite-block:

RewriteCond %{REQUEST_URI} !^/\.well-known/acme-challenge/

7. Restarting of Apache2:

sudo service apache2 restart

8. Create the script /data/ssl/letsencrypt/renew-all.sh and give execution-permissions via chmod +x renew-all.sh . The contents of the file should be:

#!/bin/bash

DIR=$( dirname "$0" )

"$DIR"/website1/renew.sh
"$DIR"/website2/renew.sh
"$DIR"/website3/renew.sh
...

# In case you are using your certificates for other serivces, please un-comment these lines by removing the "#"
#service vsftpd restart
#service postfix restart
#service cyrus-imapd restart

#service icinga2 stop
#service webmin stop
service apache2 stop
service mysql stop

service mysql start
service apache2 start
#service webmin start
#service icinga2 start

9. Create a cronjob for the user root, which renews the certificates each month:

sudo crontab -e

add following line:

0   0   1   *   *    /data/ssl/letsencrypt/renew-all.sh

10. Installing of Certbot:

Please execute following commands:

sudo aptitude update
sudo aptitude install certbot

In case the package "certbot" is not available in your Linux distribution, you can execute following commands:

sudo aptitude update
sudo aptitude install git
cd /data/ssl/letsencrypt/
git clone https://github.com/letsencrypt/letsencrypt
mv letsencrypt _certbot

11. Setup of a Linux user:

groupadd ssl
usermod -a -G ssl www-data
chown -R root:ssl /data/ssl/


Step 2 (perform for each of your websites): Creation of the scripts for your new website:

In this example, we will call the website "website1" with the domains domain1.com and domain2.com

1. Create the directories /data/ssl/letsencrypt/website1/ and /data/ssl/letsencrypt/website1/old/

2. Create /data/ssl/letsencrypt/website1/openssl.cnf with following contents and insert the proper domain name.

[req]
distinguished_name = req_distinguished_name
req_extensions = v3_req
prompt = no

[req_distinguished_name]
CN = www.domain1.com

[v3_req]
keyUsage = keyEncipherment, dataEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt_names
# Extention "Must Staple"
# Remove this line if you want to use the certificate with services that do not support OCSP-Must-Staple (e.g. Postfix)
1.3.6.1.5.5.7.1.24 = DER:30:03:02:01:05

[alt_names]
DNS.1 = domain1.com
DNS.2 = www.domain2.com
DNS.3 = domain2.com
...

3. Create /data/ssl/letsencrypt/website1/config with following contents and include your email address:

EMAIL="..."
RSASIZE=4096
SERVER="https://acme-v02.api.letsencrypt.org/directory"
# Note: staging still uses ACMEv1
#SERVER="https://acme-staging.api.letsencrypt.org/directory"

4. Create /data/ssl/letsencrypt/website1/renew.sh and give it execution permissions with chmod +x renew.sh . It should have following contents:

#!/bin/bash

# --- Initialization

DIR=$( dirname "$0" )
cd "$DIR"

if [ ! -f openssl.cnf ]; then
        echo "Please run the script in the correct directory." >&2
        exit 2
fi

. config

# --- Clean up

rm *_pkcs12.p12 2> /dev/null
rm *_private.key 2> /dev/null
rm *_cert.pem 2> /dev/null
rm *_chain.pem 2> /dev/null
rm *_req.csr 2> /dev/null
rm certbot.log 2> /dev/null

# --- Create private key

openssl genrsa -out 0000_priv.key $RSASIZE
if [ $? -ne 0 ]; then
        echo "FAILED TO CREATE PRIVATE KEY" >&2
        exit 1
fi
chown root:ssl 0000_priv.key
chmod 640 0000_priv.key

# --- Create certificate request

openssl req -new -batch -sha256 \
        -key 0000_priv.key \
        -config openssl.cnf \
        -out 0000_req.csr
if [ $? -ne 0 ]; then
        echo "FAILED TO CREATE CERTIFICATE REQUEST" >&2
        exit 1
fi

# --- Ask server to sign the certificate

if [ -f ../_certbot/certbot-auto ]; then
        EX="../_certbot/certbot-auto"
else
        EX="certbot"
fi

$EX certonly \
        --authenticator standalone \
        --preferred-challenges http-01 --http-01-port 999 \
        --server $SERVER \
        --text \
        --email $EMAIL \
        --must-staple \
        --staple-ocsp \
        --csr 0000_req.csr
if [ $? -ne 0 ]; then
        echo "CERTBOT FAILED" >&2
        exit 1
fi

# --- Security check: check if certificate and private key are matching

a=$( openssl x509 -noout -modulus -in 0000_cert.pem | openssl sha256 )
b=$( openssl rsa -noout -modulus -in 0000_priv.key | openssl sha256 )

if [ "$a" != "$b" ]
then
        echo "Error: Certificate does not match private key!" >&2
        exit 1
fi

# --- Create PKCS#12

openssl pkcs12 -export -in 0000_cert.pem -inkey 0000_priv.key -certfile 0000_chain.pem -out 0000_pkcs12.p12 -passout pass:
if [ $? -ne 0 ]
then
        echo "FEHLER bei PCKS#12-Erstellung!" >&2
        if [ -f 0000_pkcs12.p12 ]
        then
                chmod 600 0000_pkcs12.p12
                rm 0000_pkcs12.p12
        fi
        exit 1
fi

if [ ! -f 0000_pkcs12.p12 ]
then
        echo "Error: Could not create PKCS#12 file!" >&2
        exit 1
fi

chmod 600 0000_pkcs12.p12

# --- Activate certs

# Files created by certbot:
# 0000_cert.pem  = cert.pem (i.e., the server certificate)
# 0000_chain.pem = chain.pem (i.e., the intermediate certificate)
# 0001_chain.pem = fullchain.pem (i.e., a concatenation of cert.pem + chain.pem in one file).

mv -f 0000_pkcs12.p12 "old/$(date +%s).p12"
mv -f 0000_priv.key private.key
mv -f 0000_cert.pem certificate.pem
mv -f 0000_chain.pem intermediate_ca.pem
rm 0000_req.csr
rm certbot.log 2> /dev/null
rm 0001_chain.pem

# --- Additional security check: X509 Lint

if [ -f "../_x509lint/x509lint" ]; then
        ../_x509lint/x509lint certificate.pem
fi

# --- Delete expired archived certificates

FILES=old/*.p12
for f in $FILES
do
        openssl pkcs12 -in "$f" -clcerts -nokeys -passin pass: | openssl x509 -noout -checkend 0 > /dev/null
        if [ $? -eq 1 ]; then
                echo "$f has expired. Deleting."
                rm "$f"
        fi
done

# --- Post create: Restart servers etc.

if [ -f postcreate.sh ]; then
        ./postcreate.sh
fi

5. (Optional step) Create the following script /data/ssl/letsencrypt/website1/recover_cert.sh which can be used in emergency to recover a certificate together with its private key. Give it execution permissions with chmod +x recover_cert.sh and add the following content:

#!/bin/bash

DIR=$( dirname "$0" )
cd "$DIR"

if [ "$1" == "--help" ]; then
        echo "Syntax: $0 <p12file>"
        exit 2
fi

if [ ! -f "$1" ]; then
        echo "ERROR: File '$1' does not exist" >&2
        exit 1
fi

openssl pkcs12 -in "$1" -nocerts -out tmp_priv.key -passin pass: -nodes
if [ $? -ne 0 ]; then
        echo "ERROR recovering the private key" >&2
        rm tmp_priv.key 2> /dev/null
        rm tmp_cert.pem 2> /dev/null
        rm tmp_ca.pem 2> /dev/null
        exit 1
fi

openssl pkcs12 -in "$1" -clcerts -nokeys -out tmp_cert.pem -passin pass:
if [ $? -ne 0 ]; then
        echo "ERROR recovering the certificate" >&2
        rm tmp_priv.key 2> /dev/null
        rm tmp_cert.pem 2> /dev/null
        rm tmp_ca.pem 2> /dev/null
        exit 1
fi

openssl pkcs12 -in "$1" -cacerts -nokeys -out tmp_ca.pem -passin pass:
if [ $? -ne 0 ]; then
        echo "ERROR recovering the intermediate certificate" >&2
        rm tmp_priv.key 2> /dev/null
        rm tmp_cert.pem 2> /dev/null
        rm tmp_ca.pem 2> /dev/null
        exit 1
fi

mv -f tmp_priv.key private.key
if [ $? -ne 0 ]; then
        echo "ERROR moving the private key" >&2
        exit 1
fi

mv -f tmp_cert.pem certificate.pem
if [ $? -ne 0 ]; then
        echo "ERROR moving the certificate" >&2
        exit 1
fi

mv -f tmp_ca.pem intermediate_ca.pem
if [ $? -ne 0 ]; then
        echo "ERROR moving the intermediate certificate" >&2
        exit 1
fi

echo "Certificate $1 recovered."

6. Edit the configurations in /etc/apache2/sites-available/website1.conf .

In case you only have one <VirtualHost> block (with port 80), duplicate the block so you have one block for port 80 (HTTP) and one block for port 443 (HTTPS).
In the HTTPS block, insert following line to activate the Let's Encrypt certificates (in case you have more than one port 443 block, add the line to the other port 443 blocks as well):

Use LetsEncryptSSL website1 /var/log/.../website1/ssl_request.log


Step 3: Testing

Execute /data/ssl/letsencrypt/renew-all.sh the first time (this time only manual) and follow the instructions. Also note if there are any error messages.
Usually, you need to accept the rules once, and you will be asked if you want to be added to the EFF mailing list.

To receive certificates by a Test Certificate Authority first (not trusted in browsers!), change "acme-v02" to "acme-staging" in your config files. This will prevent creation of unnecessary certificates.


Troubleshooting

In case you receive a timeout during domain validation, although your website is reachable from outside, there might be the case that your domain has an IPv6 record (AAAA) but your server does not accept IPv6. Note that the Let's Encrypt bot prefers IPv6 connections if there is an AAAA DNS record!


Tested with

- Debian Stretch (amd64)
- Raspberry Pi 3 "Raspbian" (armv7l)
Daniel Marschall
ViaThinkSoft Co-Founder