🐡 OpenBSD vs Linux

What is the same, what is different and what is simply missing, on a real OpenBSD 8.0 machine: services, packages, privileges, disks and network, with a Linux-to-OpenBSD table to keep.

Video: OpenBSD vs Linux (OpenBSD video)
Prefer to watch? This short video runs this page's main commands on a real OpenBSD 8.0 machine, with on-screen captions and a synthetic voiceover (an AI voice trained on the author's own recording). Click to play, or watch it on YouTube.
rcctl enable httpd             # start at boot
rcctl start httpd              # start it now
pkg_add tree                   # install a package

If you know Linux you already know most of OpenBSD: a Unix shell, the same everyday commands, files in the same places. This page covers the parts that are different, in the order you meet them: the manual, services without systemd, packages, doas instead of sudo, the secure defaults, disks and the network files.

Every example ran for real on OpenBSD 8.0 (amd64) in a virtual machine, as an ordinary user with doas. The output you see is the output the machine gave.

What OpenBSD is, and the lab

OpenBSD is a complete operating system, not a kernel with a distribution around it: the kernel, the C library, the shell, the network tools, the firewall (pf), the web server (httpd), the mail server (smtpd), the SSH server and the package tools are all written and released together by one project, once every six months. Its first goal is to be correct and secure by default, and it prefers to remove a feature rather than ship it half-trusted.

Every example below ran for real on OpenBSD 8.0 (amd64) in a virtual machine, as an ordinary user called bsd. Where a command needs root it starts with doas, which is the OpenBSD answer to sudo. If you know Linux, most of this will feel familiar, with a few surprises. This page shows the surprises.

$ uname -srm
OpenBSD 8.0 amd64
$ sysctl kern.version | head -2
kern.version=OpenBSD 8.0 (GENERIC.MP) #84: Tue Oct  6 23:13:49 MDT 2026
    deraadt@amd64.openbsd.org:/usr/src/sys/arch/amd64/compile/GENERIC.MP
$ echo $SHELL
/bin/ksh
$ id
uid=1000(bsd) gid=1000(bsd) groups=1000(bsd), 0(wheel)

The login shell is ksh, the OpenBSD Korn shell, not bash. Everyday commands work the same, and bash is a package away if you want it. The user is in the group wheel, which is what lets doas work.

The manual is part of the system

The first difference you will notice is the documentation. On OpenBSD the manual pages are written together with the code, and a change that breaks a page is a bug. Everything has a page, including how to set up the system after the install:

$ man -k pledge | head -3
open, __pledge_open, openat(2) - open or create a file for reading or writing
pledge(2) - restrict system operations
OpenBSD::Pledge(3p) - Perl interface to OpenBSD pledge(2)
$ man -w afterboot
/usr/share/man/man8/afterboot.8
$ man afterboot | col -b | sed -n 1,12p
AFTERBOOT(8)		    System Manager's Manual		  AFTERBOOT(8)

NAME
     afterboot - things to check after the first complete boot

DESCRIPTION
   Starting out
     This document attempts to list items for the system administrator to
     check and set up after the installation and first complete boot of the
     system.  The idea is to create a list of items that can be checked off so
     that you have a warm fuzzy feeling that something obvious has not been
     missed.  A basic knowledge of Unix is assumed, otherwise run:

afterboot(8) is a checklist for a fresh install, and it is worth reading once. Section numbers are the usual ones: (1) commands, (5) file formats, (8) administration. To search by keyword use man -k, which is the same as apropos.

The base system is one thing

On Linux you assemble a system from parts that have different authors. On OpenBSD one source tree builds the kernel and almost everything you run. The practical result is that the version number is the version of everything:

$ uname -r
8.0
$ sysctl -n kern.osrelease kern.osversion
8.0
GENERIC.MP#84
$ ls -l /bsd /bsd.rd | cut -c1-60
-rwx------  2 root  wheel  33460196 Nov  4 20:55 /bsd
-rw-------  1 root  wheel   4897648 Nov  4 20:54 /bsd.rd
$ file /bin/ls | cut -c1-70
/bin/ls: ELF 64-bit LSB shared object, x86-64, version 1

