Dehydrated was firstly known as letsencrypt.sh but because letsencrypt is a trademark, they decided to rename the project, but keep the excellent features.

Purpose

If you’re already familiar with let’s encrypt and the ACME protocol, you can go directly to the next section on how to use Dehydrated.

Let’s encrypt give you the possibility to automatize the creation and renewal of SSL certificate. Those certificates and keys can be used for anything that uses SSL protocol like to activate https on your website, TLS on your SMTP/IMAP server, etc … everywhere you can use an SSL cert; you can use let’s encrypt.

Let’s Encrypt is an authorised CA which mean all the certificate generated by it are considered verified and will be recognised as it by the different clients (browser, email client, etc …). This is one of the biggest advantages against self-signed certificates, and your users won’t have to bypass a security warning or add a security exception.

Not only the generated certificate are valid, but let’s encrypt is an entirely automatize protocol, there is no need of human interaction to create and renew a certificate, all you need is a client compatible with the ACME protocol, and that were Dehydrated enter.

Dehydrated

Install

Either you take one of the release packages, or you clone the repository. I advise creating a symlink after to /usr/local/sbin/dehydrated it’s easier like this for setting the cronjob after. You also need to have the OpenSSL package installed for dehydrated to work. You also need a web server, Apache or Nginx will do fine.

Configuration

First, create a configuration folder only accessible by root: /etc/dehydrated/ You also need to create /var/www/dehydrated that will contain the challenge files for website verification.

Conf File

Now create a configuration file named config using their template:

Configuration File
########################################################
# This is the main config file for dehydrated          #
#                                                      #
# This file is looked for in the following locations:  #
# $SCRIPTDIR/config (next to this script)              #
# /usr/local/etc/dehydrated/config                     #
# /etc/dehydrated/config                               #
# ${PWD}/config (in current working-directory)         #
#                                                      #
# Default values of this config are in comments        #
########################################################

# Resolve names to addresses of IP version only. (curl)
# supported values: 4, 6
# default: <unset>
#IP_VERSION=

# Path to certificate authority (default: https://acme-v01.api.letsencrypt.org/directory)
#CA="https://acme-v01.api.letsencrypt.org/directory"

# Path to license agreement (default: https://letsencrypt.org/documents/LE-SA-v1.1.1-August-1-2016.pdf)
#LICENSE="https://letsencrypt.org/documents/LE-SA-v1.1.1-August-1-2016.pdf"

# Which challenge should be used? Currently http-01 and dns-01 are supported
#CHALLENGETYPE="http-01"

# Path to a directory containing additional config files, allowing to override
# the defaults found in the main configuration file. Additional config files
# in this directory needs to be named with a '.sh' ending.
# default: <unset>
#CONFIG_D=

# Base directory for account key, generated certificates and list of domains (default: $SCRIPTDIR -- uses config directory if undefined)
#BASEDIR=$SCRIPTDIR

# File containing the list of domains to request certificates for (default: $BASEDIR/domains.txt)
#DOMAINS_TXT="${BASEDIR}/domains.txt"

# Output directory for generated certificates
#CERTDIR="${BASEDIR}/certs"

# Directory for account keys and registration information
#ACCOUNTDIR="${BASEDIR}/accounts"

# Output directory for challenge-tokens to be served by webserver or deployed in HOOK (default: /var/www/dehydrated)
#WELLKNOWN="/var/www/dehydrated"

# Default keysize for private keys (default: 4096)
#KEYSIZE="4096"

# Path to openssl config file (default: <unset> - tries to figure out system default)
#OPENSSL_CNF=

# Program or function called in certain situations
#
# After generating the challenge-response, or after failed challenge (in this case altname is empty)
# Given arguments: clean_challenge|deploy_challenge altname token-filename token-content
#
# After successfully signing certificate
# Given arguments: deploy_cert domain path/to/privkey.pem path/to/cert.pem path/to/fullchain.pem
#
# BASEDIR and WELLKNOWN variables are exported and can be used in an external program
# default: <unset>
#HOOK=

# Chain clean_challenge|deploy_challenge arguments together into one hook call per certificate (default: no)
#HOOK_CHAIN="no"

# Minimum days before expiration to automatically renew certificate (default: 30)
#RENEW_DAYS="30"

# Regenerate private keys instead of just signing new certificates on renewal (default: yes)
#PRIVATE_KEY_RENEW="yes"

# Create an extra private key for rollover (default: no)
#PRIVATE_KEY_ROLLOVER="no"

# Which public key algorithm should be used? Supported: rsa, prime256v1 and secp384r1
#KEY_ALGO=rsa

# E-mail to use during the registration (default: <unset>)
#CONTACT_EMAIL=

# Lockfile location, to prevent concurrent access (default: $BASEDIR/lock)
#LOCKFILE="${BASEDIR}/lock"

