Serving via httpd on OpenBSD

Serving via httpd on OpenBSD

httpd is the default web server that is included in the base install on OpenBSD. It's minimal and lightweight but secure and capable, and provides a clean interface for setting up a Fossil server using FastCGI.

This article will detail the steps required to setup a TLS-enabled httpd configuration that serves multiple Fossil repositories out of a single directory within a chroot, and allow ssh access to create new repositories remotely.

NOTE: The following instructions assume an OpenBSD 6.7 installation.

Install Fossil

Use the OpenBSD package manager pkg_add to install Fossil, making sure to select the statically linked binary.

$ doas pkg_add fossil
quirks-3.325 signed on 2020-06-12T06:24:53Z
Ambiguous: choose package for fossil
      0: <None>
      1: fossil-2.10v0
      2: fossil-2.10v0-static
Your choice: 2
fossil-2.10v0-static: ok

This installs Fossil into the chroot. To facilitate local use, create a symbolic link of the fossil executable into /usr/local/bin.

$ doas ln -s /var/www/bin/fossil /usr/local/bin/fossil

As a privileged user, create the file /var/www/cgi-bin/scm with the following contents to make the CGI script that httpd will execute in response to fsl.domain.tld requests; all paths are relative to the /var/www chroot.

directory: /htdocs/fsl.domain.tld
notfound: https://domain.tld
errorlog: /logs/fossil.log

The directory directive instructs Fossil to serve all repositories found in /var/www/htdocs/fsl.domain.tld, while errorlog sets logging to be saved to /var/www/logs/fossil.log; create the repository directory and log file—making the latter owned by the www user, and the script executable.

$ doas mkdir /var/www/htdocs/fsl.domain.tld
$ doas touch /var/www/logs/fossil.log
$ doas chown www /var/www/logs/fossil.log
$ doas chmod 660 /var/www/logs/fossil.log
$ doas chmod 755 /var/www/cgi-bin/scm

Setup chroot

Fossil needs both /dev/random and /dev/null, which aren't accessible from within the chroot, so need to be constructed; /var, however, is mounted with the nodev option. Rather than removing this default setting, create a small memory filesystem and then mount it on to /var/www/dev with mount_mfs(8) so that the random and null device files can be created. In order to avoid necessitating a startup script to recreate the device files at boot, create a template of the needed /dev tree to automatically populate the memory filesystem.

$ doas mkdir /var/www/dev
$ doas install -d -g daemon /template/dev
$ cd /template/dev
$ doas /dev/MAKEDEV urandom
$ doas mknod -m 666 null c 2 2
$ doas mount_mfs -s 1M -P /template/dev /dev/sd0b /var/www/dev
$ ls -l
total 0
crw-rw-rw-  1 root  daemon    2,   2 Jun 20 08:56 null
lrwxr-xr-x  1 root  daemon         7 Jun 18 06:30 random@ -> urandom
crw-r--r--  1 root  wheel    45,   0 Jun 18 06:30 urandom

To make the mountable memory filesystem permanent, open /etc/fstab as a privileged user and add the following line to automate creation of the filesystem at startup:

swap /var/www/dev mfs rw,-s=1048576,-P=/template/dev 0 0

The same user that executes the fossil binary must have writable access to the repository directory that resides within the chroot; on OpenBSD this is www. In addition, grant repository directory ownership to the user who will push to, pull from, and create repositories.

$ doas chown -R user:www /var/www/htdocs/fsl.domain.tld
$ doas chmod 770 /var/www/htdocs/fsl.domain.tld

Configure httpd

On OpenBSD, httpd.conf(5) is the configuration file for httpd. To setup the server to serve all Fossil repositores within the directory specified in the CGI script, and automatically redirect standard HTTP requests to HTTPS—apart from Let's Encrypt challenges issued in response to acme-client(1) certificate requests—create /etc/httpd.conf as a privileged user with the following contents.

