OpenBSD: HTTPS Web Server With Let's Encrypt


Please note: this version of the article applies to an outdated version of OpenBSD. The current version can be found here.

This article explains how to set up a secure Web server that responds only to HTTPS requests using the default software of the OpenBSD operating system version 6.4, i.e. httpd(8), and acme-client(8). Please mind however, that only to HTTPS is not entirely correct: the Web server has to answer to plain HTTP requests for the sake of renewal of X.509 certificates issued by Let's Encrypt, but all the rest of requests will be served using HTTPS.

This article discusses HTTPS in particular, however certificates from the Let's Encrypt project have ‘TLS Web Server Authentication’ and ‘TLS Web Client Authentication’ as their Extended Key Usage, hence, they can be used for securing any application that communicates over the TLS protocol, i.e. not necessarily a Web server. I can testify that I successfully use such a certificate with an OpenSMTPD server for securing incoming mail of a domain that I manage.

Before You Begin

Your host must meet certain conditions during the enrollment (i.e. the very first time when the acme-client(1) is run for a particular domain) and renewals (i.e subsequent periodic executions of acme-client(1) for the purpose of validation and re-issuing) of certificates.

DNS

Every host name that is listed in the alternative names clause of acme-client.conf(5) must resolve to an IP address using the DNS protocol.

HTTP server

Every host name that is listed in the alternative names clause of acme-client.conf(5) must be reachable using the HTTP (at the time of this writing, the acme-client(1) supports only the HTTP Challenge, a.k.a http-01, as defined by the ACME Working Group). To satisfy that, you must configure, enable, and verify the proper operation of the HTTP server— prior to enrollment.

Important: when acme-client(1) requests the enrollment, the Let's Encrypt's server first creates an unauthorised account for the domain being enrolled, then sends it an HTTP Challenge, and then authorizes the account—if the challenge was properly answered by the domain (i.e. by the HTTP server). HTTP server's failure to answer the challenge during the enrollment will bring the domain's account (at the Let's Encrypt's server) into an intermediate state that will prevent subsequent attempts of enrollment. Therefore, do validate that the HTTP server responds prior to launching acme-client(1) for the first time. Use another computer, preferably from another network, to ensure that the DNS and the HTTP server are configured properly and the external traffic to this HTTP server is not blocked by a firewall. Do not proceed unless you are absolutely sure that everything is in order.

Configuration

For the purpose of explanation, I assume below that you have OpenBSD 6.4 installed on a machine that is known both as www.nxdomain.net and mx.nxdomain.net. I further assume that you intend to apply minimal changes to the default configuration of the system.

The very basic httpd.conf(5) that supports running the acme-client(1) can be created as follows

cat <<EOF | doas tee /etc/httpd.conf
server "default" {
    listen on egress port 80
    root "/htdocs"
    directory no index
    location "/.well-known/acme-challenge/*" {
        root "/acme"
        request strip 2
    }
}
EOF

Now start httpd(8):

doas rcctl -f start httpd
(Here I use the flag -f to force starting httpd(8) without enabling it first, i.e. httpd(8) will not be started on the next boot. To me this is a matter of precaution because I perceive this configuration as temporary.)

Edit acme-client.conf(5) as follows:

doas vi /etc/acme-client.conf
Using the editor, you should make three changes:
  1. Globally replace all appearances of example.org with the name of your domain (in our example, nxdomain.net).
  2. Completely uncomment the clause domain nxdomain.net.
  3. In the same clause, locate the sub-clause alternative names and replace its contents completely with the list of host names that you want to have as the value of the attribute X509v3 Subject Alternative Name of the certificate that you age going to apply for with Let's Encrypt.

Your version of acme-client.conf(5) should be similar to the following:

authority letsencrypt {
        api url "https://acme-v01.api.letsencrypt.org/directory"
        account key "/etc/acme/letsencrypt-privkey.pem"
}

authority letsencrypt-staging {
        api url "https://acme-staging.api.letsencrypt.org/directory"
        account key "/etc/acme/letsencrypt-staging-privkey.pem"
}

domain nxdomain.net {
       alternative names { www.nxdomain.net mx.nxdomain.net }
       domain key "/etc/ssl/private/nxdomain.net.key"
       domain certificate "/etc/ssl/nxdomain.net.crt"
       domain full chain certificate "/etc/ssl/nxdomain.net.fullchain.pem"
       sign with letsencrypt
}

Verify the Operation of HTTP

As explained above, you must verify that all hostnames that are listed in the alternative names sub-clause of acme-client.conf are reachable through the HTTP.

To this end, create a small HTML file called test.html under /var/www/acme:

hostname | doas tee /var/www/acme/test.html
Having such a file does not pose a security risk and you can remove this file later.

Use another computer, preferably from another network, to verify the operation of HTTP:

for name in www.nxdomain.net mx.nxdomain.net; do
    if ! curl -f "http://${name}/.well-known/acme-challenge/test.html"; then
        echo "${name} is not reachable. Aborting."
        break
    fi
done
If execution of this script produces anything else but the contents of /var/www/acme/test.html as many times as there are hostnames specified, please stop, investigate the cause of the error, correct it, and run the script again.

Do not proceed any further until this test succeeds completely, for every host name listed.

If all goes well, you can remove the test file:

doas rm /var/www/acme/test.html

Enrollment

There is no much to it, run the following:

doas acme-client -vAD nxdomain.net
Carefully examine the output (there will be a considerable amount of it): the command must succeed without any errors.

Configuration of the HTTPS Server

Overwrite httpd.conf(5) as follows:

cat <<EOF | doas tee /etc/httpd.conf
server "default" {
    listen on egress port 80
    root "/htdocs"
    directory no index
    location "/.well-known/acme-challenge/*" {
        root "/acme"
        request strip 2
    }
    location "/*" {
        block return 301 "https://\$HTTP_HOST\$REQUEST_URI"
    }
}

server "secure" {
    listen on egress tls port 443 
    tls certificate "/etc/ssl/nxdomain.net.fullchain.pem"
    tls key "/etc/ssl/private/nxdomain.net.key"
    hsts
    root "/htdocs"
    directory index index.html
}
EOF
Please note that location clauses are matched in the order of their appearance and the first clause that matches ‘wins’, i.e. the remaining clauses are not processed. The first location clause of the HTTP server permits ACME HTTP challenges to be processed, while the second redirects all other requests to HTTPS.

Enable httpd(8) and re-start it:

doas rcctl enable httpd
doas rcctl restart httpd

Prepare for Renewals

Append daily invocation of acme-client to daily(8):

cat <<EOF | doas tee -a /etc/daily.local
acme-client nxdomain.net && rcctl reload httpd
EOF

When In Trouble

Should you decide to start from the scratch (as the result of an error or because you wish to modify the list of alternative names in acme-client.conf(5)), you must remove all the keys that are known to Let's Encrypt (and, for the sake of correctness, local secret keys as well):

doas rm -f /etc/acme/letsencrypt-privkey.pem \
    /etc/ssl/nxdomain.net.crt \
    /etc/ssl/nxdomain.net.fullchain.pem \
    /etc/ssl/private/nxdomain.net.key

Vadim Penzin, March 10th, 2018


I hereby place this article into the public domain.
I publish this information in the hope that it will be useful, but without ANY WARRANTY.
You are responsible for any and all consequences that may arise as the result of using this information.