Tags: #OpenBSD #security #httpd #HTTPS #TLS #X.509 #certificate #authority #ACME
OpenBSD: HTTPD with TLS Support Using ACME
A quick introduction of how to automate Let’s Encrypt’s TLS certificates process using httpd and acme-client in OpenBSD…
General Theory
In order to be generally accepted by the majority of operating systems (OSs), the Transport Layer Security (TLS) X.509 certificates (as per RFC5280) need to be signed by a certification authority (CA) recognised by the particular OS. Because the related process of creation, validation, signing, installation and renewal of certificates is resource hungry, it has been commercialised. However, the Let’s Encrypt CA (founded by Electronic Frontier Foundation, Mozilla Foundation, University of Michigan, Akamai Technologies and Cisco Systems) provides free X.509 certificates for TLS encryption via an automated process.
Automated Certificate Management Environment (ACME) is a challenge-response protocol that was designed in order to verify a particular domain name ownership before the certificate is issued. Using DNS entries, the process involves several requests to a web server on all domains that are covered by the Subject
(Common Name
) and Certificate Subject Alt Name
(within certificate’s Extensions
field). For that purpose, some sort of an ACME client software is needed.
Important Note: There are discussions (e.g. here and here) whether DNS records must be A
records and not CNAME
aliases. Generally, CNAMEs are fine. Reader discretion advised!
OpenBSD comes with necessary software already installed, i.e. httpd
and acme-client
.
HTTPD
The HTTP daemon called httpd
is a HTTP server with TLS and FastCGI support (i.e. a binary protocol for interfacing interactive programs such as PHP). For the purpose of ACME, PHP is not needed.
Due to the fact that in OpenBSD, the httpd
is chrooted in /var/www
, it is necessary to copy some files into it as follows:
$ mkdir -p /var/www/etc/
$ cp /etc/{hosts,localtime,resolv.conf,services} /var/www/etc/
Considering that:
- the domain names
domain.tld
andwww.domain.tld
are:
- registered properly in the DNS,
- pointing to the server where the
httpd
is running, - accessible via the DNS;
- the TLS support is required for both domain names;
the httpd
can be configured in httpd.conf
as follows:
$ vi /etc/httpd.conf
### The default host (EGRESS_IP:80)
server "default" {
listen on * port 80
block return 301 "http://www.domain.tld$REQUEST_URI"
}
### domain.tld:80
server "domain.tld" {
listen on * port 80
block return 301 "http://www.domain.tld$REQUEST_URI"
}
### www.domain.tld:80
server "www.domain.tld" {
listen on * port 80
log style combined
# ACME
location "/.well-known/acme-challenge/*" {
root "/acme"
root strip 2
}
# ROOT
location "/*" {
block return 301 "https://www.domain.tld$REQUEST_URI"
}
}
The httpd.conf
makes sure that HTTP requests (to TCP port 80) made to the web server’s:
- public (
EGRESS_IP
) address are redirected (withHTTP/1.1 301 Moved Permanently
) to http://www.domain.tld (including the URI), domain.tld
domain name are redirected as in the previous step,www.domain.tld
domain name are handled as follows:
- requests to URI location:
/.well-known/acme-challenge/
have document root in the/acme
directory within the chroot (i.e. in/var/www/acme
by default), - requests to URI location:
/.well-known/acme-challenge/
are stripped two path components from the beginning of the request path (i.e..well-known
andacme-challenge
are removed from the URI), - other requests are redirected (with
HTTP/1.1 301 Moved Permanently
) to the future secure https://www.domain.tld (including the URI).
Start (or restart) of the HTTPD is straightforward:
$ rcctl restart httpd
DNS Certification Authority Authorization
The Certification Authority Authorization (CAA) was specified in RFC6844 in 2013. It operates via a new DNS resource record (RR) called CAA (type 257). Before issuing a certificate, CAs are expected to check the DNS record and refuse issuance unless they find themselves on the whitelist. Considering the Let’s Encrypt CA, the DNS RR looks as follows:
domain.tld. CAA 128 issue "letsencrypt.org"
In addition to the issue
directive, two other directives are supported:
issuewild
that restricts issuance of wildcard certificates, andiodef
, which provides support for notification in the cases something goes wrong.
Furthermore, the number 128
is a flags byte with its highest bit set, meaning the directive use is considered critical and must be followed. At a high level, the CAA has a similar purpose to public key pinning (HPKP), but the implementation is entirely different.
After the DNS update is completed, the verification can be accomplished as follows:
$ dig caa domain.tld +short
More info can be found on qualys.com.
New Certificate Using ACME Client
Considering that all DNS records are in place, such as: domain.tld
and www.domain.tld
in IPv4 environment:
$ dig +noall +answer domain.tld
domain.tld. TTL IN A PUBLIC_IP_ADDRESS
$ dig +noall +answer www.domain.tld
www.domain.tld. TTL IN A PUBLIC_IP_ADDRESS
the Automatic Certificate Management Environment (ACME) client software can be configured in the acme-client.conf
file. Separating multiple alternative names
by spaces, the domain
section can be configured as follows:
$ vi /etc/acme-client.conf
#
# $OpenBSD: acme-client.conf,v 1.2 2019/06/07 08:08:30 florian Exp $
#
authority letsencrypt {
api url "https://acme-v02.api.letsencrypt.org/directory"
account key "/etc/acme/letsencrypt-privkey.pem"
}
authority letsencrypt-staging {
api url "https://acme-staging-v02.api.letsencrypt.org/directory"
account key "/etc/acme/letsencrypt-staging-privkey.pem"
}
domain domain.tld {
alternative names { www.domain.tld }
domain key "/etc/ssl/private/domain.tld.key"
domain full chain certificate "/etc/ssl/domain.tld.fullchain.pem"
sign with letsencrypt
}
In order to initialize a new A
ccount and D
omain key, the following needs to be run:
# acme-client -ADv domain.tld
acme-client: /etc/acme/letsencrypt-privkey.pem: generated RSA account key
acme-client: /etc/ssl/private/domain.rld.key: generated RSA domain key
acme-client: https://acme-v02.api.letsencrypt.org/directory: directories
acme-client: acme-v02.api.letsencrypt.org: DNS: 172.65.32.248
acme-client: https://acme-v02.api.letsencrypt.org/acme/new-reg: new-reg
acme-client: https://acme-v02.api.letsencrypt.org/acme/new-authz: req-auth: domain.tld
acme-client: https://acme-v02.api.letsencrypt.org/acme/new-authz: req-auth: www.domain.tld
...
acme-client: https://acme-v02.api.letsencrypt.org/acme/new-cert: certificate
acme-client: http://cert.int-x3.letsencrypt.org/: full chain
acme-client: cert.int-x3.letsencrypt.org: DNS: 104.101.237.20
acme-client: /etc/ssl/domain.tld.crt: created
acme-client: /etc/ssl/domain.tld.fullchain.pem: created
For an automatic certificate refresh, a daily cron task can be created as follows:
$ echo "#\!/bin/sh\nacme-client domain.tld && rcctl reload httpd" >> /etc/daily.local
HTTPD Update to Support HTTPS
Considering that the ACME process has been successful, the certificates are located in:
/etc/ssl/domain.tld.crt
,/etc/ssl/domain.tld.fullchain.pem
,/etc/ssl/private/domain.tld.key
.
Since the httpd.conf
file can include references to other configuration files, it may seem worth to separate the configuration of HTTPS virtual hosts by adding the following lines:
$ vi /etc/httpd.conf
### HTTPS SETUP
include "/etc/httpd.conf.https"
The HTTPS-enabled virtual hosts can be defined as follows:
$ vi /etc/httpd.conf.https
### domain.tld:443
server "domain.tld" {
listen on * tls port 443
log style combined
tls {
certificate "/etc/ssl/domain.tld.fullchain.pem"
key "/etc/ssl/private/domain.tld.key"
ciphers "TLSv1.2:TLSv1.3:!CAMELLIA:!ARIA:!DSS:!ADH:!PSK:!RSA:!ECDHE-RSA-AES128-SHA256:!DHE-RSA-AES256-SHA256:!DHE-RSA-AES128-SHA256"
}
hsts { subdomains }
block return 301 "https://www.domain.tld$REQUEST_URI"
}
### www.domain.tld:443
server "www.domain.tld" {
listen on * tls port 443
log style combined
tls {
certificate "/etc/ssl/domain.tld.fullchain.pem"
key "/etc/ssl/private/domain.tld.key"
ciphers "TLSv1.2:TLSv1.3:!CAMELLIA:!ARIA:!DSS:!ADH:!PSK:!RSA:!ECDHE-RSA-AES128-SHA256:!DHE-RSA-AES256-SHA256:!DHE-RSA-AES128-SHA256"
}
hsts { subdomains }
directory {
index "index.html" # HTTPd supports a single file only; either .html or .php
}
# Last, allow access to the root directory
location "/*" {
root { "/htdocs/www.domain.tld" }
}
}
The HTTPS configuration uses the same logic as in the plain HTTP section. The following keywords require a commentary, though:
-
The
ciphers
option within thetls
options block is inspired by the Transport Layer Security Hardening -
Useful information can be found in the BetterCrypto’s recommendations as well.
-
The
hsts
options block enables HTTP Strict Transport Security (defined in RFC6797), which is a web security policy mechanism designed to protect websites against various attacks (e.g. SSL-stripping and cookie hijacking). Itssubdomains
option signals to the receiving user agent that the host including all sub domains should only be interacted with by using secure HTTPS connections (HSTS hosts).
HTTPD restart is necessary in order to apply the changes:
$ rcctl restart httpd
Verification
Verification can be done in many ways, one of which is to create a simple index.html
file in the specific document root (/htdocs/www.domain.tld
):
$ mkdir /var/www/htdocs/www.domain.tld
$ echo "It works!" > /var/www/htdocs/www.domain.tld/index.html
Opening the https://www.domain.tld in a favourite web browser should work flawlessly as well as the configured redirections.
Certificate Revocation Using ACME Client
Sometimes, it may be necessary to revoke the current certificate due to:
- security reasons, or
- the
Certificate Subject Alt Name
needs to be updated (more domains to be covered by the certificate).
Revocation can be performed as follows:
$ acme-client -rv domain.tld
The alternative names
section in the ACME’s configuration file can be updated (/etc/acme-client.conf
), and the certificate request re-submitted as follows:
$ acme-client -v domain.tld
HTTPD can be updated and restarted in order to apply the changes:
$ rcctl restart httpd
The robots.txt File
The robots.txt
file can be created to filter out allowed User-agents
. Let’s start with a “allow all” setup, which can be filtered later if necessary:
$ echo "User-agent: *\nDisallow:" > /var/www/htdocs/www.domain.tld/robots.txt