Git and cgit on OpenBSD 7.4

Posted on Nov 24, 2023

This is a guide to integrate cgit with OpenBSD’s httpd(8) web server and slowcgi(8) FastCGI server.

Unlike most other web servers, httpd runs in a chroot, meaning that nothing outside the chroot is available to the web server process. Therefore, in order for the system’s Git repositories to be readable by the cgit CGI script, they’ll need to be placed within the chroot.

Also note that cgit seems to works best on a subdomain, like git.example.com, instead of a subdirectory, like example.com/git. It can be done, but it will likely need additional tweaking. This guide goes with the subdomain method.

cgit and httpd

Run these commands as root or using doas or sudo:

# configure httpd to serve the cgit CGI script
cat > /etc/httpd.conf << EOF
server "git.example.com" {
    listen on * port 80
    # don't serve static files from CGI
    location "/cgit.*" {
        root "/cgit"
        no fastcgi
    }
    root "/cgi-bin/cgit.cgi"
    fastcgi socket "/run/slowcgi.sock"
}
EOF

# install the necessary packages
pkg_add git cgit

# enable and start the web server and FastCGI server
rcctl enable httpd slowcgi
rcctl start httpd slowcgi

# create the git user account and put its home directory under the chroot
useradd -d /var/www/pub/git -s /usr/local/bin/git-shell git
mkdir -p /var/www/pub/git && cd /var/www/pub/git

# silence the message of the day
touch .hushlogin

# add one or more public SSH keys
mkdir .ssh && chmod 700 .ssh
cp /home/user/.ssh/authorized_keys .ssh/

# executable scripts in this directory are run by git-shell
# see `man 1 git-shell` for more info
mkdir git-shell-commands
cat > git-shell-commands/no-interactive-login << EOF
#!/bin/sh
printf '%s\n' "Interactive login not allowed for \$USER."
exit 128
EOF
chmod +x git-shell-commands/no-interactive-login

# create a bare repository that we can push to
git init --bare example.git

# set the right owner and group for the git directory
chown -R git:git /var/www/pub/git

# create cgit's cache directory
mkdir /var/www/cgit/cache && chown www /var/www/cgit/cache

# create cgit config file (run `man 5 cgitrc` for full documentation)
cat > /var/www/conf/cgitrc << EOF
# this is relative to the chroot at /var/www
cache-root=/cgit/cache
# set cache size to 0 to disable caching
cache-size=1000
clone-url=http://git.example.com/\$CGIT_REPO_URL ssh://git@example.com:\$CGIT_REPO_URL
repo.url=example.git
# also relative to the chroot
repo.path=/pub/git/example.git
repo.owner=user@example.com
repo.desc=an example repository
EOF

Individual repositories can be configured with repo keys (at least repo.url and repo.path are needed), as above, or a root directory with multiple repositories can be selected with scan-path:

# read certain repo metadata fields from git config
enable-git-config=1
# text file with paths of repositories to be served (optional)
# if not specified, all repos under scan-path will be selected
project-list=/pub/git/projects.list
# relative to the chroot
scan-path=/pub/git

With this method, the owner and description text will need to be set by running git config gitweb.owner user@example.com and git config gitweb.description "an example repository" in an individual repo’s directory.

On your local system:

mkdir example && cd example
echo "Hello world" > README
git init
git add README
git commit -m "initial commit"
git remote add origin git@example.com:example.git
git push -u origin master

The repository is now available to view at http://git.example.com/example.git and can be cloned using SSH (as in the example above) or over HTTP (read-only) using the repository’s URL. If you want to disable HTTP access or use a different method for it (like Git’s Smart HTTP), add enable-http-clone=0 to cgitrc.

git-daemon

Git over SSH is handled by the OS and Git over HTTP is provided by cgit, but git-daemon is needed for git:// protocol access.

rcctl enable gitdaemon
rcctl set gitdaemon flags "--reuseaddr --base-path=/var/www/pub/git /var/www/pub/git"
touch /var/www/pub/git/example.git/git-daemon-export-ok
rcctl start gitdaemon

The magic file git-daemon-export-ok is needed for each repository published this way, or --export-all can be prepended to the gitdaemon flags to publish all repos under the specified path.

Now, the repo is available to be cloned with git clone git://example.com/example.git. (Actually, any domain or subdomain that resolves to your server’s IP address will work, since git-daemon listens on port 9418 and doesn’t care about httpd’s configuration).