# Option to add CSR-flag indicating OCSP stapling to be mandatory (default: no)
#OCSP_MUST_STAPLE="no"
In the configuration file, you need absolutely to set the CONTACT_EMAIL to an email address you own. You should also set the HOOK  variable to HOOK=/etc/dehydrated/hook.sh. We’ll create the script just after; this hook script is called at each step of the certificate renewal/creation process.

Hook file

Now let’s create the hook.sh. I’m providing you with my hook file based on their template.  The idea is to reload your webserver when a new certificate gets deployed, in my case it’s Nginx, if you use any other one, replace Nginx by yours. I also added a line for my Dovecot and Postfix server only to restart when their certificate is generated. Fell free to use this file and modify it for your needs.

Hook File
#!/usr/bin/env bash

deploy_challenge() {
 local DOMAIN="${1}" TOKEN_FILENAME="${2}" TOKEN_VALUE="${3}"

 # This hook is called once for every domain that needs to be
 # validated, including any alternative names you may have listed.
 #
 # Parameters:
 # - DOMAIN
 # The domain name (CN or subject alternative name) being
 # validated.
 # - TOKEN_FILENAME
 # The name of the file containing the token to be served for HTTP
 # validation. Should be served by your web server as
 # /.well-known/acme-challenge/${TOKEN_FILENAME}.
 # - TOKEN_VALUE
 # The token value that needs to be served for validation. For DNS
 # validation, this is what you want to put in the _acme-challenge
 # TXT record. For HTTP validation it is the value that is expected
 # be found in the $TOKEN_FILENAME file.
}

clean_challenge() {
 local DOMAIN="${1}" TOKEN_FILENAME="${2}" TOKEN_VALUE="${3}"

 # This hook is called after attempting to validate each domain,
 # whether or not validation was successful. Here you can delete
 # files or DNS records that are no longer needed.
 #
 # The parameters are the same as for deploy_challenge.
}

deploy_cert() {
 local DOMAIN="${1}" KEYFILE="${2}" CERTFILE="${3}" FULLCHAINFILE="${4}" CHAINFILE="${5}" TIMESTAMP="${6}"

 # This hook is called once for each certificate that has been
 # produced. Here you might, for instance, copy your new certificates
 # to service-specific locations and reload the service.
 #
 # Parameters:
 # - DOMAIN
 # The primary domain name, i.e. the certificate common
 # name (CN).
 # - KEYFILE
 # The path of the file containing the private key.
 # - CERTFILE
 # The path of the file containing the signed certificate.
 # - FULLCHAINFILE
 # The path of the file containing the full certificate chain.
 # - CHAINFILE
 # The path of the file containing the intermediate certificate(s).
 # - TIMESTAMP
 # Timestamp when the specified certificate was created.
 
 #systemctl reload nginx
 #if [ "$DOMAIN" = "smtp.xxx.xxx" ]
 # then
 # systemctl restart postfix dovecot
 #fi
}

unchanged_cert() {
 local DOMAIN="${1}" KEYFILE="${2}" CERTFILE="${3}" FULLCHAINFILE="${4}" CHAINFILE="${5}"

 # This hook is called once for each certificate that is still
 # valid and therefore wasn't reissued.
 #
 # Parameters:
 # - DOMAIN
 # The primary domain name, i.e. the certificate common
 # name (CN).
 # - KEYFILE
 # The path of the file containing the private key.
 # - CERTFILE
 # The path of the file containing the signed certificate.
 # - FULLCHAINFILE
 # The path of the file containing the full certificate chain.
 # - CHAINFILE
 # The path of the file containing the intermediate certificate(s).
}

invalid_challenge() {
 local DOMAIN="${1}" RESPONSE="${2}"

 # This hook is called if the challenge response has failed, so domain
 # owners can be aware and act accordingly.
 #
 # Parameters:
 # - DOMAIN
 # The primary domain name, i.e. the certificate common
 # name (CN).
 # - RESPONSE
 # The response that the verification server returned
}

request_failure() {
 local STATUSCODE="${1}" REASON="${2}" REQTYPE="${3}"

 # This hook is called when a HTTP request fails (e.g., when the ACME
 # server is busy, returns an error, etc). It will be called upon any
 # response code that does not start with '2'. Useful to alert admins
 # about problems with requests.
 #
 # Parameters:
 # - STATUSCODE
 # The HTML status code that originated the error.
 # - REASON
 # The specified reason for the error.
 # - REQTYPE
 # The kind of request that was made (GET, POST...)
}

exit_hook() {
 # This hook is called at the end of a dehydrated command and can be used
 # to do some final (cleanup or other) tasks.

 :
}

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

Domain file

Now in the same directory, create a domains.txt file containing the list of domains you want a certificate for. You need to have already a web server setup with those domains.

You can set multiple domains on the same line; they will be then put on the same certificate. I advise you to use this to keep subdomain and domain together.

Domains.txt Example
[github-contents username=”lukas2511″ repository=”dehydrated” filepath=”docs/examples/domains.txt” language=”text”]

Webserver

WELLKNOWN

