Gehe zu deutscher Webseite

ViaThinkSoft CodeLib

This article is in:
CodeLibHow-TosApache

Revision: 20 November 2024

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.1. Create the following directories:

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

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

sudo a2enmod macro
sudo a2enmod proxy
sudo a2enmod proxy_http
sudo a2enmod ssl

1.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>

1.4. Activate the configuration file by creating a symlink:

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

1.5. Add OCSP-Stapling to Apache

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


SSLUseStapling          on
SSLStaplingCache "shmcb:logs/ssl_stapling(32768)"

1.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/

1.7. Restarting of Apache2:

sudo service apache2 restart

1.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" )

TOTALRES=0

for subdir in "$DIR"/*/; do
    if [ -f "${subdir}renew.sh" ]; then
        "${subdir}renew.sh"
        if [ $? -ne 0 ]; then
            TOTALRES=1
        fi
        sleep 1
    fi
done

service apache2 restart

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

exit $TOTALRES

1.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

1.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

1.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

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

2.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
...

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

EMAIL="..."

ECCURVE=secp384r1
#RSASIZE=4096

SERVER="https://acme-v02.api.letsencrypt.org/directory"
#SERVER="https://acme-staging-v02.api.letsencrypt.org/directory"

## If WILDCARD_APITOKEN is missing, reverse proxy method will be used, otherwise DNS method.
## Wildcard DNS method requires the scripts wildcard_authenticator.sh and wildcard_cleanup.sh
## which must be prepared for your domain provider
#WILDCARD_APITOKEN=""

2.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

SELF_PATH=$(cd -P -- "$(dirname -- "$0")" && pwd -P)
DIR=$( dirname "$0" )
cd "$DIR"

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

if [ ! -d "old/" ]; then
        mkdir old
fi

. config

# --- Clean up

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

# --- Create private key

if [ "$ECCURVE" != "" ]; then
        openssl ecparam -name "$ECCURVE" -genkey -out 0000_priv.key
else
        openssl genrsa -out 0000_priv.key $RSASIZE
fi
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

if [ "$WILDCARD_APITOKEN" == "" ]; then
        $EX certonly \
                --authenticator standalone \
                --preferred-challenges http-01 --http-01-port 999 \
                --server $SERVER \
                --text \
                --email $EMAIL \
                --agree-tos \
                --must-staple \
                --staple-ocsp \
                --csr 0000_req.csr
        RES=$?
else
        $EX certonly \
                --manual --manual-auth-hook "$SELF_PATH/wildcard_authenticator.sh" --manual-cleanup-hook "$SELF_PATH/wildcard_cleanup.sh" \
                --preferred-challenges dns \
                --server $SERVER \
                --text \
                --email $EMAIL \
                --agree-tos \
                --must-staple \
                --staple-ocsp \
                --csr 0000_req.csr
        RES=$?
fi

if [ -f /var/log/letsencrypt/letsencrypt.log ]; then
        cp /var/log/letsencrypt/letsencrypt.log letsencrypt.log
fi
if [ $RES -ne 0 ]; then
        echo "CERTBOT FAILED WITH EXIT CODE $RES ($DIR)" >&2
        exit 1
fi

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

if [ "$ECCURVE" != "" ]; then
        # Extract the public key from the certificate
        a=$( openssl x509 -in 0000_cert.pem -pubkey -noout 2>/dev/null | openssl ec -pubin -outform der 2>/dev/null | openssl dgst -sha256 )
        # Derive the public key from the private key
        b=$( openssl ec -in 0000_priv.key -pubout -outform der 2>/dev/null | openssl dgst -sha256 )
else
        a=$( openssl x509 -noout -modulus -in 0000_cert.pem | openssl sha256 )
        b=$( openssl rsa -noout -modulus -in 0000_priv.key | openssl sha256 )
fi

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

# --- PKCS#12 erstellen

# TODO: add $PREVDIR to the name
openssl pkcs12 -export -in 0000_cert.pem -inkey 0000_priv.key -certfile 0000_chain.pem -name "Server Certificate" -out 0000_pkcs12.p12 -passout pass:
if [ $? -ne 0 ]
then
        echo "ERROR while PCKS#12 creation!" >&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! PCKS#12 could not be created!" >&2
        exit 1
fi

chmod 600 0000_pkcs12.p12

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

# --- 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 -f 0000_req.csr
rm -f certbot.log 2> /dev/null
rm -f 0001_chain.pem

# --- Delete expired archived certificates

FILES=old/*.p12
for f in $FILES
do
        # TODO: das ist nicht sauber, denn wenn ein anderer fehler im zertifikat vorliegt, dann würde auch $?=1 sein
        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 "$f"
        fi
done

# --- Post create: Restart servers etc.

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

2.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."

2.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-v02" 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!

If Apache does not start, you can try one of these commands to find the error message:

  • apachectl configtest
  • journalctl -u apache2.service
  • systemctl status apache2.service
  • journalctl -xe
Daniel Marschall
ViaThinkSoft Co-Founder