OpenBSD webserver with httpd, relayd and TLS

Published on 2019-06-18 and last updated on 2019-11-22

This guide will show you how to set up a TLS enabled OpenBSD webserver using httpd and relayd. For the sake of the exercise I’m assuming the following:

  • www.my-website.com is the main domain of the website.
  • my-website.com, www.yourhomepage.net and yourhomepage.net are supposed to redirect to www.my-website.com.
  • DNS records for all domains exist.
  • TLS certificates will be provided by Let’s Encrypt.
  • Scores for security headers and TLS configuration need to be at least A+.

Plaintext setup

Create a directory to put your website into:

root@openbsd ~$: mkdir /var/www/htdocs/www.my-website.com

Put a nice greeting in there so you can test whether your setup is working:

root@openbsd ~$: echo 'Hello, world!' > /var/www/htdocs/www.my-website.com/index.html

Create /etc/httpd.conf:

# make all OpenBSD default mime types available
types {
  include "/usr/share/misc/mime.types"
}

# this is the main domain
server "www.my-website.com" {

  # listen on localhost port 8080 using IPv4 and IPv6
  listen on 127.0.0.1 port 8080
  listen on ::1 port 8080

  # httpd chroots into /var/www
  # so the root directory is relative to that
  root "/htdocs/www.my-website.com"

  # make /var/www/acme availabe
  location "/.well-known/acme-challenge/*" {

    # /var/www/acme
    root "/acme"

    # remove .well-known and acme-challenge from the path
    # before looking for the file in /var/www/acme
    request strip 2
  }
}

# this block redirects to the main domain
server "my-website.com" {

  # this server block also serves the following domains:
  alias "www.yourhomepage.net"
  alias "yourhomepage.net"

  listen on 127.0.0.1 port 8080
  listen on ::1 port 8080

  # stop processing the request and send a permanent
  # redirect to the main domain instead
  block return 301 "http://www.my-website.com$REQUEST_URI"
}

Check the syntax:

root@openbsd ~$: httpd -n
configuration OK

Enable httpd to start at boot:

root@openbsd ~$: rcctl enable httpd

Start httpd:

root@openbsd ~$: rcctl start httpd
httpd(ok)

Edit /etc/relayd.conf:

# set your external IP addresses
external_ipv4 = "1.2.3.4"

# this needs to be the expanded IPv6 address
external_ipv6 = "dead:beef:0:0:0:0:42:23"

http protocol "www" {
  # you may want to remove this depending on your use case
  match request header set "Connection" value "close"

  # your web application might need these headers
  match request header set "X-Forwarded-For" value "$REMOTE_ADDR"
  match request header set "X-Forwarded-By" value "$SERVER_ADDR:$SERVER_PORT"

  # set best practice security headers
  # use https://securityheaders.com to check
  # and modify as needed
  match response header remove "Server"
  match response header set "X-Frame-Options" value "SAMEORIGIN"
  match response header set "X-XSS-Protection" value "1; mode=block"
  match response header set "X-Content-Type-Options" value "nosniff"
  match response header set "Referrer-Policy" value "strict-origin"
  match response header set "Content-Security-Policy" value "default-src 'self'"
  match response header set "Feature-Policy" value "accelerometer 'none'; camera 'none'; geolocation 'none'; gyroscope 'none'; magnetometer 'none'; microphone 'none'; payment 'none'; usb 'none'"

  # set recommended tcp options
  tcp { nodelay, sack, socket buffer 65536, backlog 100 }
}

# split IPv4 and IPv6 so they can
# be distinguished in the access log
relay "www4" {
  listen on $external_ipv4 port 80
  protocol www
  forward to 127.0.0.1 port 8080
}

relay "www6" {
  listen on $external_ipv6 port 80
  protocol www
  forward to ::1 port 8080
}

Check the syntax:

root@openbsd ~$: relayd -n
configuration OK

Enable relayd to start at boot:

root@openbsd ~$: rcctl enable relayd

Start relayd:

root@openbsd ~$: rcctl start relayd
relayd(ok)

Test the setup:

user@local ~$: curl -I http://www.my-website.com | grep 'HTTP'
HTTP/1.1 200 OK

user@local ~$: curl -I http://my-website.com | grep 'HTTP'
HTTP/1.1 200 OK

user@local ~$: curl -I http://www.yourhomepage.net | grep 'HTTP'
HTTP/1.1 200 OK

user@local ~$: curl -I http://yourhomepage.net | grep 'HTTP'
HTTP/1.1 200 OK

TLS setup

Edit /etc/acme-client.conf:

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

