Getting a wildcard certificate from Let's Encrypt and securing protocol services with TLS

I've documented here how I obtained a wildcard certificate from Let's Encrypt using the ACME-client dehydrated including renewal and deployment of the certificate.

At the time of writing, Transport Layer Security is around for more than 25 years, since 15 years bigger and smaller sites are switching over to using HTTPS instead of HTTP, and since a decade the Let’s Encrypt initiative provides an easy and cost-free way to get signed certificates. At the time of writing secured http traffic makes up around 90% of all web traffic, according to Google, W3 Technology Surveys, and others.

TLS encrypts communication between two entities in a computer network pretty reliably. The most popular case of applicaton is encryption between a web client and a web server on the internet. While the mechanism of setting up a secure communication channel is pretty straight forward, the tricky thing is that you somehow need to verify that you are communicating with the correct counterpart and not with some evil fake copy. For this reason TLS includes the presentation of a digital certificate by the server in order to proof the server’s authenticity.

While there are weaknesses in regard to digital certificates, it can still be considered much more secure communicating via TLS rather than in an unenrypted manner, especially when confidential information like passwords is exchanged.

So I finally equiped my domains with a valid certificate signed by a proper Certificate Authority that is trusted in all relevant web browsers.

Some notes on certificates and how to obtain them from Let’s encrypt

Wildcard and SAN for Domain Validated Certificates

Regarding validation levels, Let’s Encrypt offers Domain Validation (DV) certificates. They do not offer Organization Validation (OV) or Extended Validation (EV).

The certificate usually holds the domain it is issued for in the Subject field. However, for evaluating a domain, this field is not even checked by browsers anymore. The field that holds the domain and further relevant DNS names is the Subject Alternative Names (SAN) field. The DNS names in this field can even be from different domains. So you can include DNS names from e.g. exaample.com and sample.org in one certificate.

A special case is a DNS name beginning with an asterix, like *.example.com. This entry is considered a wildcard domain and covers all possible subdomains / hostnames for the domain example.com. If you use such a construct in your certificate request, the certificate you get issued is commonly called a wildcard certificate and certain rules for issuing may apply.

The pro of having a wildcard certificate:

The con:

Concluding from the above, the easiest way to secure my domains is getting a wildcard certificate from Let’s Encrypt. Let’s Encrypt are issuing the majority of digital certificates used to secure web servers on the internet, mainly because it is easy and cost-free to obtain. More details for pros and cons can be found here.

Validation methods and certificate validity

In order to increase trustworthiness of a certificate, it’s lifetime of validity is relatively short. Regarding certificates from Let’s encrpyt, currently it is 90 days with future plans to shorten the lifespan even further.

In order to initially obtain and later on renew the certificate, you need to pass a validation process.

Before you receive a certificate, Let’s encrypt is validating that you have a rightful claim to the domains you want to get a certificate for. For the validation process you have to prove that you are in control of each domain you include in the certificate. This is done through a challenge. There are two variations of the challange procedure.

In both validation casess Let’s encrypt creates a token and hands it over to you. The cases differ in how you have to offer the token back to Let’s encrypt:

If you want to get a wildcard certificate from Let’s encrypt, you need to pass the ‘dns-01’ challenge.

If you want to keep your certificate valid, at least every 90 days you need to pass a challenge again in order to renew the certificate.

Automating the renewal process and the challange procedure

In order to make the continuous renewal process and the involved passing of a challenge easier for the subscribers, the Internet Security Research Group, which hosts and runs the Let’s encrypt service, introduced the ACME-protocol to automate maintenance of the certificate.

For this purpose there a large number of ACME-clients available.

Setting up an ACME client

In the following I’ve documented how to obtain a wildcard certificate from Let’s Encrypt by using the ACME-client dehydrated including renewal and deployment of the certificate.

As discussed above, in order to get a wildcard certificate you need to pass the dns-01 challenge so your DNS provider should have some kind of API ready for this in order to automate the challenge.

I have registered my domains with Joker.com. They offer an API for the dns-01 challenge, so you can easily install a DNS TXT record as follows:

$ curl -X POST https://svc.joker.com/nic/replace -d \
'username=your-username&password=your-password&zone=your-domain.com&label=_acme-challenge&type=TXT&value=the-TXT-content-to-insert'

The https-post above creates a TXT entry with label _acme-challenge and the provided value. This method can be used by an ACME-client to fulfill the requirements of the challenge for dns-01 validation, i.e. to set above TXT entry to the appropriate value.

Using this method implicitly requires that you use the nameservices provided by Joker.com.

If you are using another domain registrar chances are that the following documentation is still useful. Just be sure to adopt to the API that your registrar is offering.

certbot or dehydrated?

Joker.com proposes to use dehydrated as an ACME-client and provides instructions how to do so as well as a hook.sh and config.sh file to use with dehydrated.

For users of certbot, the ACME-client recommended by Let’s encrypt, there is a setup guide as well in the above instructions.

Installing dehydrated and configuration for dns-01 challenge with Joker.com support

Dehydrated is packaged for all major Linux distributions. For distributions using apt simply run

$ apt install dehydrated

Now dehydrated needs to be configured.

Some notes on directories settings for dehydrated and how they are set in the debian package:

In dehydrated’s example config , also provided in the debian package within the examples directory of the documentation, ${BASEDIR} is not set. It would thus default to ${SCRIPTDIR}. ${SCRIPTDIR} is determined by dehydrated while running and will hold the directory where the dehydrated script is actually located in the filesystem. This is /usr/bin in the Debian install.

It would definitely be bad to hold any configuration files in /usr/bin, but in dehydrated’s default settings a lot of variables like DOMAINS_TXT, CHAINCACHE, CERTDIR, ALPNCERTDIR, ACCOUNTDIR and LOCKFILE use the ${BASEDIR} variable as their initial path and establish their specific directories in ${BASEDIR}.

So in the Debian-provided config file (/etc/dehydrated/config) the following is set:

$ dehydrated --env prints out which config files are read as well as the values of those variables that are exported for the use in other scripts.

Configuration

It’s a good idea to set CONTACT_EMAIL as well so you will get warnings from Let’s encrypt about expiring certificates and other problems.

As already hinted above, I created domains.txt in /etc/dehydrated and filled it in one line with

example.com *.example.com xn-exmple-cua.com *.xn-exmple-cua.com

That covers both domains I have (the latter is an IDN domain) including all first level subdomains. One certificate will be created for the domain and including all further names as SANs (Subject Alternative Names). The certificate name will be the name of the first DNS name in the line, unless you instruct dehydrated to create an alias name for the certificate by appending " > <aliasname>" at the end of the line.

So now dehydrated is instructed to request a multi-domain (SAN) Wildcard Certificate.

Since Debian sets up a conf.d directory in /etc/dehydrated/conf.d I have created a second config file for my domain registrar in there with further settings. I’ve called it joker-overrides.sh. Note that all further configs in the conf.d are read in alphabetical order and all settings done here are overriding settings done in a previous file or the main config file in /etc/dehydrated.

As getting a wildcard certificate from Let’s encrypt requires to pass a dns-01 challange instead of the default http-01 challange, I’ve set CHALLANGETYPE=“dns-01”.

I have also set the variable HOOK="${BASEDIR}/hooks/joker/hook.sh" for the setup of the hook-file provided by Joker.com. The way the Joker.com hook script is written it requires HOOK_CHAIN to be set to “yes”.

As hooks/joker does not exist in the ${BASEDIR} yet, I created the directories in /var/lib/dehydrated.

$ cd /var/lib/dehydrated
$ mkdir -p hooks/joker

Now I fetched the two files hook.sh and config.sh from the Joker.com-site and moved them into the hooks/joker directory.