/bsd is the kernel. The name in kern.osversion, GENERIC.MP, tells you it is the multiprocessor build, which the installer picks on a machine with more than one core. /bsd.rd is a RAM disk kernel with the installer in it, which is also what you boot to upgrade or to recover. The commands in /bin are the same release, there is no second package manager for the base system.

Services without systemd: rcctl

OpenBSD has no systemd. Services start from the plain shell scripts in /etc/rc.d, and one small command, rcctl, is the interface to them. First, what is running on a fresh install:

$ doas rcctl ls started
cron
dhcpleased
ntpd
pflogd
resolvd
slaacd
smtpd
sndiod
sshd
syslogd
$ ps -ax -o pid,user,command | sed -n 1,14p
  PID USER     COMMAND
    1 root     /sbin/init
22851 root     /sbin/slaacd
93168 _slaacd  slaacd: engine (slaacd)
85110 _slaacd  slaacd: frontend (slaacd)
30294 root     /sbin/dhcpleased
90205 _dhcp    dhcpleased: engine (dhcpleased)
47743 _dhcp    dhcpleased: frontend (dhcpleased)
95864 root     /sbin/resolvd
98830 root     syslogd-parent: [priv] (syslogd-parent)
58578 _syslogd /usr/sbin/syslogd
65419 root     pflogd: [priv] (pflogd)
90466 _pflogd  pflogd: [running] -s 160 -i pflog0 -f /var/log/pflog (pflogd)
 3366 _ntp     ntpd: ntp engine (ntpd)

Everything on that list belongs to the base system: init, the network helpers (slaacd, dhcpleased, resolvd), the logger, the packet-filter logger and the time daemon. Each daemon runs as its own unprivileged user, and a root-owned parent process hands out only what it must. A fresh machine listens on very little:

$ netstat -an -f inet | grep LISTEN
tcp          0      0  127.0.0.1.25           *.*                    LISTEN
tcp          0      0  *.22                   *.*                    LISTEN

Port 22 is sshd. Port 25 is smtpd, but it listens only on 127.0.0.1, so the outside world cannot reach it. Now start something. httpd is the OpenBSD web server, in the base system. It needs a configuration file first:

$ cat /tmp/httpd.conf
server "default" {
    listen on * port 80
    root "/htdocs"
}
$ doas cp /tmp/httpd.conf /etc/httpd.conf
$ doas rcctl enable httpd
$ doas rcctl start httpd
httpd(ok)
$ rcctl check httpd
httpd(ok)
$ rcctl get httpd
httpd_class=daemon
httpd_execdir=
httpd_flags=
httpd_logger=
httpd_rtable=0
httpd_timeout=30
httpd_user=root

enable makes it start at boot, start starts it now, check says whether it is running and get shows the flags. There is no unit file. The setting for the boot lives in one line of /etc/rc.conf.local:

$ cat /etc/rc.conf.local
httpd_flags=

httpd runs in a chroot at /var/www, so its root "/htdocs" is really /var/www/htdocs. Put a page there and fetch it:

$ doas sh -c 'echo "hello from OpenBSD" > /var/www/htdocs/index.html'
$ ftp -o - http://127.0.0.1/ 2>&1
hello from OpenBSD
$ netstat -an -f inet | grep LISTEN
tcp          0      0  *.80                   *.*                    LISTEN
tcp          0      0  *.22                   *.*                    LISTEN
tcp          0      0  127.0.0.1.25           *.*                    LISTEN
$ doas rcctl stop httpd
httpd(ok)
$ doas rcctl disable httpd

ftp is the OpenBSD download tool, it speaks http and https and has been doing that for decades, so it is the first thing to reach for when there is no curl. To remove a service completely, stop it, disable it and delete the configuration.

