httpd.rocks


Setup an HTTPS-enabled web server with httpd on OpenBSD. Includes A+ security report configuration with relayd or haproxy.

If you’d prefer to use something like Caddy instead, check out caddy.ninja.


Before You Begin…

This guide assumes you have already setup OpenBSD on your desired server of choice. If you need help setting up OpenBSD on a VPS, check out this helpful guide: Installing OpenBSD on Linveo KVM VPS

Most commands will need to run via doas, since you should be logged in as a created user - never root directly.

All the examples in this guide use httpd.rocks for the domains (how meta…). Please remember to change this to your desired URL.

Prep Your Domain(s)

Make sure your DNS records are setup and working as intended with your desired domain. You can check their status with:

dig httpd.rocks

Configure Firewall (pf.conf)

Before doing anything else, you need to make sure your /etc/pf.conf is allowing traffic on ports 80 and 443. You should also include sane defaults for limiting connections (in this example, we are basing these limits off a VPS with 4GB RAM / 4 cores CPU).

Make sure you include the following:

pass in on any proto tcp from any to any port {80 443} keep state \
    (max-src-conn 100, max-src-conn-rate 30/5, overload <block_table> flush global)
pass out on any proto tcp from any to any port {80 443} keep state

table <block_table> persist
block in quick from <block_table>

# Connection limits
set limit states 100000      # Maximum state table entries
set timeout interval 10      # Interval between state purges
set optimization "normal"    # General optimization for connection tracking

Once that is saved, simply reload the ruleset:

pfctl -f /etc/pf.conf

Setting Up httpd

Make initial website folder and files:

doas mkdir -p /var/www/htdocs/httpd.rocks

Place your website files into this new folder and set proper permissions:

doas chmod -R 755 /var/www/htdocs/httpd.rocks
doas chown -R www:www /var/www/htdocs/httpd.rocks

Create the initial /etc/httpd.conf file:

server "www.httpd.rocks" {
    listen on * port 80
    root "/htdocs/httpd.rocks"

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

server "httpd.rocks" {
    listen on * port 80
    root "/htdocs/httpd.rocks"

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

Then get httpd up and running:

doas rcctl start httpd

Note: If you encounter runtime errors with httpd, you might be required to add the following to your /etc/rc.conf.local file:

httpd_flags=""

If everything was setup properly, you should be able to visit the HTTP-only version of your website online. The only problem is HTTPS isn’t setup…

..yet!

Configure acme-client.conf

Before anything else, we need to create proper directories for acme-client (our next steps) and set their permissions:

doas mkdir -p -m 750 /etc/ssl/private
doas mkdir -p -m 755 /var/www/acme

Create the /etc/acme-client.conf file and include the following:

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

domain httpd.rocks {
    alternative names { www.httpd.rocks }
    domain key "/etc/ssl/private/httpd.rocks.key"
    domain full chain certificate "/etc/ssl/httpd.rocks.crt"
    sign with letsencrypt
}

Now we can run the core acme-client command to generate our certificates:

doas acme-client -v httpd.rocks

If everything goes smoothly, your new certificates should be generated and issued. The next thing you will want to do is automatically check for expired certs.

Then create a separate script (this will be helpful if you plan to host multiple sites on a single server). Name it something like renew_certs.sh and save it under a local directory (ie. /home/username/scripts):

#!/bin/sh

DOMAINS="httpd.rocks example1.com example2.com example3.com"

echo "Checking certificates for each domain..."

# Loop through each domain and run acme-client only if renewal is needed
for DOMAIN in $DOMAINS; do
    if ! doas acme-client -n "$DOMAIN"; then
        echo "Certificate for $DOMAIN needs renewal, running acme-client..."
        doas acme-client "$DOMAIN" || exit 1
    else
        echo "Certificate for $DOMAIN is still valid, skipping renewal."
    fi
done

# Reload httpd after updating certificates
echo "Reloading httpd..."
doas rcctl reload httpd
echo "httpd reloaded successfully."

For reference I have included multiple domains if you decide to host several websites through one server. Remove these if you only plan to host a single domain.

Set executable permissions on this script:

doas chmod +x /path/to/renew_certs.sh

Then setup the following cronjob by running crontab -e and entering in:

0 0 * * * doas -u <user> /path/to/renew_certs.sh

Replace <user> with your username. Certificate automation has been completed!

Setup relayd

Next we will setup security headers and HTTPS redirection with OpenBSD’s built-in relayd.

Note: If you would prefer to use HAProxy instead, follow this guide here

You will want to create a new file located at /etc/relayd.conf:

ip4="YOUR IPv4"
ip6="YOUR IPv6"
table <www> { 127.0.0.1 }
log connection

http protocol https {
        match request header append "X-Forwarded-For" value "$REMOTE_ADDR"
        match request header append "X-Forwarded-By" \
            value "$SERVER_ADDR:$SERVER_PORT"
        match request header set "Connection" value "close"
        tcp { sack, backlog 128 }
        tls { keypair httpd.rocks }
        
    match request header "Host" value "httpd.rocks" forward to <www>

     # Add security headers
        match response header append "Strict-Transport-Security" value "max-age=31536000; includeSubDomains; preload"
        match response header append "Content-Security-Policy" value "default-src 'self'; script-src 'self'; object-src 'none';"
        match response header append "X-Content-Type-Options" value "nosniff"
        match response header append "X-Frame-Options" value "DENY"
        match response header append "Referrer-Policy" value "no-referrer"
        match response header append "Permissions-Policy" value "geolocation=(), microphone=(), camera=()"
}

relay wwwtls {
        listen on $ip4 port 443 tls
        protocol https
        forward to <www> port 80 check icmp
}
relay www6tls {
        listen on $ip6 port 443 tls
        protocol https
        forward to <www> port 80 check icmp
}

Make sure you edit your IPv4 and IPv6 with your server IPs. Then replace all instances of httpd.rocks with your own domain. The security headers are set to my own personal preference - feel free to change these as you see fit! Test the config with the following command:

doas relayd -n

If configuration passes, we just need to enable relayd on boot and start it up:

doas rcctl enable relayd
doas rcctl start relayd

Now HTTPS will be working as intended!

Forward Traffic to Non-WWW

Return to the core /etc/httpd.conf file and add the following redirect block to your www server section:

server "www.httpd.rocks" {
    listen on * port 80
    root "/htdocs/httpd.rocks"
    block return 301 "https://httpd.rocks$REQUEST_URI"

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

Restart your httpd server again:

rcctl restart httpd

It’s Alive!

Now check out your website! Everything should work perfectly!

You should have valid TLS, your standard HTTP request should forward to HTTPS, all www requests should forward to non-www, and your security headers should score an A+.

That’s it!


References

Please refer to these additional (and mostly better) resources, books, and documentation:


Contribute

I’m far from an OpenBSD expert! Please help improve this project!