domain www.my-website.com {
        alternative names { my-website.com www.yourhomepage.net yourhomepage.net }
        domain key "/etc/ssl/private/www.my-website.com.key"
        domain certificate "/etc/ssl/www.my-website.com.crt"
        domain full chain certificate "/etc/ssl/www.my-website.com.pem"
        sign with letsencrypt
}

Get your certificates:

root@openbsd ~$: acme-client -ADv www.my-website.com
[...]
acme-client: /etc/ssl/www.my-website.com.crt: created
acme-client: /etc/ssl/www.my-website.com.pem: created

relayd expects your certificates to be in a specific location and named after the IP addresses it is listening on. This cannot be changed at the current time. It is good practice to name certificates after the domain(s) they have been issued for.

Create symbolic links named after the relayd listen IP addresses pointing to the real certificates to satisfy all requirements:

root@openbsd ~$: cd /etc/ssl
root@openbsd ~/etc/ssl$: ln -s www.my-website.com.pem dead:beef::42:23.pem
root@openbsd ~/etc/ssl$: ln -s www.my-website.com.pem 1.2.3.4.pem

# create .crt links pointing to the full certificate chain in the .pem file
root@openbsd ~/etc/ssl$: ln -s www.my-website.com.pem dead:beef::42:23.crt
root@openbsd ~/etc/ssl$: ln -s www.my-website.com.pem 1.2.3.4.crt

root@openbsd ~$: cd /etc/ssl/private
root@openbsd ~/etc/ssl/private$: ln -s www.my-website.com.key dead:beef::42:23.key
root@openbsd ~/etc/ssl/private$: ln -s www.my-website.com.key 1.2.3.4.key

Expand the httpd config to support redirection of plaintext to TLS:

# make all OpenBSD default mime types available
types {
  include "/usr/share/misc/mime.types"
}

# this is the main domain
server "www.my-website.com" {

  # listen on localhost port 8080 using IPv4 and IPv6
  listen on 127.0.0.1 port 8080
  listen on ::1 port 8080

  # httpd chroots into /var/www
  # so the root directive is relative to that
  root "/htdocs/www.my-website.com"

  # make /var/www/acme availabe
  location "/.well-known/acme-challenge/*" {

    # /var/www/acme
    root "/acme"

    # remove .well-known and acme-challenge from the path
    # before looking for the file in /var/www/acme
    request strip 2
  }
}

# this block redirects all TLS protected requests to
# secondary domains to the TLS protected main domain
server "my-website.com" {

  # this server block also serves the following domains:
  alias "www.yourhomepage.net"
  alias "yourhomepage.net"

  listen on 127.0.0.1 port 8080
  listen on ::1 port 8080

  # stop processing the request and send a permanent
  # redirect to the main domain instead
  block return 301 "https://www.my-website.com$REQUEST_URI"
}

# this block redirects all plaintext requests to
# their respective TLS domains
server "www.my-website.com" {

  alias "my-website.com"
  alias "www.yourhomepage.net"
  alias "yourhomepage.net"

  # listen on localhost port 8081(!) using IPv4 and IPv6
  listen on 127.0.0.1 port 8081
  listen on ::1 port 8081

  # make the acme-challenge directory available via plaintext
  location "/.well-known/acme-challenge/*" {
    root "/acme"
    request strip 2
  }

  # redirect all other requests
  block return 301 "https://$SERVER_NAME$REQUEST_URI"
}

Check the syntax:

root@openbsd ~$: httpd -n
configuration OK

Restart httpd:

root@openbsd ~$: rcctl restart httpd
httpd(ok)
httpd(ok)

Expand the relayd config to support both plaintext and TLS:

external_ipv4 = "1.2.3.4"

# this needs to be the expanded IPv6 address
external_ipv6 = "dead:beef:0:0:0:0:42:23"

http protocol "www" {
  # you may want to remove this depending on your use case
  match request header set "Connection" value "close"

  # your web application might need these headers
  match request header set "X-Forwarded-For" value "$REMOTE_ADDR"
  match request header set "X-Forwarded-By" value "$SERVER_ADDR:$SERVER_PORT"

  # set best practice security headers
  # use https://securityheaders.com to check
  # and modify as needed
  match response header remove "Server"
  match response header set "X-Frame-Options" value "SAMEORIGIN"
  match response header set "X-XSS-Protection" value "1; mode=block"
  match response header set "X-Content-Type-Options" value "nosniff"
  match response header set "Referrer-Policy" value "strict-origin"
  match response header set "Content-Security-Policy" value "default-src 'self'"
  match response header set "Feature-Policy" value "accelerometer 'none'; camera 'none'; geolocation 'none'; gyroscope 'none'; magnetometer 'none'; microphone 'none'; payment 'none'; usb 'none'"

  # set recommended tcp options
  tcp { nodelay, sack, socket buffer 65536, backlog 100 }
}