Packages: pkg_add

Software that is not in the base system comes as packages, installed with pkg_add. The mirror is set in one file:

$ cat /etc/installurl
https://cdn.openbsd.org/pub/OpenBSD
$ doas pkg_add -I tree
quirks-7.227 signed on 2026-11-04T15:24:59Z
$ tree -d -L 1 /var | head -4
/var
|-- account
|-- agentx
|-- audit
$ pkg_info | sed -n 1,5p
quirks-7.227        exceptions to pkg_add rules
tree-0.62           print ascii formatted tree of a directory structure
updatedb-0p0        pkg_add speed up cache
vmm-firmware-1.16.3p12 SeaBIOS and EDK II binary images for vmd(8)
$ pkg_info -E /usr/local/bin/tree
/usr/local/bin/tree: tree-0.62
tree-0.62           print ascii formatted tree of a directory structure

-I means non-interactive, just like -y elsewhere. pkg_info lists what is installed, and -E asks which package a file belongs to. To find a package you have not installed, use pkg_info -Q, which searches the mirror:

$ pkg_info -Q htop | head -3
debug-htop-3.5.3
htop-3.5.3
$ doas pkg_delete tree

The packages are built for the release you run, so you do not mix releases. Updates are one command, pkg_add -u, and the next page in this corner covers the full routine of updating the base system as well as the packages.

doas instead of sudo

OpenBSD does not ship sudo. It ships doas, a program with one configuration file and about a tenth of the features. The file is /etc/doas.conf, and only root can read it. In this lab the user bsd is allowed to run anything without a password, so the examples can run unattended:

$ doas cat /etc/doas.conf
permit nopass bsd
permit nopass keepenv root
$ doas id
uid=0(root) gid=0(wheel) groups=0(wheel), 2(kmem), 3(sys), 4(tty), 5(operator), 20(staff), 31(guest)

On a normal install the rule is permit keepenv :wheel, which lets anyone in the group wheel use doas with their own password. Each rule is: permit or deny, some options, who, and optionally as which user and which command. A tighter rule allows one user to restart one service and nothing else. Here is such a rule, in a file of its own, checked without installing it:

$ cat /tmp/test-doas.conf
permit nopass alice as root cmd rcctl args restart httpd
$ doas -C /tmp/test-doas.conf && echo "the test rule file is valid"
the test rule file is valid

The check command, doas -C file, parses a file without running anything, so use it on a new rule file before you put it in place. A syntax error in /etc/doas.conf is a good way to lose root, and doas -C is how you avoid that.

Secure by default

A few things that OpenBSD does without being asked, and that you can look at:

$ sysctl kern.securelevel
kern.securelevel=1
$ sysctl hw.ncpu
hw.ncpu=2
$ ulimit -a | sed -n 1,6p
time(cpu-seconds)    unlimited
file(blocks)         unlimited
coredump(blocks)     unlimited
data(kbytes)         1572864
stack(kbytes)        4096
lockedmem(kbytes)    87381
$ doas sysctl kern.securelevel=0
sysctl: kern.securelevel: Operation not permitted
$ mount | grep /usr
/dev/sd0f on /usr type ffs (local, nodev)
/dev/sd0g on /usr/X11R6 type ffs (local, nodev)
/dev/sd0h on /usr/local type ffs (local, nodev, wxallowed)
/dev/sd0j on /usr/obj type ffs (local, nodev, nosuid)
/dev/sd0i on /usr/src type ffs (local, nodev, nosuid)

kern.securelevel=1 means that once the machine has booted, even root cannot lower the level, clear the immutable flag on a file, or write to raw disk devices and to kernel memory. Root tried above, and was refused. That is one of the reasons an attacker with a root shell has less room.

Two features show up in the manual pages for almost every program. pledge(2) lets a program promise the kernel what it will do, for example "read files and print, nothing else". If it breaks the promise, the kernel kills it. unveil(2) hides the rest of the file system from a program except the paths it names. Most of the base system uses both. The pledge page is a good place to start:

$ man pledge | col -b | sed -n 1,16p
PLEDGE(2)		      System Calls Manual		     PLEDGE(2)

NAME
     pledge - restrict system operations

SYNOPSIS
     #include <unistd.h>

     int
     pledge(const char *promises, const char *execpromises);

DESCRIPTION
     The pledge() system call separates the POSIX feature set into a group of
     approximately 3 dozen subsystems.	By calling pledge() the program can
     declare which subsystems it will need in the future in a space-separated
     string called promises.  Subsystems not listed become unavailable, and

Memory is another default. Memory is either writable or executable, never both (W^X), which is why one of the mount lines above has a flag called wxallowed. Only /usr/local has it, because some packaged programs, such as browsers, need both. Every other partition refuses. Addresses are randomised, and malloc puts guard pages around allocations, so many bugs that go unnoticed on other systems crash right away here. For a user this means a program with a hidden memory bug will fail loudly on OpenBSD, which is the point.

Disks and file systems

Disks are named after the driver: sd0 for a SCSI or virtio disk, wd0 for an IDE disk, nvme0 and so on. Partitions are not numbered, they are lettered inside one OpenBSD slice, and the layout is called a disklabel:

$ df -h
Filesystem     Size    Used   Avail Capacity  Mounted on
/dev/sd0a      924M    106M    772M    13%    /
/dev/sd0k      5.5G   20.0K    5.3G     1%    /home
/dev/sd0d      1.3G   14.0K    1.3G     1%    /tmp
/dev/sd0f      2.9G    1.8G    992M    66%    /usr
/dev/sd0g      836M    2.0K    794M     1%    /usr/X11R6
/dev/sd0h      3.3G    3.5M    3.1G     1%    /usr/local
/dev/sd0j      8.4G    2.0K    7.9G     1%    /usr/obj
/dev/sd0i      2.2G    2.0K    2.1G     1%    /usr/src
/dev/sd0e      2.1G    6.5M    1.9G     1%    /var
$ doas disklabel sd0 | grep -E '^ +[a-p]:|duid'
duid: e33282234d106d96
  a:          1957664               64  4.2BSD   2048 16384 12960 # /
  b:          3464800          1957728    swap                    # none
  c:         62914560                0  unused
  d:          2886528          5422528  4.2BSD   2048 16384 12960 # /tmp
  e:          4455072          8309056  4.2BSD   2048 16384 12960 # /var
  f:          6372960         12764128  4.2BSD   2048 16384 12960 # /usr
  g:          1776704         19137088  4.2BSD   2048 16384 12960 # /usr/X11R6
  h:          7048576         20913792  4.2BSD   2048 16384 12960 # /usr/local
  i:          4854496         27962368  4.2BSD   2048 16384 12960 # /usr/src
  j:         18097600         32816864  4.2BSD   2048 16384 12960 # /usr/obj
  k:         12000096         50914464  4.2BSD   2048 16384 12960 # /home
$ mount | sed -n 1,3p
/dev/sd0a on / type ffs (local)
/dev/sd0k on /home type ffs (local, nodev, nosuid)
/dev/sd0d on /tmp type ffs (local, nodev, nosuid)

Each letter is one partition and has its own mount point: a is the root, b is swap, c is the whole disk and the other letters are what you see in the comments. The default layout is a set of separate partitions (/var, /usr, /home and more) because it limits what a full disk or a runaway program can damage, and each one gets its own mount options such as nodev and nosuid. The file system is FFS (also known as UFS), and the options are in /etc/fstab:

