How To... | Why not..? | Scripts | Patches |
Last update: 2021-04-16
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.
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:
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:
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
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.
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="AEAD-AES256-GCM-SHA384:AEAD-CHACHA20-POLY1305-SHA256:AEAD-AES128-GCM-SHA256: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.
As usual OpenBSD provides a sane default for the ciphers setting:
HIGH:!NULL
. You are perfectly fine if you go for the default instead
of my paranoid ciphers list. Your site will still score an A+ on Qualis
SSL Labs.
If you decide to go with the default you can actually remove the line above with the macro as well as the line that uses the macro actually.
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
tls keypair "www.example.net"
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 are the blocks where the packet action happens. Here you define on which IP relayd(8) listens for incoming packets, how it treats these and where to send these afterwards:
relay "https4" {
listen on $ipv4 port https tls
protocol "https"
forward to <www> port https
}
relay "https6" {
listen on $ipv6 port https tls
protocol "https"
forward to <www> port https
}
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
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.