worrbase

Setting up an OpenBSD Gitlab runner

2020-12-18

I think one of the larger problems I've had when writing software has been doing proper testing CI on OpenBSD, which for me, obviously is a problem. Much of the time, I end up either just doing CI on Linux or not setting up CI at all.

There are a few popular CI options available:

  • Travis CI (supports linux and macos)
  • Appveyor (supports Windows, linux and macos)
  • GitHub Actions (supports Windows, linux and macos)
  • GitLab (officially supports Windows, linux, macos and FreeBSD)

Finally, I should mention that there is one platform that does absolutely solve my problem, definitely better than I will in this blog post. That's Sourcehut! They officially have machines for a variety of Linux platforms, FreeBSD, OpenBSD and 9front, and on a few different architectures as well!

I'm on the fence about sr.ht at the moment, and in general I'm pretty fond of GitLab, so I wanted to tackle the issue of having some degree of OpenBSD testing through GitLab.

NB: It's worth noting that the setup that I'm going to describe isn't particularly secure, and doesn't provide build isolation. CI is quite literally remote code execution as a service, so the setup I'm going to describe is not suitable for colocation with anything else.

Getting an OpenBSD VM

This isn't particularly related to the guide, but if you're looking for a hosting platform that supports OpenBSD, I've enjoyed Vultr quite a bit. I've had fairly good experiences with their support and had p good uptime with my VMs. They also offer cheap $2.50/mo, IPv6-only VMs (yes, GitLab and the GitLab runner have full IPv6 support, so this is an option).

I should also mention OpenBSD Amsterdam, which is run by OpenBSD fans and makes monthly donations to the OpenBSD project with the proceeds.

Building the runner

Building is simple. We're gonna deviate from the official instructions a bit, because we don't need the helper images or anything.

$ doas pkg_add bash gmake
$ git clone https://gitlab.com/gitlab-org/gitlab-runner
$ cd gitlab-runner
$ BUILD_PLATFORMS='-osarch openbsd/$(arch -s) gmake runner-bin

The runner binary will be in out/binaries/gitlab-runner-openbsd-$(arch -s)

Machine setup

I'm assuming you're starting with a completely fresh install.

First, install the following packages:

$ doas pkg_add bash git git-lfs curl jq

Despite GitLab's docs saying they support sh, they only support an sh that supports long options (--login), so we have to use bash here.

We're going to run the runner (and our builds) as a separate user, not as root. There's no great way to do proper isolation of arbitrary binaries on OpenBSD outside of vmd, which I don't believe supports nested virtualization. We could run this in a chroot, but unfortunately it'd be a pretty loaded chroot, since the deps for all of our builds would also live in the chroot.

$ doas useradd -s /sbin/nologin -m gitlab

If you have other users, I'd recommend making their home directories unreadable to others.

$ doas chmod -R o-rx /home/*

Resources

To reduce the impact of abuse, we're gonna try and restrict the usable resources a bit. We're going to modify /etc/login.conf.

The following values are specific for my use-case, and won't necessarily be applicable for your system.

gitlab:\
	:coredumpsize=0:\  # prevent coredumps
	:filesize=30M:\    # largest file can be 30M
	:maxproc=50:\      # max 50 processes
	:datasize=1G:\     # 1G heap per proc
	:stacksize=4M:\    # 4M stack per proc
	:openfiles=512:\   # 512 open files per process
	:tc=default:       # inherit other values from `default` class

Since I'm on Vultr, and they have a weird partition scheme where /home isn't its own partition, I'm going to set up filesystem quotas for gitlab.

# edit `fstab` to add the `quota` option to `/`
$ doas mount -o update,quota /
$ doas edquota
$ doas quotaon
$ doas -u gitlab quota

Network

We're also going to restrict network access for the gitlab user. We'll do this through adding the following lines to our /etc/pf.conf

table <github> file "/etc/pf/github"
table <allowed_hosts> const { gitlab.com crates.io static.crates.io }

block out log proto {tcp udp} user gitlab                     # by default block outgoing traffic and log to pflog0
block in proto {tcp udp} user gitlab                          # block any incoming traffic
pass out proto {tcp udp} to 127.0.0.1 port 53 user gitlab     # pass dns traffic
pass out proto tcp to <allowed_hosts> port 443 user gitlab
pass out proto tcp to <github> port 443 user gitlab

I'm primarily writing rust, so I just need the three hosts in the <allowed_hosts> table, as well as whatever IPs github is using that day. Part of the reason I log blocked outgoing calls, is that if a build fails due to a network issue, it makes it a lot easier to figure out what new rules you may have to add to your pf.conf to get builds flowing again.

To build the github table, I run the following script in an hourly cronjob:

#!/bin/sh

/usr/local/bin/curl -sSH "Accept: application/vnd.github.v3+json" https://api.github.com/meta |
	/usr/local/bin/jq --raw-output '.api+.git+.web|sort|join("\n")' > /etc/pf/github
/sbin/pfctl -T replace -t github -f /etc/pf/github

Finally, I also add the following to /etc/ssh/sshd_config to prevent this user from logging in via ssh.

DenyUsers gitlab

Installing the runner

To install, copy the binary into /usr/local/bin. I'd recommend keeping it out of the home directory, so it's harder for a rogue job to overwrite.

I drop the following config file in /etc/gitlab/config.toml

concurrent = 1
check_interval = 3
log_level = "info"

[session_server]
  session_timeout = 1800

[[runners]]
  name = "openbsd-6.8"
  url = "https://gitlab.com/"
  token = "<secret>"
  executor = "shell"
  shell = "bash"
  [runners.custom_build_dir]
  [runners.cache]
    [runners.cache.s3]
    [runners.cache.gcs]

I also have an rc script to start it

#!/bin/ksh

daemon="/usr/local/bin/gitlab-runner-openbsd-amd64"
daemon_class="gitlab"
daemon_user="gitlab"
daemon_flags="run -c /etc/gitlab/config.toml"
daemon_timeout=60

. /etc/rc.d/rc.subr

pexp="${daemon} ${daemon_flags}"

rc_bg="YES"
rc_reload=NO

rc_pre() {
	# There are some interesting network timeouts
	# if I don't refresh the pf rules
	/sbin/pfctl -f /etc/pf.conf
}

rc_start() {
	${rcexec} "${daemon} ${daemon_flags}" >> /var/log/gitlab-runner.log 2>&1 &
}

rc_cmd $1

Following all of this, you can just follow the normal gitlab instructions.

Conclusion

Does this work?

Well, yeah.

The big problem is that I wouldn't necessarily call this secure by any means. At the end of the day, users submitting PRs to my project that use this runner are still able to run arbitrary code on my gitlab runner. All CI/CD is remote code execution as a service. Even with the steps taken above, there are still ripe opportunities for abuse.

I definitely don't feel comfortable integrating this machine with my personal infrastructure. I'm certainly not setting it up to forward mail to my mail servers, since that'd require a key on the box. For now, this is roughly the best I think I'll do.

What would be better?

Ephemeral VMs (also with similar settings) would be the ideal here. If vmd supported nested VMs, it'd be awesome to add vmd support to the gitlab runner and launch all of my jobs in individual VMs. Alas, as far as I'm aware, this would only be possible if I had a bare metal machine handy. VirtualBox on a non-OpenBSD platform could also be a fair option worth exploring.

For right now, this works for my case, is cheap enough, and this VM is essentially ephemeral to me and easily trashed if I need to replace it for any reason.

Feel free to reach out if you think that there are ways to improve on this setup, or if I got something super wrong.