$_ BSDHowTo.ch
How To... Why not..? Scripts Patches RSS logo

How to setup a web server with OpenBSD

Last update: 2020-06-03

Introduction

The title of this post sounds simple. But what I describe in this one goes further than just configure and start httpd(8) and you're done. It is about integrating all the required parts of OpenBSD base into a fully functioning web server that scores an A+ at SSL Labs Server Test using a free certificate from Let's Encrypt.

The configuration I show in this post bases on the assumption that you run all components on the same machine. If this is not the case you have to adapt it. Hints about this are in the relevant sections.

Configuration of httpd(8)

This is web server, so let us configure the web server daemon httpd(8) first. The first server block in the configuration file /etc/httpd.conf makes httpd(8) listen on port 80 of the egress interface. This serves two purposes:

  1. The traffic for acme-client(1) is handled by httpd(8)
  2. All other traffic is redirected to the HTTPS port

Of course this doesn't work if your web server is running on the same machine as your reverse proxy. In this case you have to forward all the traffic coming in on port 80 of the proxy to the web server.

server "www.example.org" {
    listen on egress port http
    alias "example.org"
    root "/"

    location "/.well-known/acme-challenge/*" {
        request strip 2
        root "/acme"
    }

    location * {
        block return 301 "https://www.bsdhowto.ch$REQUEST_URI"
    }
}

The second server block is where the the actual action happens. This one listens on port 443 on the loopback interface. In the configuration of relayd(8) we will use this as the target. Although I use port 443 I will not configure any certificates for this one. It would be pointless to encrypt traffic that stays within the server itself.

In the case of separated machines for relayd(8) and httpd(8) you must change two things to the above configuration:

  1. Listen on the egress interface instead of loopback
  2. Use TLS on the web server

The second point is optional, but I would recommend it. Especially if you have the pleasure to get security audits from time to time.

server "www.example.net" {
    listen on lo port https
    log style forwarded
}

This assumes that the files your web page is made of are all stored in the directory /var/www/htdocs. Of course you can change this by adding a line like root "/htdocs/www.example.net".

Last but not least you should add one block that is used by httpd(8) to handle the Content-Type header:

types {
    include "/usr/share/misc/mime.types"
}

Make sure your config file is free of typos by checking it:

$ doas httpd -n
configuration OK

Configuration of acme-client(1)

The next step is configuring acme-client(1) so you can generate the needed certificate for your web server. Without the certificate relayd(8) will refuse to start. Put something similar in /etc/acme-client.conf:

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

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

With this configuration in place it is time to start httpd(8) and request the certificate for the first time:

$ doas rcctl enable httpd
$ doas rcctl start httpd
$ doas acme-client www.example.net

If you didn't get any error messages check if the certificate is in place:

$ ls /etc/ssl/www.example.net.fullchain.pem

Requesting a certificate from Let's Encrypt is only part of the story. You also want to use OCSP stapling for your web server as measure of protecting your visitors privacy and speeding up access times. Use ocspcheck(8) to generate an .ocsp file that can be used by relayd(8):

$ doas ocspcheck -No /etc/ssl/www.example.net.ocsp /etc/ssl/www.example.net.fullchain.pem

Now that all the pieces are in place there is one more very important step to take. Your certificate will expire every 90 days and your OCSP response will expire every week (at the time of writing). You want some automated maintenance for both pieces. I use the following script for this task:

#!/bin/ksh

dir=/etc/ssl
domain=www.example.net

/usr/sbin/acme-client $domain
if [ $? -eq 0 ] ; then
    /usr/sbin/ocspcheck -No $dir/$domain.ocsp $dir/$domain.fullchain.pem
    /usr/sbin/rcctl restart relayd
fi

/usr/sbin/ocspcheck -i $dir/$domain.ocsp $dir/$domain.fullchain.pem
if [ $? -eq 1 ] ; then
    /usr/sbin/ocspcheck -No $dir/$domain.ocsp $dir/$domain.fullchain.pem
    /usr/sbin/rcctl restart relayd
fi

exit 0

Store the script somewhere and make an entry in the crontab(5) of root to run it once a day. Or let daily(8) do its job by calling the script from /etc/daily.local. Either way make sure you receive the mails generated by cron(8) to be the first to know if something goes wrong with the certificate renewal.

Configuration of relayd(8)

All the configuration of relayd(8) goes into /etc/relayd.conf. First you add some global options to it:

log state changes
log connection
prefork 10

This makes sure that any state changes of the destination are logged by relayd(8), e. g. if httpd(8) stops listening. Also all connections to this server are logged with source IP address and port number. The last entry makes sure that relayd(8) starts ten working processes right away. You may want to increase this number on busy servers even more.

The next part contains macros and tables used in the latter sections. I assume that your web server is reachable via IPv4 and IPv6:

list="ECDHE-RSA-CHACHA20-POLY1305:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256"
ipv4="192.0.2.80"
ipv6="2001:db8:19bf:e::c000:0250"

table <www> { 127.0.0.1 }

Make sure that the table in your configuration points to the host name or IP address your httpd(8) is listening on.

The list of allowed TLS ciphers is very strict. This will prevent older browsers from connecting to your site. And it will prevent any attacks on your server using known weak ciphers which should no longer be used.

The following is the block in the config that actually makes sure the HTTP traffic to and from your server complies to certain security measures. It is the part where the filtering happens and where we tweak the HTTP headers.

http protocol "https" {
    tls ciphers $list

    return error

    match request header set "X-Forwarded-For" value "$REOTE_ADDR"
    match request header set "X-Forwarded-Port" value "$REMOTE_PORT"

    match response header set "Content-Security-Policy" value \
        "default-src 'self'"
    match response header set "Referrer-Policy" value "no-referrer"
    match response header set "Strict-Transport-Security" value \
        "max-age=15552000; includeSubDomains; preload"
    match response header set "X-Content-Type-Options" value "nosniff"
    match response header set "X-Frame-Options" value "SAMEORIGIN"
    match response header set "X-XSS-Protection" value "1; mode=block"

    match method GET tag ok
    match method HEAD tag ok

    block
    pass tagged ok forward to <www>
}

The last part of the configuration is the block where the packet action happens. Here you define where to listen for incoming packets, how to treat them and where to send them afterwards:

relay "https" {
    listen on $ipv4 port https tls
    listen on $ipv6 port https tls
    protocol "https"
    forward to <www> port https
}

Final steps

Now that the configuration is ready there is one more thing to do. The pattern that relayd(8) uses to search for certificate and key files is designed to use the same certificate/key pair on different ports, e. g. if your relayd(8) forwards SMTP too. The easiest way to achieve this is to use symlinks:

$ cd /etc/ssl
$ doas -s
# ln -s www.example.net.fullchain.pem www.example.net:443.crt
# ln -s www.example.net.ocsp www.example.net:443.ocsp
# cd private
# ln -s www.example.net.key www.example.net:443.key
# ^D

This way you can add additional relays to the config and simply create the new symlinks with the matching port number. This scheme also comes in handy if you have more than one line of tls keypair in your config to support different host names with SNI.

Finally, your web server is ready to serve some clients. Check the configuration to be sure that all the typos are gone:

$ doas relayd -n
configuration OK

Now you can go ahead and start relayd(8):

$ doas rcctl enable relayd
$ doas rcctl start relayd

Closing thoughts

Important warning: Make sure you understand what all the response headers do that my configuration sets. Some or all of the values might prevent web sites or parts of web sites from working as expected. The values chosen are right for this site, but your site may be different. Don't complain to me if your site breaks.

I'm aware of some other security mechanisms propagated in the wild like HPKP. Not all of these headers are required for all the sites. And not all of these headers actually strengthen the security. In fact, security is a process you have to follow, not a knob or header that you switch on and be done with it.