I have renamed config.sh to joker-credentials.sh as this name better fits the actual content of the file. Within the file you have to fill in your domain and your DynDNS User and DynDNS Password for each of your domains with an entry as below.

*".example.com" | "example.com")
  USERNAME="YOUR-DYNDNS-USER"
  PASSWORD="YOUR-DYNDNS-PASSWORD"
  ZONE="example.com"

dehydrated is calling the hook script for the following functions:

dehydrated checks that the hook script can handle unexpected and undefined functions. Therefore the hook script should call the functions with

HANDLER="$1"; shift
if [[ "${HANDLER}" =~ ^(deploy_challenge|clean_challenge|deploy_cert|unchanged_cert|invalid_challenge|request_failure|exit_hook)$ ]]; then
  "$HANDLER" "$@"
fi

Testing dehydrated’s configuration using Let’s encrypt’s staging environment

At this point my configuraton should be sufficient for a first dry run using the Let’s encrypt test environment.

In order to use the test environment I included CA=“letsencrpyt-test” in the main config file. Note that the staging server has it’s own user management so you need to agree to it’s terms and conditions separately and repeat this for the production server. Hint: Invoking dehydrated without any arguments will print out help information.

$ dehydrated --display-terms

points to the Terms and Conditions of Let’s encrypt as does dehydrated --register

For agreeing to the terms and conditions and creating an account I need to issue

$ dehydrated --register --accept-terms

which leads to the follwing response:

#INFO: Using main config file /etc/dehydrated/config
#INFO: Using additional config file /etc/dehydrated/conf.d/joker-overrides.sh
+ Generating account key...
+ Registering account key with ACME server...
+ Fetching account URL...
+ Done!

Now I have an account and I can issue

$ dehydrated -c

in order to cycle through generating and signing the certificate.

If everything is going well, the output is as follows:

 + Signing domains...
 + Generating private key...
 + Generating signing request...
 + Requesting new certificate order from CA...
 + Received 4 authorizations URLs from the CA
 + Handling authorization for example.com
 + Handling authorization for xn--exmple-cua.com
 + Handling authorization for example.com
 + Handling authorization for xn--exmple-cua.com
 + 4 pending challenge(s)
 + Deploying challenge tokens...
challenge-hook for: example.com
OK: 2 inserted, 0 deleted
challenge-hook for: xn--exmple-cua.com
OK: 2 inserted, 0 deleted
 + Responding to challenge for example.com authorization...
 + Challenge is valid!
 + Responding to challenge for xn--exmple-cua.com authorization...
 + Challenge is valid!
 + Responding to challenge for example.com authorization...
 + Challenge is valid!
 + Responding to challenge for xn--exmple-cua.com authorization...
 + Challenge is valid!
 + Cleaning challenge tokens...
OK: 0 inserted, 2 deleted
OK: 0 inserted, 2 deleted
 + Requesting certificate...
 + Order is processing...
 + Checking certificate...
 + Done!
 + Creating fullchain.pem...
 + Done!

Great! Now I got the following files in my *${CERTDIR}/<domainname> directory:

/var/lib/dehydrated/certs/example.com:

-rw------- 1 root root  603 Nov 21 16:56 cert-1763740614.csr
-rw------- 1 root root 1484 Nov 21 16:57 cert-1763740614.pem
lrwxrwxrwx 1 root root   19 Nov 21 16:57 cert.csr -> cert-1763740614.csr
lrwxrwxrwx 1 root root   19 Nov 21 16:57 cert.pem -> cert-1763740614.pem
-rw------- 1 root root 1660 Nov 21 16:57 chain-1763740614.pem
lrwxrwxrwx 1 root root   20 Nov 21 16:57 chain.pem -> chain-1763740614.pem
-rw------- 1 root root 3144 Nov 21 16:57 fullchain-1763740614.pem
lrwxrwxrwx 1 root root   24 Nov 21 16:57 fullchain.pem -> fullchain-1763740614.pem
-rw------- 1 root root  288 Nov 21 16:56 privkey-1763740614.pem
lrwxrwxrwx 1 root root   22 Nov 21 16:57 privkey.pem -> privkey-1763740614.pem