By default, git-daemon only allows read access to repositories. See man 1 git-daemon for further configuration options.

Markdown rendering and syntax highlighting

cgit can render Markdown files (such as README.md) and provide syntax highlighting for source code files with filters. Since httpd runs in a chroot, any programs and library dependencies used by those filters need to be copied into the chroot. While hard links could work (created with ln source target), they can’t span file systems, and by default on OpenBSD, /var, /usr, and /usr/local are all separate file systems (you can check with mount). So, copying it is.

Markdown rendering (for README.md)

I used lowdown since it has no external dependencies, but any Markdown to HTML converter will work, as long as the necessary files are copied into the chroot. For instance, the about-formatting.sh filter included with the cgit source code uses python-markdown (py3-markdown in OpenBSD).

However, note that shell scripts will require a shell, like /bin/sh, to be added to the chroot, which presents a security risk. It’s better to set the about filter to a static program that takes Markdown as input and outputs HTML. For whatever reason, lowdown won’t work when specified directly (see here for a discussion), but we can create a simple static program that calls the lowdown C API directly (as seen in the example section of man 3 lowdown_file) to convert Markdown to an HTML fragment:

#include <err.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/queue.h>
#include <lowdown.h>

int main(void) {
    struct lowdown_opts opts;
    char *buf;
    size_t bufsz;

    memset(&opts, 0, sizeof(struct lowdown_opts));
    opts.type = LOWDOWN_HTML;
    opts.feat = LOWDOWN_FOOTNOTES |
        LOWDOWN_AUTOLINK |
        LOWDOWN_TABLES |
        LOWDOWN_SUPER |
        LOWDOWN_STRIKE |
        LOWDOWN_FENCED |
        LOWDOWN_COMMONMARK |
        LOWDOWN_DEFLIST |
        LOWDOWN_IMG_EXT |
        LOWDOWN_METADATA;
    opts.oflags = LOWDOWN_HTML_HEAD_IDS |
        LOWDOWN_HTML_NUM_ENT |
        LOWDOWN_HTML_OWASP |
        LOWDOWN_SMARTY;
    if (!lowdown_file(&opts, stdin, &buf, &bufsz, NULL))
        errx(1, "lowdown_file");
    fwrite(buf, 1, bufsz, stdout);
    free(buf);
}

Put the above code in convert-markdown.c, then run the following:

pkg_add lowdown

cc convert-markdown.c -o convert-markdown -I/usr/local/include -L/usr/local/lib -llowdown -lm -static -pie
strip convert-markdown
mkdir -p /var/www/bin
cp convert-markdown /var/www/bin

cat >> /var/www/conf/cgitrc << EOF
# this script will run before rendering the about section of a repo
about-filter=/bin/convert-markdown
# the contents of this file are shown in the about section
readme=:README.md
EOF

Syntax highlighting (for source code)

Warning: this may cause source code pages to be very slow to render, even with caching.

pkg_add py3-pygments

cd /var/www/cgit/filters
ftp https://git.zx2c4.com/cgit/plain/filters/syntax-highlighting.py
sed -i "s|/usr/bin/env python3|/usr/local/bin/python3|" syntax-highlighting.py
chmod +x syntax-highlighting.py

cat >> /var/www/conf/cgitrc << EOF
source-filter=/cgit/filters/syntax-highlighting.py
EOF

mkdir -p /var/www/usr/bin
mkdir -p /var/www/usr/lib
mkdir -p /var/www/usr/libexec
mkdir -p /var/www/usr/local/lib

cp /usr/local/bin/python3 /var/www/usr/bin/
cp -r /usr/local/lib/python3.10 /var/www/usr/local/lib/
cp /usr/local/lib/libpython3.10.so.0.0 /usr/local/lib/libintl.so.8.0 /usr/local/lib/libiconv.so.7.1 /var/www/usr/lib/
cp /usr/lib/libpthread.so.27.1 /usr/lib/libutil.so.17.0 /usr/lib/libm.so.10.1 /usr/lib/libc.so.97.1 /var/www/usr/lib/
cp /usr/libexec/ld.so /var/www/usr/libexec/

The library version numbers may be different depending on the version of OpenBSD you’re running. By default, Python’s exec prefix is set to /usr/local, so it will look there for modules and site-packages. However, the dynamic libraries are fine to go in /usr/lib within the chroot.

This configuration uses cgit’s own Pygments-based syntax highlighting script. An alternative that uses highlight can be found here.