$ cat /etc/fstab | cut -c1-80
e33282234d106d96.b none swap sw
e33282234d106d96.a / ffs rw 1 1
e33282234d106d96.k /home ffs rw,nodev,nosuid 1 2
e33282234d106d96.d /tmp ffs rw,nodev,nosuid 1 2
e33282234d106d96.f /usr ffs rw,nodev 1 2
e33282234d106d96.g /usr/X11R6 ffs rw,nodev 1 2
e33282234d106d96.h /usr/local ffs rw,wxallowed,nodev 1 2
e33282234d106d96.j /usr/obj ffs rw,nodev,nosuid 1 2
e33282234d106d96.i /usr/src ffs rw,nodev,nosuid 1 2
e33282234d106d96.e /var ffs rw,nodev,nosuid 1 2

The fstab uses DUIDs, the long hexadecimal disk identifiers, rather than device names, so the disk can be found whatever its name is on the next boot.

Network configuration

There is no NetworkManager and no netplan. The configuration of each interface is a file called /etc/hostname.<interface>, and the machine name is in /etc/myname:

$ cat /etc/myname
obsd.my.domain
$ cat /etc/hostname.vio0
inet autoconf
inet6 autoconf
$ ifconfig vio0 | grep -E 'status|inet '
	status: active
	inet 10.0.2.15 netmask 0xffffff00 broadcast 10.0.2.255
$ route -n show -inet | sed -n 1,6p
Routing tables

Internet:
Destination        Gateway            Flags   Refs      Use   Mtu  Prio Iface
default            10.0.2.2           UGS        5       23     -     8 vio0
224/4              127.0.0.1          URS        0        0 32768     8 lo0
$ cat /etc/resolv.conf
nameserver 10.0.2.3 # resolvd: vio0
lookup file bind

inet autoconf means DHCP. A static address is one line: inet 192.0.2.10 255.255.255.0. The default gateway for a static setup goes in /etc/mygate, and the DNS servers in /etc/resolv.conf. To apply the files without a reboot, run sh /etc/netstart vio0 as root. ifconfig changes made by hand do not survive a reboot, and the hostname files do.

Linux to OpenBSD, side by side

A short table to keep. The left side is what your fingers know, the right side is what to type.

  Linux                          OpenBSD
  systemctl enable --now X       rcctl enable X; rcctl start X
  systemctl status X             rcctl check X
  apt install X / dnf install X  pkg_add X
  apt search X                   pkg_info -Q X
  apt upgrade                    pkg_add -u (and syspatch for the base)
  sudo                           doas
  ip addr / ip route             ifconfig / route -n show
  ss -tlnp                       netstat -an -f inet | grep LISTEN
  journalctl                     less /var/log/messages
  iptables / nftables            pf (pfctl)
  /etc/sysctl.conf               /etc/sysctl.conf (the same name)
  man -k                         man -k (the same)
  curl                           ftp -o - URL
  free / vmstat                  vmstat / top
  lsblk                          sysctl hw.disknames; disklabel sd0

Three habits that help. Read afterboot(8) once. Look for the man page before you look for a web page, because on OpenBSD the page is usually correct. And do not look for the option that turns a protection off, it is usually not there on purpose.

When something goes wrong

Three errors you will meet in the first week, with the real messages.

A package that does not exist, and a command that is not installed. pkg_info -Q prints nothing when the search finds no match, and its exit status is 0 all the same, so a script has to look at the output, not at the status:

$ pkg_info -Q nosuchpackage-xyz; echo "exit status $?"
exit status 0
$ nosuchcommand 2>&1
ksh: nosuchcommand: not found
(exit status 127)

A command that needs root:

$ rcctl enable httpd 2>&1 | head -1
rcctl: "enable" needs root privileges
$ syspatch -c 2>&1 | head -1
syspatch: need root privileges

Both tools tell you plainly that they need root privileges. Put doas in front of them. A file you cannot read for the same reason:

$ cat /etc/doas.conf 2>&1 | head -1
cat: /etc/doas.conf: Permission denied

If the machine will not boot after a change, boot the /bsd.rd RAM disk from the boot prompt (type boot bsd.rd), choose (S)hell, and mount the root partition to fix the file. And if you forget the root password, the same RAM disk can be used to set a new one.