I’ve got a private key (privatekey.pem), a certificate (cert.pem), the chain certificate (chain.pem), the chain and certificate in one file (fullchain.pem) as well as the certificate signing request (cert.csr).

A look at the certificate files

I can now have a closer look at the files and examine them using openssl.

$ openssl req -in cert.csr -noout -text

shows the contents of the certificate signing request.

$ openssl x509 -in cert.pem -text -noout

shows the contents of the certificate.

With

$ openssl crl2pkcs7 -nocrl -certfile chained.pem | openssl pkcs7 -print_certs -text -noout

I can check the chain certificate. The same commands work for fullchain.pem, that includes my certificate for example.com together with the chain certificate.

Running

$ dehydrated -c

again will produce the following output:

# INFO: Using main config file /etc/dehydrated/config
# INFO: Using additional config file /etc/dehydrated/conf.d/joker-overrides.sh
Processing example.com with alternative names: *.example.com xn--exmple-cua.com *.xn--exmple-cua.com
 + Checking domain name(s) of existing cert... unchanged.
 + Checking expire date of existing cert...
 + Valid till Feb 19 14:59:14 2026 GMT (Longer than 32 days). Skipping renew!
still valid: example.com

So nothing happens before 32 days prior to expiry of the certificate. This means I can safely run dehydrated with option -c via cron every week to make sure that the certificate gets renewed in time.

Deploying the certificate

Now let’s take care about deployment of the certificates for productive use. I took some inspiration from https://www.sput.nl/software/dehydrated/#fndepcrt when configuring the deployment script called deploy_certs.sh.

The script is called from the deploy_cert() function in hook.sh. The current timestamp is passed on. After checking for existence of the necessary files and directories, the script copies the current fullchain.pem and passkey.pem to the correct locations for Apache, Exim, and Dovecot. After adjusting ownership and permission the services are restarted.

After deploy_certs.sh remove-old-certs.sh is called to remove old (and probably soon to be expired) certificates from all directories they were copied to. If you are 100% sure that everything works as intended and only old certificates are removed, you can activated the removal of old certificates in the original dehydrated directory as well. This could also be achieved with the commands dehydrated --cleanup or dehydrated --cleanup-delete

Once the copying is tested and the copied certificates are deleted (remember, they are only staging and not for production), I am now all set to request real certificates.

As a last practice let’s test revocation of the staging certificates

dehydrated --revoke /var/lib/dehydrated/certs/example.com/cert-<timestamp>.pem

When this works the certificate is renamed to cert-<timestamp>.pem-revoked

Getting ready for production

Finally I can switch to the production server:

The line CA=“letsencrpyt-test” in /etc/dehydrated/config can be deleted or changed to CA=“letsencrpyt”.

The final log output is:

# INFO: Using main config file /etc/dehydrated/config
# INFO: Using additional config file /etc/dehydrated/conf.d/joker-overrides.sh
+ Generating account key...
+ Registering account key with ACME server...
+ Fetching account URL...
+ Done!
# INFO: Using main config file /etc/dehydrated/config
# INFO: Using additional config file /etc/dehydrated/conf.d/joker-overrides.sh
Processing example.com with alternative names: *.example.com xn--exmple-cua.com *.xn--exmple-cua.com
 + Signing domains...
 + Generating private key...
 + Generating signing request...
 + Requesting new certificate order from CA...
 + Received 4 authorizations URLs from the CA
 + Handling authorization for example.com
 + Handling authorization for xn--exmple-cua.com
 + Handling authorization for example.com
 + Handling authorization for xn--exmple-cua.com
 + 4 pending challenge(s)
 + Deploying challenge tokens...
 - hooks/joker/hook.sh: deploying challanges for: example.com
OK: 2 inserted, 0 deleted
 - hooks/joker/hook.sh: deploying challanges for: xn--exmple-cua.com
OK: 2 inserted, 0 deleted
 + Responding to challenge for example.com authorization...
 + Challenge is valid!
 + Responding to challenge for xn--exmple-cua.com authorization...
 + Challenge is valid!
 + Responding to challenge for example.com authorization...
 + Challenge is valid!
 + Responding to challenge for xn--exmple-cua.com authorization...
 + Challenge is valid!
 + Cleaning challenge tokens...
 - hooks/joker/hook.sh: cleaning challanges for: example.com
OK: 0 inserted, 2 deleted
 - hooks/joker/hook.sh: cleaning challanges for: xn--exmple-cua.com
OK: 0 inserted, 2 deleted
 + Requesting certificate...
 + Checking certificate...
 + Done!
 + Creating fullchain.pem...
 - hooks/joker/deploy-certs.sh: No current-timestamp file found.
 - hooks/joker/deploy-certs.sh: Certificate copied to Apache directory /etc/apache2/ssl
 - hooks/joker/remove-old-certs.sh: current-timestamp file not found
 - hooks/joker/remove-old-certs.sh: Old certificates not removed

As a final step I have put the following commands in root’s crontab

# m h  dom mon dow   command
00 00 * * 0 /usr/bin/systemd-cat -t dehydrated /usr/bin/dehydrated --cron
15 00 * * 0 /usr/bin/systemd-cat -t dehydrated /usr/bin/dehydrated --cleanup
30 00 * * 0 /usr/bin/systemd-cat -t dehydrated find /var/lib/dehydrated/archive -type f -mtime 300 -delete

so every Sunday dehydrated will check for certificate renewals and clean out old certificates to the archive directory where they get deleted if they are older than 300 days.

From now on I should have a carefree certificate renewal process.

Configuring Apache2 for TLS

Check /etc/apache2/ports.conf. It should contain

<IfModule ssl_module>
	Listen 443
</IfModule>

in sites-available, edit 000-default.conf and add a redirect like this

Redirect permanent / https://example.com/

If you have any other rewrite rules, move them to default-ssl.conf.

Now edit default-ssl.conf and include:

SSLEngine on
SSLCertificateFile /etc/apache2/ssl/fullchain.pem
SSLCertificateKeyFile /etc/apache2/ssl/privkey.pem
SSLCertificateChainFile /etc/apache2/ssl/fullchain.pem

For all other sites that are enabled you need to edit the corresponding file in sites-available. The files need to have a layout like this:

<VirtualHost *:80>
  ServerName subdomain.example.com

  Redirect permanent / https://subdomain.example.com/
</VirtualHost>

<VirtualHost *:443>
  ServerName subdomain.example.com

  # SSL Configuration
  SSLEngine on
  SSLCertificateFile /etc/apache2/ssl/fullchain.pem
  SSLCertificateKeyFile /etc/apache2/ssl/privkey.pem
  SSLCertificateChainFile /etc/apache2/ssl/fullchain.pem

  # Other Apache Configuration

</VirtualHost>

Let’s activate ssl for apache2:

$ a2enmod ssl
$ a2ensite default-ssl
$ systemctl restart apache2

Now all your sites should be proted with TLS.

Setup Exim for TLS

For the Debian split config file the correct macros in 03_exim4-config_tlsoptions in +/etc/exim4/conf.d/main+ need to be set. This can be done in +000_localmacros+ in the same directory. Set the following macros:

MAIN_TLS_ENABLE = yes
MAIN_TLS_CERTIFICATE = /etc/exim4/ssl/fullchain.pem
MAIN_TLS_PRIVATEKEY = /etc/exim4/ssl/privkey.pem

The deploy_certs.sh script is taking care of setting the file ownership to the Debian-exim group.

Further advise is in the README.Debian provided with the exim4 Debian Package.