server "fsl.domain.tld" {
    listen on * port http
    root "/htdocs/fsl.domain.tld"
    location "/.well-known/acme-challenge/*" {
        root "/acme"
        request strip 2
    location * {
        block return 301 "https://$HTTP_HOST$REQUEST_URI"
    location  "/*" {
        fastcgi { param SCRIPT_FILENAME "/cgi-bin/scm" }

server "fsl.domain.tld" {
    listen on * tls port https
    root "/htdocs/fsl.domain.tld"
    tls {
        certificate "/etc/ssl/domain.tld.fullchain.pem"
        key "/etc/ssl/private/domain.tld.key"
    hsts {
        max-age 15768000
    connection max request body 104857600
    location  "/*" {
        fastcgi { param SCRIPT_FILENAME "/cgi-bin/scm" }
    location "/.well-known/acme-challenge/*" {
        root "/acme"
        request strip 2

The default limit for HTTP messages in OpenBSD’s httpd server is 1 MiB. Fossil chunks its sync protocol such that this is not normally a problem, but when sending unversioned content, it uses a single message for the entire file. Therefore, if you will be storing files larger than this limit as unversioned content, you need to raise the limit as we’ve done above with the “connection max request body” setting, raising the limit to 100 MiB.

NOTE: If not already in possession of a HTTPS certificate, comment out the https server block and proceed to securing a free Let's Encrypt Certificate; otherwise skip to Start httpd.

Let's Encrypt Certificate

In order for httpd to serve HTTPS, secure a free certificate from Let's Encrypt using acme-client. Before issuing the request, however, ensure you have a zone record for the subdomain with your registrar or nameserver. Then open /etc/acme-client.conf as a privileged user to configure the request.

authority letsencrypt {
    api url ""
    account key "/etc/acme/letsencrypt-privkey.pem"

authority letsencrypt-staging {
    api url ""
    account key "/etc/acme/letsencrypt-staging-privkey.pem"

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

Start httpd with the new configuration file, and issue the certificate request.

$ doas rcctl start httpd
$ doas acme-client -vv domain.tld
acme-client: /etc/acme/letsencrypt-privkey.pem: account key exists (not creating)
acme-client: /etc/acme/letsencrypt-privkey.pem: loaded RSA account key
acme-client: /etc/ssl/private/domain.tld.key: generated RSA domain key
acme-client: directories
acme-client: DNS:
N(Q????Z???j?j?>W#????b???? H????eb??T??*? DNosz(???n{L}???D???4[?B] (1174 bytes)
acme-client: /etc/ssl/domain.tld.crt: created
acme-client: /etc/ssl/domain.tld.fullchain.pem: created

A successful result will output the public certificate, full chain of trust, and private key into the /etc/ssl directory as specified in acme-client.conf.

$ doas ls -lR /etc/ssl
-r--r--r--   1 root  wheel   2.3K Mar  2 01:31:03 2018 domain.tld.crt
-r--r--r--   1 root  wheel   3.9K Mar  2 01:31:03 2018 domain.tld.fullchain.pem

-r--------  1 root  wheel   3.2K Mar  2 01:31:03 2018 domain.tld.key

Make sure to reopen /etc/httpd.conf to uncomment the second server block responsible for serving HTTPS requests before proceeding.

Start httpd

With httpd configured to serve Fossil repositories out of /var/www/htdocs/fsl.domain.tld, and the certificates and key in place, enable and start slowcgi—OpenBSD's FastCGI wrapper server that will execute the above Fossil CGI script—before checking that the syntax of the httpd.conf configuration file is correct, and (re)starting the server (if still running from requesting a Let's Encrypt certificate).

$ doas rcctl enable slowcgi
$ doas rcctl start slowcgi
$ doas httpd -vnf /etc/httpd.conf
configuration OK
$ doas rcctl start httpd

Configure Client

To facilitate creating new repositories and pushing them to the server, add the following function to your ~/.cshrc or ~/.zprofile or the config file for whichever shell you are using on your development box.

finit() {
    fossil init $1.fossil && \
    chmod 664 $1.fossil && \
    fossil open $1.fossil && \
    fossil user password $USER $PASSWD && \
    fossil remote-url https://$USER:$PASSWD@fsl.domain.tld/$1 && \
    rsync --perms $1.fossil $USER@fsl.domain.tld:/var/www/htdocs/fsl.domain.tld/ >/dev/null && \
    chmod 644 $1.fossil && \
    fossil ui

This enables a new repository to be made with finit repo, which will create the fossil repository file repo.fossil in the current working directory; by default, the repository user is set to the environment variable $USER. It then opens the repository and sets the user password to the $PASSWD environment variable (which you can either set with export PASSWD 'password' on the command line or add to a secured shell environment file), and the remote-url to https://fsl.domain.tld/repo with the credentials of $USER who is authenticated with $PASSWD. Finally, it rsync's the file to the server before opening the local repository in your browser where you can adjust settings such as anonymous user access, and set pertinent repository details. Thereafter, you can add files with fossil add, and commit with fossil ci -m 'commit message' where Fossil, by default, will push to the remote-url. It's suggested you read the Fossil documentation; with a sane and consistent development model, the system is much more efficient and cohesive than git—so the learning curve is not steep at all.

Return to the top-level Fossil server article.