TLS for OpenHTTPd

This guide shows you how to enable TLS for OpenHTTPd. It assumes you have already set up plaintext OpenHTTPd listening on port 80, and you have successfully requested TLS certs using acme-client.

Configuring

In the plaintext OpenHTTPd guide, we used /etc/examples/httpd.conf as a template for httpd.conf(5), with two sections commented out:

server "example.com" {
       listen on * port 80
       location "/.well-known/acme-challenge/*" {
               root "/acme"
               request strip 2
       }
#       location * {
#                 block return 302 "https://$HTTP_HOST$REQUEST_URI"
#       }
}

#server "example.com" {
#        listen on * tls port 443
#        tls {
#                certificate "/etc/ssl/example.com.fullchain.pem"
#                key "/etc/ssl/private/example.com.key"
#        }
#        location "/pub/*" {
#                directory auto index
#        }
#        location "/.well-known/acme-challenge/*" {
#                root "/acme"
#                request strip 2
#        }
#}

NOTE: You must replace example.com with your own domain

We commented out these two sections because we did not yet request TLS certs. Now that we have certs from acme-client, we will uncomment the second block.

TLS Block Explained

Below, we have uncommented the block (and made one significant change). We will provide a line-by-line description of the TLS block:

server "example.com" {
        listen on * tls port 443
        tls {
                certificate "/etc/ssl/example.com.crt"
                key "/etc/ssl/private/example.com.key"
        }
        location "/pub/*" {
                directory auto index
        }
        location "/.well-known/acme-challenge/*" {
                root "/acme"
                request strip 2
        }
}

First note that in line 4 above, we changed the certificate from /etc/ssl/example.com.fullchain.pem to /etc/ssl/example.com.crt

Here, we deviate from the example httpd.conf(5) because we later plan to use relayd to provide SSL acceleration, and relayd hard codes the paths it searches for the public and private keypair. relayd(8) will only search for public certificates that end in the .crt suffix; it will ignore certificates that end with the suffix .fullchain.pem.

Lines 2-6 tells the web server to listen on all IPs on port 443. As a result, we need a TLS block to specify which certs to use.

Lines 7-9 say that, for any request beginning with https://example.com/pub/, the web server should automatically show a directory listing. Normally this is not a good idea for security reasons, but for a public folder, it should be fine.

Note: You can (optionally) leave the 302 forwarding block commented out:

#       location * {
#                 block return 302 "https://$HTTP_HOST$REQUEST_URI"
#       }

We recommend leaving this block commented out because some (old) web browsers do not support modern TLS, and so allowing plaintext access can improve accessibility. Plaintext access may be important in some restrictive countries that prohibit modern TLS.

In a normal production server, if OpenHTTPd is already running, reloading is best to avoid downtime:

$ doas rcctl reload httpd

For your first test however, you will want to stop OpenHTTPd:

$ doas rcctl stop httpd

Use ps or pgrep to ensure that all httpd(8) processes have been stopped:

$ pgrep httpd

If all httpd processes have been stopped, you should see no output. If you see a number representing a process ID, see the guides on killing processes.

Then, check that your configuration is valid:

$ doas httpd -n

Once you are certain it has been configured properly, you can start the server:

$ doas rcctl start httpd

Testing

To test if your web server has a working SSL cert, use openssl:

$ openssl s_client -connect example.com:443

NOTE: You must replace example.com with your actual hostname.

You should see the correct SSL subject and issuer:

$ openssl s_client -connect example.org:443
CONNECTED(00000003)
depth=2 C = US, O = Internet Security Research Group, CN = ISRG Root X1
verify return:1
depth=1 C = US, O = Let's Encrypt, CN = R11
verify return:1
depth=0 CN = example.com
verify return:1
---
Certificate chain
 0 s:/CN=example.com
   i:/C=US/O=Let's Encrypt/CN=R11
 1 s:/C=US/O=Let's Encrypt/CN=R11
   i:/C=US/O=Internet Security Research Group/CN=ISRG Root X1
---
Server certificate
-----BEGIN CERTIFICATE-----
MIIF+jCCBOKgAwIBAgISBBiSmYI1JcgnGriQsYnjgYNaMA0GCSqGSIb3DQEBCwUA
MDMxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQwwCgYDVQQD
EwNSMTEwHhcNMjQxMTE0MDAzNjU3WhcNMjUwMjEyMDAzNjU2WjAfMR0wGwYDVQQD
ExRqcm11Lmhvc3QuaXJjbm93Lm9yZzCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCC
AgoCggIBANuLaDDkipvSVq4lPmSymFhbnqt7Exv3LRmzq6YvqzWpLOd1wkHNXFHg
yxCE6AbVdz3jqZT00sVO1uF/A3YdN63qlziWJFP1GaCZzcyuJ7a2NAVX/igggxO3
guwzlfFh844AoudJ3+KPBCGfCmI8qWftjOTIz4/huCr3CRsPwuABySWKGh/p9n+3
wJE5EU425hkiTGGDNhF65aU8B/cT3clhdkFKwcNGEX4vkrQwlZeF43Mj9cQf3G3v
uAOdP0DEGqhxyYQUrsGP/ml9S99VnQ91hxta1J4EYwTqCnG4UwyZ/unFJ3vRpajQ
/8LKkVPBQxKaREJNafB0cv29sEqE2RTBWzot8RT6mSFN59b07O7m4pxqHs+OenkW
ltH3lM9pwrFBc0RLipAXkkgauVSohBH7SbVuMDIwCMYFdOHCBRqgW6eDTk+hhklh
nXWR0JJ2lRF1IUQQjduJWadEUDK9O/iUfLfnZr1a5ZfjXs4dlFqVU8NUQWQd3G5J
9d4iCX7VkEigXlJrxTgbohFLkPzeDiSPqdwKqx1GMEWLxrW65a71UR81AJEYTJJE
ixOwGEb1kXtGEqKhM4CYywBLKiDNOEoMPsRg3UsOfHS1eaSDF6io42brmhKILAJL
SP5CTPZw5LYKaqc+aO13keucLBTne5+aWhaQBD0ihqsssYPlxFehAgMBAAGjggIa
MIICFjAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUF
BwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFAVeGed5J7eod57c69KSUDiVDq7m
MB8GA1UdIwQYMBaAFMXPRqTq9MPAemyVxC2wXpIvJuO5MFcGCCsGAQUFBwEBBEsw
STAiBggrBgEFBQcwAYYWaHR0cDovL3IxMS5vLmxlbmNyLm9yZzAjBggrBgEFBQcw
AoYXaHR0cDovL3IxMS5pLmxlbmNyLm9yZy8wHwYDVR0RBBgwFoIUanJtdS5ob3N0
LmlyY25vdy5vcmcwEwYDVR0gBAwwCjAIBgZngQwBAgEwggEGBgorBgEEAdZ5AgQC
BIH3BIH0APIAdwDPEVbu1S58r/OHW9lpLpvpGnFnSrAX7KwB0lt3zsw7CAAAAZMo
T6OZAAAEAwBIMEYCIQD+t4oiZ3lkJeY+nH1glYZjlktnSc31rKjJlBbJwnPTfwIh
ALwVTA0TNEa2jo5zmOq7nypo7awprI48XnDofYsb7GK+AHcAE0rfGrWYQgl4DG/v
THqRpBa3I0nOWFdq367ap8Kr4CIAAAGTKE+kEwAABAMASDBGAiEA5nosfBa3GTMC
Rw9xjef4RVpwdvaaRsC8xDZy95CW86ECIQCLSXo2BqI9coah2trzV3gxq0LnEn9r
XcciSxO0ZH4mCzANBgkqhkiG9w0BAQsFAAOCAQEApjLhmAFD1bEgI5lxzIcGQrdM
3CSgDn7OZEqQS6pbmTGdjk3aiWAUNsNlwBdatdWra171lytEd2wufDf/iN7RWkcK
6BK3RZeTsKK8KNdKiV7oXL9Kd/1NpYSHizVN1obqF3Knh1JM+Kes6YXTxod7L1Av
ozkhle3d61jrUhUz4VEp053pNxi8ylDRd6jeDnIAQbAJlGJapD1P3Sfy0VL+Kprs
ZoEucBa3ZaSh+JNNS0fxSnl/qKfWlwOSsiMNL8yj7sy6hcVEgWqhMkviGGYpNikY
harUihdi26bReT1MXM9nFsYZa20+B1BUGk7Y/0TQ7zo1JtjhSXVIP4pB1zpuwg==
-----END CERTIFICATE-----
subject=/CN=example.com
issuer=/C=US/O=Let's Encrypt/CN=R11
---
No client certificate CA names sent
Server Temp Key: ECDH, X25519, 253 bits
---
SSL handshake has read 3645 bytes and written 386 bytes
---
New, TLSv1/SSLv3, Cipher is TLS_AES_256_GCM_SHA384
Server public key is 4096 bit
Secure Renegotiation IS NOT supported
Compression: NONE
Expansion: NONE
No ALPN negotiated
SSL-Session:
    Protocol  : TLSv1.3
    Cipher    : TLS_AES_256_GCM_SHA384
    Session-ID:
    Session-ID-ctx:
    Master-Key:
    Start Time: 1731552214
    Timeout   : 7200 (sec)
    Verify return code: 0 (ok)
---

At this point, you can make normal HTTP GET requests like with netcat or telnet.

You can also visit the website using your web browser. Open your web browser to https://example.com. If you see an error such as 403 Forbidden, it may mean you have not set up a website.

Look for the SSL padlock in the address bar (which indicates your site is secure), then view more information about the certificate:

Automation

Let's Encrypt TLS certs expire after 90 days. As a result, you should automate the renewal of TLS certs. Otherwise, once a cert expires, your users will be confronted with invalid certificate errors.

We can automate the request process using crontab.

$ doas crontab -e

Add this line at the bottom:

~       ~       *       *       *       acme-client example.com >> /var/log/acme-client.log 2>&1 && sleep 300 && rcctl reload httpd

This cronjob will check the certificate once each day at a random time to see if it needs to be renewed. If it does, it will renew the cert, wait 300 seconds, then reloads httpd(8) to use it.

Troubleshooting

Configuration Errors

If you were unable to establish the connection above, the first test is to see if httpd(8) has any critical errors.

First, stop all httpd processes:

$ doas rcctl stop httpd

Next, confirm that httpd is no longer running:

$ ps -ax | grep httpd

You may need to kill any httpd processes that aren't shut down with rcctl.

Finally, run httpd(8) in debug mode:

$ doas httpd -d

Observe if there are any errors. If so, fix those errors before proceeding with troubleshooting.

Permissions Error

$ ls -l /var/www/htdocs/index.html
-rw-r--r--  1 root  daemon  14 Nov 12 00:15 /var/www/htdocs/index.html

If the file index.html is not readable by httpd, the webpage cannot load. See chmod(1) and chown for more information about permissions on BSD.

Firewall

If you changed your firewall rules?, double check to ensure your firewall is not blocking port 80. By default, packet filter? allows web traffic, so the firewall will not block web traffic unless you have edited the default firewall ruleset.

See Also:

  1. Httpd and Relayd Mastery contains many helpful examples.
  2. https://learnbchs.org/LearnBCHS
  3. acme-client
  4. OpenHTTPd (with TLS)