Before You Start: Hardware and Hosting Choices
OpenBSD runs on amd64, arm64, i386, and a dozen other platforms. For server work, amd64 is the safe choice. Hardware support is narrower than Linux - check the 7.6 platform page before buying anything exotic. Broadcom NICs, some Realtek variants, and certain RAID controllers are unsupported or poorly supported.
For cloud, your options are limited but workable. Vultr offers OpenBSD as a first-class OS image at https://vultr.com/?ref=PLACEHOLDER - their $6/month shared CPU instance (1 vCPU, 1GB RAM) is enough for a firewall, a small web server, or a bastion host. We used their Amsterdam region for this guide. Avoid providers that only offer KVM with broken VirtIO drivers; OpenBSD's vio(4) driver covers VirtIO NICs correctly on Vultr.
Minimum disk for a base install is under 1GB, but allocate at least 20GB if you plan to run ports or build from source. OpenBSD's default disklabel layout separates /usr, /var, /tmp, and /home onto separate partitions with nodev/nosuid flags - do not flatten this into one partition to save effort.
# Check your NIC is supported before provisioning
dmesg | grep -E '^(em|bge|vio|re|ixl|ix)[0-9]'
Installation: The Installer Is Faster Than You Think
Boot from the install76.iso image. The OpenBSD installer is a shell script driven by questions - not ncurses, not a GUI. It runs in under 10 minutes on a VPS if you know the answers ahead of time.
Key decisions during install:
1. Disk layout: Accept the default auto-layout unless you have specific needs. The auto-layout creates separate partitions with correct mount options. On a 20GB disk it produces roughly: / (1G), swap (2x RAM), /tmp (2G), /var (4G), /usr (6G), /usr/local (3G), /home (remainder).
2. Sets: For a server, deselect xbase, xfont, xshare, xserv, xman - these are X11 components. Select bsd.mp (multiprocessor kernel) if your host has more than one CPU. The game sets (games76.tgz) are 2.7MB and harmless but skip them anyway.
3. Root password: Set it. You will disable root SSH login immediately after.
4. Network: The installer configures one interface. On Vultr, it is vio0. Enter the IP, netmask, and gateway from the Vultr control panel. Set the hostname to your FQDN now - changing it later requires editing /etc/myname and /etc/hosts.
After the installer finishes and reboots, you are in at the console. First action: confirm you are running the right kernel.
uname -a
# Expected output example:
# OpenBSD hostname.example.com 7.6 GENERIC.MP#1 amd64
# Confirm sets installed
ls /var/db/installed.db
First Boot: Users, Doas, and Disabling Root SSH
OpenBSD ships with sudo removed. The replacement is doas(1), a 300-line C program with a simpler config format. Configure it before you do anything else.
Create your admin user and configure doas:
# Add admin user
useradd -m -G wheel -s /bin/ksh admin
passwd admin
# Configure doas - /etc/doas.conf
echo 'permit persist :wheel' > /etc/doas.conf
# Verify doas works before locking root
doas -C /etc/doas.conf && echo 'config ok'
# Test as admin user
su - admin
doas id
# Should return: uid=0(root) gid=0(wheel) ...
SSH Hardening: What OpenBSD Does by Default and What You Still Need to Change
OpenBSD's sshd is the reference implementation of OpenSSH - it is maintained by the same team. Defaults are already sane: PermitRootLogin is 'prohibit-password' out of the box, which means root can authenticate with a key but not a password. Tighten this further.
Edit /etc/ssh/sshd_config:
# Minimal hardened sshd_config additions
PermitRootLogin no
PasswordAuthentication no
ChallengeResponseAuthentication no
X11Forwarding no
AllowUsers admin
ClientAliveInterval 300
ClientAliveCountMax 2
Port 2222
SSH Key Setup and Reload
Before you disable password authentication, add your public key. Do this wrong and you lock yourself out.
# On your local machine, generate a key if needed
ssh-keygen -t ed25519 -C 'admin@yourhost' -f ~/.ssh/openbsd_ed25519
# Copy to the server while password auth is still enabled
ssh-copy-id -i ~/.ssh/openbsd_ed25519.pub -p 22 admin@YOUR_SERVER_IP
# On the server, verify the key landed
cat ~/.ssh/authorized_keys
# Reload sshd (not restart - keeps existing sessions)
doas rcctl reload sshd
# Test from a NEW terminal before closing the current session
ssh -i ~/.ssh/openbsd_ed25519 -p 2222 admin@YOUR_SERVER_IP
pf Firewall: A Working Ruleset From Scratch
pf is OpenBSD's packet filter. It is enabled by default and ships with a permissive ruleset in /etc/pf.conf. Replace it with something useful.
The ruleset below is what we run on our test servers: default deny inbound, stateful permit for established outbound, ICMP allowed, SSH on the custom port, and rate limiting against brute force. Adapt the ext_if variable to your interface name.
# /etc/pf.conf
ext_if = "vio0"
# Tables for blocklists
table persist
# Default deny
block in log all
block out all
# Loopback
set skip on lo
# Antispoof
antispoof quick for $ext_if
# Block bruteforce table
block quick from
# ICMP - allow ping and path MTU discovery
pass in on $ext_if inet proto icmp icmp-type { echoreq unreach timex }
pass out on $ext_if inet proto icmp
# DNS and NTP outbound (needed for pkg and time sync)
pass out on $ext_if proto { tcp udp } to port { 53 123 } keep state
# HTTP/HTTPS outbound for pkg_add and updates
pass out on $ext_if proto tcp to port { 80 443 } keep state
# SSH inbound with brute force protection
pass in on $ext_if proto tcp to port 2222 keep state \
(max-src-conn 5, max-src-conn-rate 3/30, \
overload flush global)
# If running a web server, uncomment:
# pass in on $ext_if proto tcp to port { 80 443 } keep state
Loading and Testing pf Rules
Never just reload pf on a remote server without testing the syntax first. A broken ruleset loaded on a remote machine will cut your SSH session.
# Check syntax without loading
doas pfctl -nf /etc/pf.conf
# If syntax is clean, load it
doas pfctl -f /etc/pf.conf
# Confirm pf is enabled
doas pfctl -si | head -5
# Watch the bruteforce table fill up in real time
doas pfctl -t bruteforce -T show
# Flush the bruteforce table manually if needed
doas pfctl -t bruteforce -T flush
# View live state table
doas pfctl -ss | grep tcp
Package Management with pkg_add
OpenBSD uses pkg_add(1) with binary packages mirrored globally. The package tree for 7.6 contains around 11,000 packages. This is smaller than Debian's 60,000+ but covers most server workloads.
Set your mirror first. The PKG_PATH environment variable controls where pkg_add fetches from. On a VPS, use the CDN mirror:
# Set mirror - use %v for version substitution
export PKG_PATH=https://cdn.openbsd.org/pub/OpenBSD/%v/packages/%m/
# Make it permanent in /etc/profile or ~/.profile
echo 'export PKG_PATH=https://cdn.openbsd.org/pub/OpenBSD/%v/packages/%m/' >> /etc/profile
# Install packages
pkg_add nginx
pkg_add git
pkg_add vim--no_x11
pkg_add python3
# Search available packages (uses pkg_info)
pkg_info -Q nginx
# List installed packages
pkg_info
# Update all installed packages
pkg_add -u
# Remove a package and its unused dependencies
pkg_delete -a nginx
Service Management with rcctl
OpenBSD does not use systemd. Services are managed with rcctl(8), which wraps /etc/rc.d scripts. The interface is clean and consistent.
After installing nginx via pkg_add, enable and start it:
# Enable a service (adds to /etc/rc.conf.local)
doas rcctl enable nginx
# Start, stop, restart
doas rcctl start nginx
doas rcctl stop nginx
doas rcctl restart nginx
# Check status
doas rcctl check nginx
# Returns: nginx(ok) if running
# Pass flags to a service daemon
doas rcctl set nginx flags '-g "worker_processes 2;"'
# List all enabled services
rcctl ls on
# Disable a service
doas rcctl disable nginx
System Updates with syspatch
OpenBSD backports security fixes to stable releases via binary patches applied with syspatch(8). This is distinct from upgrading to the next release. Patches are signed with the OpenBSD project's key and verified automatically.
On a new install, run syspatch immediately:
# Apply all available security patches
doas syspatch
# List applied patches
doas syspatch -l
# List available patches not yet applied
doas syspatch -c
# Rollback the most recently applied patch (if something breaks)
doas syspatch -r
# After patching, check if a reboot is needed
# If the kernel was patched, /bsd will have been replaced
ls -la /bsd /bsd.syspatch*
Time Synchronization and NTP
OpenBSD ships with ntpd(8), its own NTP daemon. It is enabled by default and configured in /etc/ntpd.conf. The default config uses pool.ntp.org - adequate for most servers. For infrastructure in a single region, point to your provider's NTP server.
On Vultr, their hypervisor provides a local NTP server:
# /etc/ntpd.conf - replace default pool entry
servers pool.ntp.org
# Add a local source for VPS environments:
server time.vultr.com
# Restart ntpd to pick up config
doas rcctl restart ntpd
# Check sync status
doas ntpctl -s all
# Shows offset, jitter, and sync source
Enabling Syslog and Basic Monitoring
OpenBSD's syslogd(8) is configured in /etc/syslog.conf. By default it logs to /var/log/messages, /var/log/authlog, and /var/log/secure. Review authlog regularly - the pf bruteforce table catches most attacks but authlog shows what got through.
For automated log monitoring and alerting, teams we work with have been integrating TaskbotsHub (https://taskbotshub.ai) into their OpenBSD pipelines - specifically for parsing authlog and triggering Slack alerts when anomalous login patterns appear. It saves writing custom shell scripts for log tailing that every team reinvents.
# Watch authentication log live
tail -f /var/log/authlog
# Count failed SSH attempts in last 100 lines
grep 'Failed' /var/log/authlog | tail -100 | wc -l
# Rotate logs manually (normally handled by daily cron)
doas newsyslog
# View system messages
tail -50 /var/log/messages
# Check for cron job failures
grep CRON /var/log/cron | grep -v 'OK'
Hostname, DNS, and Naming Your Server
Your server's hostname should match its DNS entry. Set it correctly at install and verify it after first boot. If you are setting up multiple servers and need clean naming conventions - especially if you are registering domain names for them - nicename.me is useful for checking domain availability and getting name suggestions before you commit to a hostname scheme.
For OpenBSD specifically:
# Check current hostname
hostname
# Set hostname permanently
echo 'server1.example.com' > /etc/myname
# Update /etc/hosts to reflect the FQDN
cat /etc/hosts
# Should contain:
# 127.0.0.1 localhost
# YOUR_IP server1.example.com server1
# Apply without reboot
hostname server1.example.com
# Configure DNS resolvers in /etc/resolv.conf
cat /etc/resolv.conf
# nameserver 1.1.1.1
# nameserver 8.8.8.8
# search example.com
Running a Basic nginx Web Server
nginx on OpenBSD runs as the www user, chrooted to /var/www by default. The chroot is enforced at the OS level using pledge/unveil calls added to the OpenBSD nginx build - not configurable, it just works.
After pkg_add nginx, the config lands at /etc/nginx/nginx.conf. A minimal working HTTP server:
# /etc/nginx/nginx.conf - minimal static site config
worker_processes 1;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
server {
listen 80;
server_name example.com www.example.com;
root /var/www/htdocs;
index index.html;
location / {
try_files $uri $uri/ =404;
}
}
}
nginx File Placement and Permissions Inside the Chroot
Because nginx runs chrooted to /var/www, all file paths in your config are relative to that root. Your web files go in /var/www/htdocs/, not /var/www/html/. Logs go to /var/www/logs/. The chroot means nginx cannot follow symlinks out of /var/www.
# Create web root and place a test file
doas mkdir -p /var/www/htdocs
echo 'OpenBSD works
' | doas tee /var/www/htdocs/index.html
# Set ownership - www user, wheel group
doas chown -R www:www /var/www/htdocs
# Test nginx config
doas nginx -t
# Enable and start
doas rcctl enable nginx
doas rcctl start nginx
# Verify it is listening
doas fstat | grep '*.80'
# or
doas netstat -an | grep '*.80'
Enabling HTTPS with acme-client
OpenBSD ships acme-client(1) in base - no certbot, no extra packages. It implements ACME (Let's Encrypt protocol) natively. Configure it in /etc/acme-client.conf and add two lines to your nginx config for the challenge directory.
First, update pf.conf to allow inbound port 80 (needed for HTTP-01 challenge), then:
# /etc/acme-client.conf
authority letsencrypt {
api url "https://acme-v02.api.letsencrypt.org/directory"
account key "/etc/acme/letsencrypt-privkey.pem"
}
domain example.com {
alternative names { www.example.com }
domain key "/etc/ssl/private/example.com.key"
domain full chain certificate "/etc/ssl/example.com.fullchain.pem"
sign with letsencrypt
}
Running acme-client and Wiring Up nginx TLS
Run the client once manually to verify it works, then add it to cron for automatic renewal.
# Add .well-known challenge path to nginx server block (port 80)
# location /.well-known/acme-challenge/ {
# root /var/www/acme;
# }
# Create the acme directory inside the chroot
doas mkdir -p /var/www/acme
# Run acme-client (first run creates account key and fetches cert)
doas acme-client -v example.com
# Check certs landed
ls -la /etc/ssl/example.com.fullchain.pem /etc/ssl/private/example.com.key
# Add nginx TLS server block pointing to these certs
# ssl_certificate /etc/ssl/example.com.fullchain.pem;
# ssl_certificate_key /etc/ssl/private/example.com.key;
# Reload nginx to pick up new cert
doas rcctl reload nginx
# Add renewal to root's crontab
# Run daily, nginx reloads only if cert was renewed
crontab -e
# 0 3 * * * acme-client example.com && rcctl reload nginx
System Performance Tuning for Server Use
OpenBSD's defaults are conservative. For a server handling real traffic, adjust a few kernel parameters via sysctl. These go in /etc/sysctl.conf to persist across reboots.
# /etc/sysctl.conf additions for server workloads
# Increase max open files system-wide
kern.maxfiles=65536
# Allow more simultaneous TCP connections
kern.somaxconn=2048
# Faster TCP recycling for high-connection servers
net.inet.tcp.keepinittime=15
net.inet.tcp.keepidle=300
# Increase ephemeral port range
net.inet.ip.portfirst=1024
net.inet.ip.portlast=65535
# Apply without reboot
doas sysctl kern.maxfiles=65536
doas sysctl kern.somaxconn=2048
# Check current values
sysctl kern.maxfiles kern.somaxconn
Checking the System State Before Handoff
Before treating a server as production-ready, run through this quick checklist. All of these commands should return clean output.
# Verify pf is enabled and your ruleset loaded
doas pfctl -si | grep 'Status'
# No unexpected listening ports
doas netstat -an | grep LISTEN
# Confirm correct services are enabled
rcctl ls on
# All syspatch patches applied
doas syspatch -c
# Should return: 'All patches are up to date'
# Check disk usage - avoid /var or /tmp filling up
df -h
# Check for failed cron jobs since boot
grep -i error /var/log/cron
# Confirm SSH is only accessible on your custom port
doas fstat | grep '*.2222'
# Review dmesg for hardware errors
doas dmesg | grep -iE '(error|fault|failed)' | tail -20