OpenBSD Virtual Server on Vultr Runbook

I started these notes in May 2016. Last revised January 2018.

This runbook describes a relatively simple OpenBSD web server set up as a small instance. We use Let’s Encrypt for SSL certificates, and Nginx to do SSL termination and as a reverse proxy.


OpenBSD 6.1 introduced syspatch, and official binary patch system for core. This eliminates the need for the third-party openup utility.

$ doas syspatch

To update non-core binary packages:

$ doas pkg_add -Uu

Initial VM Setup

Create a Vultr account, and fund it.

Add an ISO to Vultr, such as:

Install through the Vultr noVNC web console. We need to connect to a high port for noVNC to work.

Configure the new VM’s network via DHCP.

Use a custom disklabel partition scheme, since disk space is relatively tight on our VPS:

/        1G 4.2BSD
swap     1G swap
/usr     4G 4.2BSD
/home    2G 4.2BSD
/var     7G (the remainder) 4.2BSD

Since we don’t want to install X stuff, do -x* during set selection.

Add a user during install. The install should make that first user part of the wheel group.

After install, remove the ISO in the Vultr control panel. Servers -> Instance -> Server Information -> Settings -> Custom ISO -> Remove ISO



Read afterboot(8).

Set up doas (the OpenBSD sudo replacment) by editing /etc/doas.conf:

permit nopass keepenv { PKG_PATH ENV PS1 SSH_AUTH_SOCK } :wheel

(Omit the nopass if we want it to prompt for passwords.)

Add our client’s ssh keys to ~/.ssh/authorized_keys.

Generate an ssh key for our user:

$ ssh-keygen -b 4096

Disable password-based authentication by editing /etc/ssh/sshd_config:

PasswordAuthentication no

Reload the sshd config:

$ doas /etc/rc.d/sshd reload

Write a /etc/pf.conf:

# $OpenBSD: pf.conf,v 1.54 2014/08/23 05:49:42 deraadt Exp $
# See pf.conf(5) and /etc/examples/pf.conf
ext_if = "vio0"
tcp_pass_in = "{ 80 443 22 }"
tcp_pass_out = "{ 80 443 22 53 123 43 67 68 }"
udp_pass_out = "{ 53 123 67 68 }"
table <friendly_ip_addrs> { nnn.0.nnn.153, nnn.148.nnn.214, nn.49.nn.170 }
set skip on lo0
match in all scrub (no-df random-id max-mss 1440)
block in all
pass out all
pass in proto tcp to port $tcp_pass_in
pass in on $ext_if from <friendly_ip_addrs>

(Outbound should probably be more restrictive!)

Check the syntax and reload pf:

$ doas pfctl -nf /etc/pf.conf
$ doas pfctl -f /etc/pf.conf

Install the ports collection:

$ cd /tmp
$ ftp$(uname -r)/ports.tar.gz
$ ftp$(uname -r)/SHA256.sig
$ signify -Cp /etc/signify/openbsd-$(uname -r | cut -c 1,3) -x SHA256.sig ports.tar.gz
$ cd /usr
$ doas tar xzf /tmp/ports.tar.gz

(The ports tree should use less than 400 MB at install.)

Edit ~/.profile:

export PKG_PATH=$(uname -r)/packages/$(machine -a)/
export HISTSIZE=1000
export HISTFILE=$HOME/.history
export PS1='--- \! --- \h \w \$ '
export PAGER='less -gJ'
export LC_CTYPE=en_US.UTF-8
export LSCOLORS=gxfxcxdxbxxggdabagacad
alias l='ls -F'
alias ll='ls -Fl'
alias la='ls -Fa'
alias h='history -60 | sort -k2 | uniq -f2 | sort -bn'

After we set $PKG_PATH in our ~/.profile:

$ cd /usr/ports
$ make search key='^vim.*no_x11'
$ doas pkg_add vim-7.4.900-no_x11
$ doas pkg_add git pftop wget

(Note that package installation notes/warning/instructions are saved in /usr/local/share/doc/pkg-readmes/.)

Create ~/.tmux.conf:

set-option -g default-terminal "screen-256color"
set-option -g default-shell /bin/ksh
set -g status-bg "colour143"
set -g status-fg "black"
set -g prefix C-a
unbind C-b
bind-key C-a send-prefix
set -g history-limit 10000
set -g base-index 1
set-window-option -g mode-keys vi
bind-key -t vi-copy 'v' begin-selection
bind-key -t vi-copy 'y' copy-selection
unbind-key j
bind-key j select-pane -D
unbind-key k
bind-key k select-pane -U
unbind-key h
bind-key h select-pane -L
unbind-key l
bind-key l select-pane -R
bind-key -n M-F1 select-window -t :1
bind-key -n M-F2 select-window -t :2
bind-key -n M-F3 select-window -t :3
bind-key -n M-F4 select-window -t :4
bind-key -n M-F5 select-window -t :5
bind-key -n M-F6 select-window -t :6
bind-key -n M-F7 select-window -t :7
bind-key -n M-F8 select-window -t :8
bind-key -n M-F9 select-window -t :9

Set up mail alias to our user account. Edit /etc/mail/aliases:

root: paulgorman

and run:

$ doas newaliases

Web Server Setup

[These notes were revised in January 2018 when I started using Nginx as a reverse proxy for OpenBSD’s httpd.]

Set up Nginx to do SSL termination and reverse proxying:

$ doas pkg_add nginx

$ doas vim /etc/nginx/nginx.conf
worker_processes  3;