With http-01-type verification (default in this script, there is also support for DNS-based verification) Let’s Encrypt (or the ACME-protocol in general) is checking if you are in control of a domain by accessing a verification file on a URL similar tohttp://example.org/.well-known/acme-challenge/m4g1C-t0k3n.
It will do that for any (sub-)domain you want to sign a certificate for.

At the moment you’ll need to have that location available over standard HTTP on port 80 (redirect to HTTPS will work, but the starting point is always HTTP!).

Dehydrated has a config variable called WELLKNOWNwhich corresponds to the directory which should be served under /.well-known/acme-challenge on your domain. So in the above example, the token would have been saved as $WELLKNOWN/m4g1C-t0k3n.

If you only have one docroot on your server, you could easily do something like WELLKNOWN=/var/www/.well-known/acme-challenge for anything else look at the example below.

Example Usage

If you have more than one docroot (or you are using your server as a reverse proxy/load balancer), the simple configuration mentioned above won’t work, but with just a few lines of web server configuration, this can be solved.

An example would be to create a directory /var/www/dehydrated and set WELLKNOWN=/var/www/dehydrated in the scripts config.

You’ll need to configure aliases on your Webserver:

Nginx example config

With Nginx you’ll need to add this to any of your server/VHost config blocks:

server {
  [...]
  location /.well-known/acme-challenge {
    alias /var/www/dehydrated;
  }
  [...]
}

Apache example config

With Apache just add this to your config and it should work in any VHost:

Alias /.well-known/acme-challenge /var/www/dehydrated

<Directory /var/www/dehydrated>
        Options None
        AllowOverride None

        # Apache 2.x
        <IfModule !mod_authz_core.c>
                Order allow,deny
                Allow from all
        </IfModule>

        # Apache 2.4
        <IfModule mod_authz_core.c>
                Require all granted
        </IfModule>
</Directory>

 Lighttpd example config

With Lighttpd just add this to your config, and it should work in any VHost:

modules += "alias"

alias.url += (
 "/.well-known/acme-challenge/" => "/var/www/dehydrated/"
)

DNS Server

Dehydrated can also configure for you (using the hooks) the DNS records of your domain to use the dns-01 type verification.

Tutorial

I’ve covered in another blog post the DNS-01 validation in great details.

dns-01 challenge

This script also supports the new dns-01-type verification. This type of verification requires you to be able to create a specific TXT DNS record for each hostname included in the certificate.

You need a hook script that deploys the challenge to your DNS server!

The hook script (indicated in the config file or the –hook/-k command line argument) gets four arguments: an operation name (clean_challenge, deploy_challenge, or deploy_cert) and some operands for that. For deploy_challenge $2 is the domain name for which the certificate is required, $3 is a “challenge token” (which is not needed for dns-01), and $4 is a token which needs to be inserted in a TXT record for the domain.

Typically, you will need to split the subdomain name in two, the subdomain name and the domain name separately. For example, for “my.example.com”, you’ll need “my” and “example.com” separately. You then have to prefix “_acme-challenge.” before the subdomain name, as in “_acme-challenge.my” and set a TXT record for that on the domain (e.g. “example.com”) which has the value supplied in $4

_acme-challenge IN TXT $4
_acme-challenge.my IN TXT $4

That could be done manually (as most providers don’t have a DNS API), by having your hook script echo $1, $2 and $4 and then wait (read -s -r -e < /dev/tty) – give it a little time to get into their DNS system. Usually, providers give you a box to put “_acme-challenge.my” and the token value in, and a dropdown to choose the record type, TXT.

Or when you do have a DNS API, pass the details accordingly to achieve the same thing.

You can delete the TXT record when called with operation clean_challenge, when $2 is also the domain name.

Here are some examples: Examples for DNS-01 hooks

First Run

Now that you have configured everything, that you have a set your web server (and restarted/reloaded it after the configuration changes). You’re set to run the command for the first time; it will create your account, generate your private key and create all the certificate your needs with their keys.

dehydrated -f -c /etc/dehydrated/config

Certificates

Now you should find a folder /etc/dehydrated/certs/ with a folder for each of your domain set in your domains.txt file.

In each of those folders, you’ll find two important symbolic links that you need to use in all your application that rely on that certificate-key pair.

  1. fullchain.pem :  /etc/dehydrated/certs/example.com/fullchain.pem
  2. privkey.pem :  /etc/dehydrated/certs/example.com/privkey.pem

The first one is your certificate will the different root certificates prepended to it, in other words, the one you need to set for your service. The second one is the private key of the certificate.

Cron

Using your favourite cron editor, add a new cron to be run every week. By default dehydrated will renew the certificates 30 days before their expiration, you can change that in the configuration if you want, keep in mind a let’s encrypt cert has an expiration of 3 months. This cron run it every Sunday at 2:05 AM.

5 2 * * 6 /usr/local/sbin/dehydrated -c -f /etc/dehydrated/config

More info