http protocol "wwwsecure" {
  # you may want to remove this depending on your use case
  match request header set "Connection" value "close"

  # your web application might need these headers
  match request header set "X-Forwarded-For" value "$REMOTE_ADDR"
  match request header set "X-Forwarded-By" value "$SERVER_ADDR:$SERVER_PORT"

  # set best practice security headers
  # use https://securityheaders.com to check
  # and modify as needed
  match response header remove "Server"
  match response header set "Strict-Transport-Security" value "max-age=31536000; includeSubDomains"
  match response header set "X-Frame-Options" value "SAMEORIGIN"
  match response header set "X-XSS-Protection" value "1; mode=block"
  match response header set "X-Content-Type-Options" value "nosniff"
  match response header set "Referrer-Policy" value "strict-origin"
  match response header set "Content-Security-Policy" value "default-src 'self'"
  match response header set "Feature-Policy" value "accelerometer 'none'; camera 'none'; geolocation 'none'; gyroscope 'none'; magnetometer 'none'; microphone 'none'; payment 'none'; usb 'none'"

  # set recommended tcp options
  tcp { nodelay, sack, socket buffer 65536, backlog 100 }
}

# split IPv4 and IPv6 so they can
# be distinguished in the access log
relay "www4" {
  listen on $external_ipv4 port 80
  protocol www
  forward to 127.0.0.1 port 8081 # watch out: 8081, not 8080
}

relay "www6" {
  listen on $external_ipv6 port 80
  protocol www
  forward to ::1 port 8081 # watch out: 8081, not 8080
}

relay "wwwsecure4" {
  listen on $external_ipv4 port 443 tls
  protocol wwwsecure
  forward to 127.0.0.1 port 8080
}

relay "wwwsecure6" {
  listen on $external_ipv6 port 443 tls
  protocol wwwsecure
  forward to ::1 port 8080
}

Check the syntax:

root@openbsd ~$: relayd -n
configuration OK

Restart relayd:

root@openbsd ~$: rcctl restart relayd
relayd(ok)
relayd(ok)

Test whether the setup is working:

user@local ~$: curl -I http://www.my-website.com | grep 'Location\|HTTP'
HTTP/1.0 301 Moved Permanently
Location: https://www.my-website.com/

user@local ~$: curl -I http://my-website.com | grep 'Location\|HTTP'
HTTP/1.0 301 Moved Permanently
Location: https://www.my-website.com/

user@local ~$: curl -I http://www.yourhomepage.net | grep 'Location\|HTTP'
HTTP/1.0 301 Moved Permanently
Location: https://www.my-website.com/

user@local ~$: curl -I http://yourhomepage.net | grep 'Location\|HTTP'
HTTP/1.0 301 Moved Permanently
Location: https://www.my-website.com/

user@local ~$: curl -I https://www.my-website.com | grep 'Location\|HTTP'
HTTP/1.1 200 OK

user@local ~$: curl -I https://my-website.com | grep 'Location\|HTTP'
HTTP/1.0 301 Moved Permanently
Location: https://www.my-website.com/

user@local ~$: curl -I https://www.yourhomepage.net | grep 'Location\|HTTP'
HTTP/1.0 301 Moved Permanently
Location: https://www.my-website.com/

user@local ~$: curl -I https://yourhomepage.net | grep 'Location\|HTTP'
HTTP/1.0 301 Moved Permanently
Location: https://www.my-website.com/

Edit your contab with crontab -e and add the following line to automate certificate renewal:

#minute hour    mday    month   wday    command
21      5       *       *       *       acme-client www.my-website.com && rcctl reload relayd

Put a reminder into your calendar to check whether renewing the certificates has worked.

For the future: OCSP stapling

relayd does not support OCSP stapling yet (httpd does). This is how you generate an OCSP bundle:

root@openbsd ~$: ocspcheck -N -o /etc/ssl/www.my-website.com.ocsp /etc/ssl/www.my-website.com.pem

The -N option disables the usage of nonces for the OCSP request, because Let’s Encrypt does not support them.

Regenerate the OCSP staple every 5 days:

#minute hour    mday    month   wday    command
33      16       */5       *       *       ocspcheck -N -o /etc/ssl/www.my-website.com.ocsp /etc/ssl/www.my-website.com.pem