worker_rlimit_nofile 1024;
events {
	worker_connections  800;

http {
	include       mime.types;
	default_type  application/octet-stream;
	charset       utf-8;
	index         index.html index.htm;

	keepalive_timeout  65;

	server_tokens off;

	server {
		listen       80;
		listen       [::]:80;

		location ^~ /.well-known/acme-challenge/ {

		location / {
			return       301 https://$host$request_uri;

	server {
		listen       443;
		listen       [::]:443;

		ssl                  on;
		ssl_certificate      /etc/ssl/letsencrypt/fullchain.pem;
		ssl_certificate_key  /etc/ssl/letsencrypt/private/privkey.pem;
		ssl_session_timeout  5m;
		ssl_session_cache    shared:SSL:1m;
		ssl_ciphers  HIGH:!aNULL:!MD5:!RC4;
		ssl_prefer_server_ciphers   on;

		location / {
			proxy_set_header Host $http_host;

$ doas rcctl enable nginx
$ doas rcctl start nginx

Set up OpenBSD’s httpd (see httpd.conf(5) and /etc/examples/httpd.conf):

$ doas vim /etc/httpd.conf
#### Macros ####
#### Global Options ###
prefork 3
types {
	include "/usr/share/misc/mime.types"
#### Servers ####
server "" {
	root "/"
	listen on $ext_addr port 80
	directory {
		auto index,
		index "index.html"
	location "/" {
		block return 302 "/blog/"
	location "/index.html" {
		block return 302 "/blog/"
	location "*.php" {
		fastcgi socket "/run/php-fpm.sock"
	location "/ttrss/*" {
		authenticate with "/htpasswd"
		directory index "index.php"
	location "/.well-known/acme-challenge/*" {
		root {
			strip 2
server "" {
	root "/"
	listen on $ext_addr port 80
	directory {
		auto index,
		index "index.html"
	location "/" {
		block return 302 "/technical/blog/"
	location "/index.html" {
		block return 302 "/technical/blog/"
	location "*.php" {
		fastcgi socket "/run/php-fpm.sock"
	location "/blog/*" {
		directory index "index.php"
	location "/.well-known/acme-challenge/*" {
		root {
			strip 2
server "" {
	listen on $ext_addr port 80
	block return 301 "$REQUEST_URI"
server "" {
	listen on $ext_addr port 80
	block return 301 "$REQUEST_URI"

$ doas rcctl enable httpd
$ doas rcctl start httpd

PHP Setup

It seems that php-fpm was merged into the php packages in OpenBSD 5.9. Don’t choose the “-ap2” php packages; they’re for Apache.

$ cd /usr/ports
$ make search key='^php' | grep '^Port' | sort
$ doas pkg_add php-5.6.18 mariadb-server mariadb-client php-mysql-5.6.18 php-mcrypt-5.6.18 php-curl-5.6.18
$ doas rcctl enable php56_fpm

Enable MySQL and mcrypt and curl support in PHP:

$ doas ln -s /etc/php-5.6.sample/mysql.ini /etc/php-5.6/mysql.ini
$ doas ln -s /etc/php-5.6.sample/mcrypt.ini /etc/php-5.6/mcrypt.ini
$ doas ln -s /etc/php-5.6.sample/curl.ini /etc/php-5.6/curl.ini

Add to /etc/my.cnf:

expire_logs_days = 3
max_binlog_size = 25600000

Start php and restart httpd:

$ doas rcctl start php56_fpm
$ doas rcctl restart httpd

Let’s Encrypt SSL

[As of OpenBSD 6.0 (late 2016) the below stuff, especially Let’s Encrypt, is changing. Until I can revise these notes, see]

Install Let’s Encrypt:

$ doas pkg_add letsencrypt
$ doas letsencrypt certonly --webroot \
-w /var/www/ -d -d \
-w /var/www/ -d -d -d

The certs are saved under /etc/letsencrypt/live/

Tiny Tiny RSS

Generate a test htpasswd file. The file must be readable by the webserver user.

$ doas htpasswd /var/www/htpasswd myuser
$ doas chown root:www /var/www/htpasswd
$ doas chmod 640 /var/www/htpasswd


$ doas /usr/local/bin/mysql_install_db
$ doas rcctl enable mysqld
$ doas rcctl start mysqld
$ doas /usr/local/bin/mysql_secure_installation

Install tt-rss.

$ mysql -uroot -p
MariaDB [(none)]> CREATE DATABASE ttrss;
MariaDB [(none)]> CREATE USER 'ttrss'@'' IDENTIFIED BY '*******************';
MariaDB [(none)]> GRANT ALL ON ttrss.* TO 'ttrss'@'';
$ mysql -uroot -p ttrss < ttrss.sql
$ tar -zxf ttrss-html.tgz
$ doas mv ttrss /var/www/
$ doas chown -R root:daemon /var/www/
$ doas htpasswd /var/www/htpasswd myuser
$ doas chown root:daemon /var/www/htpasswd
$ doas chmod g+r /var/www/htpasswd
$ doas mkdir /var/www/

(Use “” rather than “localhost”. uses TCP sockets; localhost uses unix sockets. Unix sockets would be more efficient, but needs a lot more configuration because httpd is chroot’d. We’d have to tell MySQL to write its socket file to /var/www/run/mysql/mysql.sock, and tell PHP to look there.)

Set up a cron job for the www user to update the tt-rss feeds:

$ doas crontab -u www -e

*/89  *   *    *    *   cd /var/www/ && /usr/local/bin/php-5.6 /var/www/ --feeds --qui

et >/dev/null 2>&1

(Sigh. This will break when the php version changes, but I don’t see an obvious way I can avoid it.)

For convenience:

$ doas chown -R paulgorman:www /var/www/
$ doas chown -R paulgorman:www /var/www/


Set up backups. Create ~/bin/

BOX=$(hostname -s)
DAY=$(date +'%d')
/usr/local/bin/mysqldump -u root --password='***********************' --all-databases > /var/backups/${BOX}-alldatabases.sql
chmod 640 /var/backups/${BOX}-alldatabases.sql
tar -czf /var/backups/backup-${BOX}-${DAY}.tgz \
/etc \
/var/cron \
/var/www/ \
/var/www/ \
/var/www/cgi-bin \
/var/www/htpasswd \
/var/backups/${BOX}-alldatabases.sql \
/home/paulgorman/.ssh \
/var/repo \
chmod 640 /var/backups/backup-${BOX}-${DAY}.tgz

Create root’s cron jobs:

1  3  2,16  *  *    /home/paulgorman/bin/

One the remote machine, we create a cron job to pull the backup:

1 0 3,17 * * scp\*tgz /data/share/backups/


Create home for git repositories:

$ doas mkdir /var/repo
$ doas chown paulgorman:paulgorman /var/repo/
$ ln -s /var/repo /home/paulgorman/repo

General OpenBSD Stuff

Use rcctl(8) to enable and disable services. rcctl get foo will show what rc knows about service foo.

Get info on current pf state:

$ doas pfctl -sr
$ doas pfctl -ss
$ doas pfctl -sa

Finding packages:

$ cd /usr/ports && make search key='foo' | grep 'Port:' && cd -
$ pkg_info -Q foo

(The first searches all package field for the key; the second only searches package names.)