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.
▶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.