How Package Management Works on OpenBSD
OpenBSD uses a binary package system built on top of the ports tree. The primary tool is pkg_add, and it pulls from a mirror you set in the PKG_PATH environment variable. Unlike apt or dnf, there is no persistent package database daemon running in the background. Everything is file-based under /var/db/pkg/.
Set your mirror before doing anything else. The recommended approach is to export PKG_PATH in your shell profile or /etc/profile:
Once PKG_PATH is set, installing a package works exactly as you would expect. Searching is done with pkg_info -Q:
Upgrading all installed packages is a single command: pkg_add -u. This is the equivalent of apt upgrade or dnf update. In our testing on a minimal server install, pkg_add -u on a fresh 7.6 system with around 40 packages completes in under two minutes on a fast mirror.
Removing a package uses pkg_delete. Add -a to also remove automatically installed dependencies that are no longer needed, which is the behavior most Linux users expect by default:
One difference to internalize: OpenBSD does not have a concept of package pinning in the same sense as Debian. Packages are tied to a specific OpenBSD release. If you are running -current (the rolling development branch), your PKG_PATH must point to snapshots, not a stable release mirror. Mixing the two breaks things silently.
export PKG_PATH=https://cdn.openbsd.org/pub/OpenBSD/$(uname -r)/packages/$(uname -m)/
# Search for nginx
pkg_info -Q nginx
# Install nginx
pkg_add nginx
# Remove a package and orphaned dependencies
pkg_delete -a nginx
How rc.d and Service Management Works
OpenBSD does not use systemd. It uses a BSD-style init with rc.d scripts stored in /etc/rc.d/ for base system services and /usr/local/etc/rc.d/ for packages. There is no journald, no socket activation by default, and no unit files to write.
To enable a service at boot, add it to the pkg_scripts variable in /etc/rc.conf.local. This file does not exist on a fresh install; you create it. The pattern is clean and predictable:
To start, stop, restart, or check the status of a running service without rebooting, use rcctl:
rcctl also handles service flags, which is how you pass arguments to daemons. For example, to make sshd listen on a non-standard port without editing sshd_config, you can set flags, though editing the config file is the more portable approach:
In our experience, the most common mistake Linux admins make is editing /etc/rc.conf directly. That file is overwritten on system upgrades. Always use /etc/rc.conf.local for your changes.
There is no equivalent to systemctl status for detailed structured output. rcctl check servicename tells you if a service is running. For logs, OpenBSD uses syslog by default, and most package daemons log to /var/log/daemon. The base httpd daemon logs to /var/www/logs/ inside the chroot.
# /etc/rc.conf.local
pkg_scripts="nginx unbound redis"
# Start nginx now
rcctl start nginx
# Check status
rcctl check nginx
# Set flags for a daemon
rcctl set nginx flags "-c /etc/nginx/nginx.conf"
# Disable a service
rcctl disable nginx
Configuring pf Without Locking Yourself Out
pf is OpenBSD's packet filter and it is loaded via /etc/pf.conf. The default install includes a minimal pf.conf that passes all traffic. When you start writing rules, the single most important thing is to test before committing.
The correct workflow is: write your rules, check syntax with pfctl -nf, then load with pfctl -f. Do not reload pf over an interactive SSH session without a safety net. The classic approach is to schedule a cron job that flushes rules and reloads the permissive defaults in 5 minutes, giving yourself time to confirm the new rules work:
A minimal but functional server ruleset that allows SSH and HTTP/HTTPS, blocks everything else incoming, and passes all outbound traffic looks like this:
The set skip on lo line is critical. Without it, pf processes loopback traffic and breaks inter-process communication on the local machine.
For stateful connection tracking, pf keeps a state table. You can inspect it with pfctl -s state. On a busy server, this table can grow large. Set limits in pf.conf if you are running high-connection-count workloads:
OpenBSD's pf documentation in the man pages is genuinely good. Run man pf.conf before reading anything else. The OpenBSD FAQ on the project's official site covers pf in depth and is updated with each release.
# Safety net: reload permissive rules in 5 minutes
echo "pfctl -F all -f /etc/pf.conf.default" | at now + 5 minutes
# Check syntax without loading
pfctl -nf /etc/pf.conf
# Load rules
pfctl -f /etc/pf.conf
# Minimal server pf.conf
set skip on lo
block all
pass out
pass in on egress proto tcp to port { 22 80 443 } keep state
# State table limit for high traffic
set limit states 500000
Disk Layout, disklabel, and FFS2
OpenBSD does not use GPT by default on amd64, though it supports it. The installer uses MBR with OpenBSD's own disklabel to partition the OpenBSD slice. This is a frequent point of confusion for Linux admins expecting fdisk to show their partitions.
Use disklabel to view and modify the partition layout inside the OpenBSD MBR slice:
OpenBSD uses letters for partition names within disklabel: a is always root, b is always swap, c is the whole disk, and d onward are user partitions. This is not optional or configurable.
The default filesystem is FFS2, which is a modernized Fast File System with support for files larger than 2GB and soft updates for journal-like consistency. You will see it listed as ffs in disklabel output. OpenBSD also ships with support for ext2fs (read-only for Linux compatibility), NTFS, FAT, and ISO 9660.
For production servers, the installer's auto-layout is reasonable. It creates separate partitions for /usr, /tmp, /var, and /home by default, with nodev and nosuid mount flags applied where appropriate. You can see these in /etc/fstab after install:
If you need to add a new disk, run disklabel -E sd1 (replace sd1 with your device) to enter the interactive editor. Use 'a' to add partitions, 'w' to write, and 'q' to quit. Then create the filesystem with newfs and mount it.
# View partition layout
disklabel sd0
# Interactive edit
disklabel -E sd0
# Create FFS2 filesystem on new partition
newfs /dev/sd1d
# Mount
mount /dev/sd1d /mnt/data
# Check /etc/fstab entry format
# /dev/sd0d /var ffs rw,nodev,nosuid 1 2
User Management and doas Instead of sudo
OpenBSD replaced sudo with doas in the base system starting with OpenBSD 5.8. sudo is still available as a package, but the OpenBSD project recommends doas for most use cases. The configuration is simpler and the attack surface is smaller.
doas is configured in /etc/doas.conf. The syntax is minimal by design:
The persist keyword caches authentication for a short period, similar to sudo's timestamp behavior. nopass skips the password prompt entirely, useful for specific commands in automation.
For user creation, OpenBSD uses adduser (interactive) or useradd (scriptable). The default shell for new users is /bin/ksh, OpenBSD's public domain Korn shell, not bash. Bash is available via pkg_add bash but is not the default:
OpenBSD's login.conf file controls resource limits, password policies, and login class behavior. It is more granular than Linux's /etc/security/limits.conf. To give a specific user or class elevated file descriptor limits, edit /etc/login.conf and rebuild the database:
Group membership follows the same pattern as Linux. The wheel group grants doas/sudo access when configured in doas.conf. Add users to wheel with usermod -G wheel username.
# /etc/doas.conf examples
permit persist :wheel
permit nopass :wheel cmd /sbin/reboot
permit nopass keepenv { PKG_PATH } :wheel
# Create a user
useradd -m -G wheel -s /bin/ksh deploy
passwd deploy
# Rebuild login.conf database after edits
cap_mkdb /etc/login.conf
Network Configuration: Hostname.if Files
OpenBSD configures network interfaces through per-interface files in /etc, named hostname.if where 'if' is the interface name. For a machine with an em0 interface, the file is /etc/hostname.em0. There is no NetworkManager, no Netplan, no ip command in the Linux iproute2 sense.
A static IP configuration in hostname.em0 looks like this:
For DHCP, the file contains a single line. The DHCP client is dhclient, and it runs automatically if the hostname.if file contains 'dhcp':
The ifconfig command on OpenBSD is more capable than on Linux and handles everything from IP assignment to VLAN tagging to WiFi association. To bring up an interface temporarily without editing the hostname.if file:
Routing is managed through /etc/mygate for the default gateway and /etc/resolv.conf for DNS. On systems running unbound (the local DNS resolver, which is part of base), resolv.conf typically points to 127.0.0.1.
VLANs are configured by creating a hostname.vlan0 file and specifying the parent interface and VLAN ID. Bridges use hostname.bridge0. The pattern is consistent across all interface types, which makes it scriptable. If you are automating network provisioning across multiple OpenBSD machines, tools like Ansible with the openbsd_pkg module or custom scripts triggered via SSH work well. For teams building out more complex DevOps automation pipelines, platforms like taskbotshub.ai handle multi-step provisioning workflows including network configuration templating across BSD targets.
# /etc/hostname.em0 - static
inet 192.168.1.10 255.255.255.0
# /etc/hostname.em0 - DHCP
dhcp
# /etc/mygate
192.168.1.1
# Bring up interface manually
ifconfig em0 inet 192.168.1.10 netmask 255.255.255.0 up
# Add default route manually
route add default 192.168.1.1
# VLAN example in /etc/hostname.vlan0
vlan 10 vlandev em0
inet 10.0.10.5 255.255.255.0
System Updates: syspatch and sysupgrade
OpenBSD has two distinct update mechanisms. syspatch applies binary security patches to the running base system between releases. sysupgrade upgrades the entire system to the next release. Do not confuse them.
syspatch downloads and applies patches signed by the OpenBSD project. It is the equivalent of applying CVE fixes without a full OS upgrade:
In our testing, syspatch on a fresh 7.6 install with 5 pending patches completes in under 30 seconds. It updates binaries in place and requires a reboot only when the kernel itself is patched. Check whether a reboot is needed by looking at /var/syspatch/*/revert.
sysupgrade handles full version upgrades. It downloads the next release's install sets, verifies signatures, and reboots into the upgrade process automatically:
The -n flag runs a dry run. sysupgrade without flags upgrades to the next stable release. Use sysupgrade -s to upgrade to -current (snapshots), which is not recommended for production.
After a sysupgrade, run pkg_add -u to update all installed packages to their new versions for the upgraded release. Then run sysmerge to handle configuration file conflicts between the old and new base system:
sysmerge is interactive. It presents diffs between your installed config files and the new defaults. Accept, reject, or merge each conflict. On a heavily customized server, budget 15-20 minutes for this step.
# Apply security patches
doas syspatch
# List applied patches
syspatch -l
# Full release upgrade
doas sysupgrade
# Upgrade packages after OS upgrade
doas pkg_add -u
# Merge configuration files
doas sysmerge
Compiling from Ports vs Installing Binary Packages
The ports tree lives at /usr/ports/ and is not installed by default. You fetch it with CVS or from a tarball. For most production use, binary packages via pkg_add are faster and sufficient. Build from ports only when you need compile-time options not present in the binary package.
Fetch the ports tree for OpenBSD 7.6:
To build a port, cd to its directory under /usr/ports/ and run make install. Dependencies are handled automatically but may require significant disk space and build time. The /usr/ports/infrastructure/bin/dpb tool (distributed ports builder) handles parallel builds across multiple cores or machines.
A practical middle path: use pkg_add for most software and build from ports only for the one or two packages where you need custom flags. The ports tree and binary packages coexist cleanly because they use the same packaging format.
One real-world example: nginx in the binary package is built without the nginx-rtmp-module. If you need RTMP streaming support, you pull the net/nginx port, edit its Makefile or FLAVORS to include the module, and build locally. The result is installed into the same /usr/local/ prefix as binary packages.
# Fetch ports tree via FTP (OpenBSD 7.6)
cd /usr
ftp https://cdn.openbsd.org/pub/OpenBSD/7.6/ports.tar.gz
tar xzf ports.tar.gz
# Build and install a port
cd /usr/ports/www/nginx
make install
make clean
# Search for a port
cd /usr/ports
make search key=nginx
Common Gotchas When Running OpenBSD in Production
The /tmp directory is memory-backed with mfs (memory filesystem) on many configurations, which means it is wiped on reboot and size-limited. Do not use /tmp for build artifacts or large temporary files on a long-running server. Use /var/tmp instead, which persists across reboots.
OpenBSD's default umask is 022, same as Linux, but the base httpd daemon runs chrooted to /var/www. Any files your web application needs must exist inside that chroot. If you are deploying a custom application, you need to either use the chroot correctly or switch to nginx or Apache from packages, which can be configured outside the chroot.
OpenBSD enforces W^X (write XOR execute) at the kernel level. Memory pages cannot be both writable and executable simultaneously. Most modern runtimes (Go, Rust, Java) handle this correctly. Some older JVMs and JIT compilers require the mmap MAP_JIT flag or explicit pledge/unveil allowances. Test your runtime before deploying to production.
The kern.maxfiles and kern.maxproc sysctl values default conservatively. On a production server running many processes or connections, increase them in /etc/sysctl.conf:
If you are naming an internal OpenBSD-based service or project and need a clean domain to go with it, registering a memorable name through a service like nicename.me can simplify the process, particularly for short project names that are difficult to find available elsewhere.
Finally, OpenBSD's pledge(2) and unveil(2) system calls are used extensively in base system binaries. Third-party software may not be pledged yet, which is normal. If a process crashes with 'pledge violation', it means it attempted a syscall outside its declared policy. Check dmesg and /var/log/messages for the specific violation.
# /etc/sysctl.conf production tuning
kern.maxfiles=65536
kern.maxproc=8192
net.inet.ip.forwarding=1
# Apply without reboot
doas sysctl kern.maxfiles=65536
# Check pledge violations
dmesg | grep pledge
tail -f /var/log/messages | grep pledge