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:
- If you have many subdomains (such as www1.example.com, www2.example.com, www3.example.com, etc.) you can use a single wildcard certificate to secure all possible hostnames, even DNS names, that are created after the certificate is issued. This may make sense when you have a large number of subdomains, or your list of subdomains in use is constantly changing.
The con:
- The biggest concern with wildcard certificates is that when one of your subdomain covered by the certificat is compromised, all subdomains may be compromised. In other words, the simplicity of the wildcard can create significant problems should things go wrong.
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:
- for the ‘http-01’ challenge you need to offer the token at a specific URL including your domain name via http protocol served on port 80.
- for the ‘dns-01’ challenge you need to offer the token via a TXT record for your domain.
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:
- ${BASEDIR} is set to /var/lib/dehydrated
- WELLKNOWN is set do ${BASEDIR}/acme-challenge
- DOMAINS_TXT is set to /etc/dehydrated/domains.txt
$ 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:
- deploy_challange() - called once for every domain validated, should deploy the token value to the right place
- clean_challange() - called after validation, token values can be removed
- sync_cert() - called after certificate file creation but before creating symlinks. Here is a chance to sync the files, if needed,
- deploy_cert() -called after certificate production. Certificate can be copied to specific location and services can be restarted.
- deploy_oscp() - called after OCSP stapling file has been received. File contains infos about the certificates status, i.e. if valid or revoked. Here is a chance to copy that file.
- unchanged_cert() - called for a certificate that is still valid and was therefore not reissued.
- invalid_challange() - called when challenge response has failed
- request_failure() - called when http request to ACME server fails
- generate_csr() - called before any certificate signing operation takes place. Can be used for e.g. handing over signing to third party tools.
- start_hook() - called before the cron command. Initial tasks can be done, e.g. starting a webserver.
- exit_hook() - called at the end of the cron command. Used for cleanup etc.
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.