Setup an HTTPS-enabled web server with httpd
on OpenBSD. Includes A+ security report configuration with haproxy
.
I’m far from an expert! Please help improve this project
This guide assumes you have already setup OpenBSD on your desired server of choice. 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.
Make sure your DNS records are setup and working as intended with your desired domain. You can check their status with:
dig httpd.rocks
Before doing anything else, you need to make sure your /etc/pf.conf
is allowing traffic on ports 80
and 443
. Make sure you include the following:
pass in on any proto { tcp, udp } from any to any port 53
pass out on any proto { tcp, udp } from any to any port 53
pass in on any proto tcp from any to any port {80 443}
pass out on any proto tcp from any to any port {80 443}
Once that is saved, simply reload the ruleset:
pfctl -f /etc/pf.conf
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!
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.fullchain.pem"
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.
First 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"
CERTS_OUTPUT_DIR="/etc/ssl/certs"
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
# Combine .fullchain.pem and .key into a single .pem file in /etc/ssl/certs
echo "Combining .fullchain.pem and .key files for each domain..."
for DOMAIN in $DOMAINS; do
FULLCHAIN="/etc/ssl/$DOMAIN.fullchain.pem"
KEY="/etc/ssl/private/$DOMAIN.key"
COMBINED_PEM="$CERTS_OUTPUT_DIR/$DOMAIN.pem"
if [ -f "$FULLCHAIN" ] && [ -f "$KEY" ]; then
doas sh -c "cat '$FULLCHAIN' '$KEY' > '$COMBINED_PEM'"
doas chmod 644 "$COMBINED_PEM"
echo "Combined $FULLCHAIN and $KEY into $COMBINED_PEM"
else
echo "Missing $FULLCHAIN or $KEY for $DOMAIN, skipping."
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:
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.
This will check if you need to renew certificates every day at midnight (server time). If new certs are needed, it will properly combine the generated fullchain.pem
and key
files into a single <project-name>.pem
file under the shared directory /etc/ssl/certs
.
Since we haven’t run this script yet, we should execute it for building the initial pem
files required for HAProxy:
doas sh /path/to/renew_certs.sh
Many people tend to reach for relayd
in OpenBSD when deciding to setup proxies or include security headers for their sites. Maybe I’m just too dull, but I always struggle to get things running smoothly with it.
That’s why I opt for using HAProxy.
First, install the package:
doas pkg_add haproxy
Now configure the core /etc/haproxy/haproxy.cfg
(take note of the extension! OpenBSD uses cfg
rather than the standard conf
for HAProxy) and add the following to the existing file:
defaults
log global
mode http
option httplog
option dontlognull
timeout connect 5000ms
timeout client 50000ms
timeout server 50000ms
frontend http_in
bind *:80
redirect scheme https if !{ ssl_fc }
default_backend main_backend
frontend https_in
bind *:443 ssl crt /etc/ssl/certs/
default_backend main_backend
# Backend to httpd with security headers, no TLS
backend main_backend
http-response set-header Strict-Transport-Security "max-age=31536000; includeSubDomains"
http-response set-header X-Content-Type-Options "nosniff"
http-response set-header X-Frame-Options "DENY"
http-response set-header X-XSS-Protection "1; mode=block"
http-response set-header Content-Security-Policy "script-src 'self'"
http-response set-header Referrer-Policy "no-referrer"
http-response set-header Permissions-Policy "microphone=()"
server local_httpd 127.0.0.1:8080
The haproxy.cfg
in a nutshell:
frontend http_in
:main_backend
to set security headersfrontend https_in
:main_backend
to set security headersbackend main_backend
:Important: Take note of the line:
bind *:443 ssl crt /etc/ssl/certs/
This tells HAProxy to dynamically scan a directory containing our certificates. We set this up previously with our automated renew_certs.sh
script.
This is handy if you decide to host multiple sites on a single server. Otherwise, you would have to edit and reload your HAProxy config everytime you setup a new website.
So we need to create our certs
directory:
doas mkdir /etc/ssl/certs
doas chmod 644 /etc/ssl/certs
And then create /etc/ssl/certs/httpd.rocks.pem
by combining your certificate and private key if not done (only if you didn’t do this earlier when configuring the renew_certs
script):
cat /etc/ssl/httpd.rocks.crt /etc/ssl/private/httpd.rocks.key > /etc/ssl/certs/httpd.rocks.pem
Once that’s complete, test that everything is working, and if so, enable and start HAProxy:
doas haproxy -c -f /etc/haproxy/haproxy.cfg
doas rcctl enable haproxy
doas rcctl start haproxy
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
Now check out your website!
Everything should work as intended. 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!
I am far from an OpenBSD expert. Please refer to these additional (and mostly better) resources and documentation: