OpenBSD: Security Architecture and Protection Mechanisms

OpenBSD: Security Architecture and Protection Mechanisms

OpenBSD implements security through privilege separation, pledge(), unveil(), W^X memory protection, and proactive code audits. Securelevels restrict root privileges, while LibreSSL replaces OpenSSL.

OpenBSD develops security through design decisions in the kernel and userland code. Privilege separation is a common pattern: many daemons distribute tasks across processes with different privileges. The pledge() and unveil() system calls restrict programs to defined operations and file access. W^X prevents memory regions from being writable and executable at the same time. The kernel provides security levels that restrict even root access.

System Configuration shows doas as a privilege escalation tool and sysctl for kernel parameters. Package Management explains package signatures using signify(1).

Secure by Default – Design Philosophy

After a default installation, OpenBSD starts only a few network services. SSH can already be enabled during installation; cron is part of normal system operation. Other base daemons are enabled through the rc configuration as needed. pf is enabled by default and loads /etc/pf.conf at boot. Which connections are allowed or blocked is determined by the loaded ruleset—a default-deny policy only results from corresponding filter rules.

The project performs continuous and repeated code audits, deliberately searching for classes of errors such as buffer overflows, format string bugs, and race conditions. Depending on the findings, code is corrected, removed, or rewritten. The developers prefer manageable, auditable implementations over unnecessary complexity.

This philosophy is also reflected in components developed specifically for the base system. LibreSSL originated in 2014 as a fork of OpenSSL following the review of the codebase triggered by Heartbleed. doas replaced sudo in the base system. After Apache and, for a time, nginx, OpenBSD developed its own web server, httpd, with a deliberately limited feature set. A smaller and more clearly bounded codebase makes auditing and maintenance easier.

Privilege Separation – Process Isolation

Many OpenBSD daemons divide tasks among multiple processes with different privileges. Privileged processes retain only operations that require elevated privileges—such as binding privileged ports or opening protected files. Other tasks run in processes with fewer privileges.

The SSH daemon sshd demonstrates this principle. The listener, authentication, privileged session preparation, and the actual user session are distributed across different processes or components. After successful login, the actual session runs with the privileges of the authenticated user. Compromising this session therefore initially provides that user’s privileges rather than immediate root privileges.

$ ps aux | grep sshd
root     48124  0.0  0.2  1104  2492 ??  S       8:16AM    0:00.05 sshd: /usr/sbin/sshd [listener] 0 of 10-100 startups (sshd)
root     75300  0.0  0.4  1596  3928 ??  S       8:22AM    0:00.11 sshd-session: michael [priv] (sshd-session)
michael   9433  0.0  0.4  1880  3628 ??  S       8:23AM    0:00.04 sshd-session: michael@ttyp0 (sshd-session)

The listener process ([listener]) runs as root and waits for incoming connections. The 0 of 10-100 startups value belongs to the MaxStartups limit for simultaneously unauthenticated SSH connections; it does not count active user sessions. After successful authentication, a privileged session process (sshd-session: michael [priv]) prepares the user context. The actual session process (sshd-session: michael@ttyp0) then runs as the logged-in user.

httpd also uses process and privilege separation. Privileged tasks remain separate from the actual request processing; workers run with restricted privileges. CGI programs can be executed through the separate FastCGI server slowcgi. This architecture reduces the privileged attack surface: compromising an unprivileged worker initially provides only that worker’s privileges; obtaining root privileges would require an additional privilege escalation. The participating processes communicate through defined IPC interfaces.

pledge() – System Call Restriction

The pledge() system call reduces available kernel operations to a defined set. Programs declare which functions they require. The kernel denies all other system calls and terminates the process with SIGABRT on violations.

The syntax defines permitted operations as a string:

if (pledge("stdio rpath inet dns", NULL) == -1)
    err(1, "pledge");

This call permits standard I/O (stdio), read-only file access (rpath), network operations (inet), and DNS resolution (dns). All other system calls are prohibited—the program cannot write files, start new processes, or open devices.

The most important promises:

  • stdio – Standard I/O, memory allocation, signals
  • rpath – Read-only file access
  • wpath – Write file access
  • cpath – Create and remove files
  • inet – IPv4/IPv6 sockets
  • unix – Unix-domain sockets for IPC
  • dns – DNS resolution
  • proc – Process operations (fork, kill)
  • exec – Execute programs
  • prot_exec – PROT_EXEC for mmap() and mprotect()

The promise list evolves with each OpenBSD release. OpenBSD 7.8 included tmppath for temporary files in /tmp—as of 7.9, this promise has been removed and returns EINVAL. The replacement is "rpath wpath cpath" combined with unveil("/tmp", "rwc"). The complete, version-specific list is provided by:

$ man 2 pledge

The HTTP server httpd uses pledge() systematically. After initialization and binding its ports, the worker process restricts itself:

pledge("stdio rpath inet", NULL);

Workers can read files and handle network connections—and nothing more. They cannot write files, start new processes, or change kernel parameters.

ℹ️ Info
pledge() is irreversible. After the call, the program cannot remove the restrictions. Only stricter promises are possible by calling pledge() again with fewer privileges.

Most base system programs use pledge(). The ls command requires only stdio and rpath. The vi editor additionally needs wpath and cpath for file modifications. SSH clients use stdio inet dns tty proc exec for terminal interaction and remote commands.

unveil() – File System Restriction

The unveil() system call restricts file system access to explicitly permitted paths. The first call removes visibility of the entire file system—only the specified path with the defined permissions remains accessible. Additional calls can expose further paths.

The syntax defines the path and access permissions:

if (unveil("/etc/httpd.conf", "r") == -1)
    err(1, "unveil");
if (unveil("/var/www", "r") == -1)
    err(1, "unveil");
if (unveil(NULL, NULL) == -1)
    err(1, "unveil");

This code permits read access to /etc/httpd.conf and /var/www. The final unveil(NULL, NULL) call disables further unveil() calls—the configuration is frozen. Access to paths that have not been exposed fails: EACCES if the permission does not match, ENOENT if no unveil() entry qualifies for the path.

The available permissions:

  • r – Read
  • w – Write
  • x – Execute
  • c – Create and remove

The mail transfer agent smtpd uses unveil() for different processes. The network listener only needs configuration files:

unveil("/etc/mail/smtpd.conf", "r");
unveil("/etc/ssl/", "r");
unveil(NULL, NULL);

The queue manager requires write access to the spool directory:

unveil("/var/spool/smtpd/", "rwc");
unveil(NULL, NULL);

A compromised process sees only the paths and access types exposed to it through unveil().

ℹ️ Info
After unveil(NULL, NULL), further unveil() calls are no longer possible. OpenBSD 7.9 explicitly recommends locking unveil() after configuration—a program that leaves its file system view open exposes unnecessary attack surface.

Combined with pledge(), unveil() provides layered restrictions. pledge("stdio rpath") limits the available operations, while unveil() additionally limits the visible and accessible portion of the file system. The two mechanisms complement each other without requiring an external sandbox daemon.

Securelevels – Kernel Security Levels

OpenBSD implements kernel security levels that restrict even root access. Securelevels limit particularly critical modifications during operation. During bootstrapping and in single-user mode, the system is normally at securelevel 0. When transitioning to multi-user operation, the securelevel is raised to 1. Securelevel -1 is a special case in which this automatic increase does not occur.

The levels define increasing restrictions:

Securelevel -1: Permanently Insecure – functionally equivalent to securelevel 0, but init(8) does not automatically raise the securelevel.

Securelevel 0: Insecure – used during bootstrapping and in single-user mode. All devices are readable and writable, and system file flags can be cleared.

Securelevel 1: Secure – the default in multi-user operation. Prevents:

  • Opening /dev/mem and /dev/kmem
  • Write access to raw disk devices of mounted file systems
  • Removing system immutable and system append-only file flags
  • Changes to security-relevant sysctl variables (hw.allowpowerdown, kern.allowkmem, kern.utc_offset)
  • Increasing the ddb.console and ddb.panic sysctl variables
  • gpioctl can only access GPIO pins configured at boot

Securelevel 2: Highly Secure – all restrictions of level 1, plus:

  • Raw disk devices are always read-only—even when not mounted
  • The system time cannot be set backward or close to an overflow
  • pf filter and NAT rules are completely immutable

The current securelevel is shown by sysctl:

$ sysctl kern.securelevel
kern.securelevel=1

During normal multi-user operation, the securelevel is 1:

$ doas sysctl kern.securelevel=1
kern.securelevel: 1 -> 1
🚨 Warning
Only init(8) can lower the securelevel—not root, not doas. Lowering it on a running system is not possible. For maintenance tasks that require lower securelevels: work in single-user mode.

Root can raise the securelevel. Once at securelevel 1, it cannot be reset to a lower value; lowering it is reserved for init(8). Securelevel 1 restricts access to kernel memory, among other things; securelevel 2 additionally prevents changes to pf filter and NAT rules. Mutable PF table contents are not automatically included in this restriction.

/etc/rc.securelevel contains commands that are executed before rc(8) raises the securelevel—for example, to set file flags or adjust kernel parameters that would subsequently become immutable.

Because the securelevel can be manipulated through the in-kernel debugger ddb(4), it is advisable to disable the debugger on production systems:

$ sysctl ddb.console
ddb.console=0
$ sysctl ddb.panic
ddb.panic=1
$ doas sysctl ddb.panic=0
ddb.panic: 1 -> 0
$ doas sysctl ddb.panic=1
sysctl: ddb.panic: Operation not permitted

At securelevel 1 and above, ddb.console and ddb.panic can no longer be increased. If they are set to 0 beforehand, they therefore remain disabled during ongoing multi-user operation.

W^X – Write XOR Execute Memory Protection

OpenBSD enforces W^X (Write XOR Execute): memory must not be writable and executable at the same time. This separation makes code injection attacks more difficult, in which malicious code is first written to writable memory and then executed.

Code and data regions therefore receive different memory permissions. Data, heap, and stack are typically readable and writable, but not executable. Code regions are executable and not writable; on supported architectures, OpenBSD additionally uses execute-only protection for code. Execute-only is an additional hardening measure and is not equivalent to W^X itself.

The mmap() system call enforces this rule. Programs cannot mark memory as both PROT_WRITE and PROT_EXEC.

Example (rejected): PROT_WRITE|PROT_EXEC

void *mem = mmap(NULL, size, PROT_WRITE|PROT_EXEC,
                 MAP_ANON, -1, 0);

Return value: MAP_FAILED, errno = ENOTSUP

Correct: write first, then switch to executable

void *mem = mmap(NULL, size, PROT_READ|PROT_WRITE,
                 MAP_ANON, -1, 0);
write_code(mem);
mprotect(mem, size, PROT_READ|PROT_EXEC);

This sequence permits just-in-time compilers or self-modifying code under controlled conditions. After mprotect() with PROT_EXEC, the memory is no longer writable.

ℹ️ Info
Exception: Programs that require JIT compilation—such as Firefox or LuaJIT—can use PROT_WRITE|PROT_EXEC if two conditions are met: the file system is mounted with wxallowed, and the binary is tagged with wxneeded at link time. On production servers without such programs, this exception is not necessary.

For W^X problems, kern.wxabort helps with diagnostics:

$ sysctl kern.wxabort
kern.wxabort=0

With the default value 0, a W^X violation results in an error. With kern.wxabort=1, the kernel produces an abort instead, which can make diagnosing problematic programs easier:

$ doas sysctl kern.wxabort=1
kern.wxabort: 0 -> 1

OpenBSD applies W^X both in userland and in the kernel. The kernel itself also separates writable and executable memory regions.

On modern architectures, OpenBSD uses the available hardware mechanisms for non-executable memory regions and supplements them with additional protection mechanisms in the kernel and virtual memory management.

The implementation also affects stack protection. The stack is explicitly non-executable, making buffer overflow exploits more difficult. Attackers can overwrite stack data but cannot place and execute code there. Combined with stack protector canaries, this provides layered defense.

Cryptography Stack

OpenBSD integrates cryptography deeply into the system. LibreSSL replaces OpenSSL as the TLS library. The arc4random() random number generator provides cryptographically secure random numbers without seed management. Password hashing uses bcrypt with an adjustable work factor.

LibreSSL – OpenSSL Fork

Following the Heartbleed vulnerability in 2014, OpenBSD began a comprehensive review of the OpenSSL codebase and forked OpenSSL 1.0.1g into LibreSSL. During the initial cleanup, more than 90,000 lines of C code were removed, including no-longer-needed platform support and historically accumulated code. The goal was a smaller, more auditable TLS and cryptography codebase.

LibreSSL is part of the OpenBSD base system and is maintained together with the system. Additionally installed packages can use LibreSSL, but they are not inherently tied to the same TLS library.

LibreSSL retained many OpenSSL-compatible interfaces, but deliberately removed obsolete or unwanted APIs and has continued to evolve independently since the fork. OpenSSL compatibility is therefore not a general guarantee.

arc4random() – Cryptographic PRNG

The arc4random() function generates 32-bit random numbers without manual seed management. The name can be read as “A Replacement Call for Random”—historically, it originates from the formerly used ARC4/RC4 generator. Since OpenBSD 5.5, the implementation has used ChaCha20. The generator is regularly reseeded from the kernel random subsystem; it is also reseeded after fork(2) so that parent and child do not simply continue using the same inherited PRNG state.

Random number between 0 and 99:

uint32_t random = arc4random_uniform(100);

Fill a buffer with random data—for example, for key material:

unsigned char key[32];
arc4random_buf(key, sizeof(key));

Random number between min and max:

uint32_t random_range(uint32_t min, uint32_t max) {
    return min + arc4random_uniform(max - min + 1);
}

The arc4random_uniform() function avoids modulo bias. Naive implementations using arc4random() % n distribute numbers unevenly when n is not a power of two. LibC contains an optimized bias-free implementation.

The kernel mixes entropy material from system activity and available hardware random sources. OpenBSD also stores entropy material for the next boot and uses it to initialize the random number generator early in the startup process. /dev/random is an alias for /dev/urandom on OpenBSD; both provide non-blocking data from the same cryptographic generator. On other operating systems, /dev/random may have different semantics.

bcrypt – Password Hashing

OpenBSD uses bcrypt for password hashing. The algorithm is based on Blowfish with an adaptive work factor—higher work factors mean more CPU time per hash and make brute-force attacks against stolen password databases more difficult.

The /etc/master.passwd file stores bcrypt hashes:

michael:$2b$10$TX6ZNHMmjVF8RiD9q2dfBOU0S5djznlXkxYSzuOj8cLzQ3n4t0RfS:1001:1001:...

The format: $2b$ identifies bcrypt. 10 is the logarithmic work factor and corresponds to a cost parameter of 2^10. The following string contains the salt and hash in bcrypt representation.

The encrypt(1) command generates bcrypt hashes:

$ encrypt "testpassword"
$2b$10$TX6ZNHMmjVF8RiD9q2dfBOU0S5djznlXkxYSzuOj8cLzQ3n4t0RfS

OpenBSD can automatically select the bcrypt work factor based on system performance. For local password hashes, the default configuration uses bcrypt,a; programs can also specify a fixed bcrypt cost parameter through crypt_newhash(3).

ℹ️ Info
crypt(3) is deprecated. New programs use crypt_newhash(3) to create password hashes and crypt_checkpass(3) to verify them.

Additional Security Features

OpenBSD implements additional protection mechanisms that make exploit development more difficult. Address Space Layout Randomization (ASLR) randomizes memory regions. Stack protector canaries detect buffer overflows. Return-Oriented Programming (ROP) gadgets are reduced through compiler optimizations.

ASLR – Address Space Layout Randomization

ASLR randomizes the location of important regions of a process’s address space, including the stack, heap, libraries, and the main program. This prevents exploits from reliably using hard-coded memory addresses. When a program is loaded through exec(), a newly randomized layout is created; a plain fork(), in contrast, initially inherits the parent process’s address space.

OpenBSD enables ASLR by default. In addition, library_aslr in /etc/rc.conf controls further ROP hardening at boot:

$ grep library_aslr /etc/rc.conf
library_aslr=YES		# set to NO to disable library randomization

PIE is enabled by default in the OpenBSD compiler. This allows executables to be placed at varying base addresses when loaded. Individual builds can explicitly disable PIE if necessary.

Stack Protector and RETGUARD

The classic stack protector detects certain stack buffer overflows using canary values. On supported architectures, OpenBSD additionally or alternatively uses RETGUARD. RETGUARD specifically protects a function’s return address with a function-specific random cookie.

Before the function returns, it checks whether the stored return address still matches the cookie. Manipulating the return address causes the program to abort. Unlike a classic stack canary, RETGUARD therefore checks the return address directly.

void test(char *input) {
    char buffer[64];
    strncpy(buffer, input, sizeof(buffer)-1);
    printf("%s\n", buffer);
}

On an architecture supported by RETGUARD, the compiler can instrument the function accordingly. Examining the compiled binary shows the generated RETGUARD cookies:

$ cc -g -o test test.c
$ nm test | grep retguard
00002bc0 r __retguard_1205
00002bd8 r __retguard_1471
00002be0 r __retguard_1773
00002be8 r __retguard_2333
00002bd0 r __retguard_2473
00002bf0 r __retguard_2997

The __retguard_* symbols show the RETGUARD cookies generated by the compiler. The random values reside in the ELF section .openbsd.randomdata and are initialized when the program is loaded.

RETGUARD is architecture-specific. In OpenBSD 7.8 and 7.9, the mechanism is used on amd64 and arm64, among other supported architectures. It is therefore not a universal feature of every OpenBSD binary on every platform.

ROP Mitigation

Return-Oriented Programming (ROP) uses existing snippets of code (gadgets) for exploits. W^X prevents new code injection—ROP circumvents this by chaining existing instructions.

OpenBSD makes ROP more difficult through several measures:

Library Randomization: With library_aslr=YES, OpenBSD rearranges certain system libraries at boot. Among other things, object files are relinked in random order. This changes internal addresses and code locations of interest for ROP between system boots.

Return Address Protection: On supported architectures, RETGUARD protects the return addresses of instrumented functions using function-specific random cookies.

W^X: Memory must not be writable and executable at the same time. This makes the direct execution of newly injected code more difficult.

In addition, the OpenBSD compiler reduces certain instruction sequences suitable for ROP on supported architectures. No single mechanism is insurmountable—together, they significantly increase the effort required for successful exploits.

Security Through Simplicity

The mechanisms described operate at different points in the system. Privilege separation distributes tasks and privileges, pledge() and unveil() limit the capabilities of running processes, securelevels restrict modifications to the running system, and exploit mitigations make memory corruption vulnerabilities more difficult to exploit.

OpenBSD does not rely on a single protection mechanism. Multiple independent boundaries are intended to prevent a single flaw or compromised component from affecting the entire system without encountering further obstacles. Manageable components and continuous code audits complement these technical boundaries during the development of the system itself.

The article OpenBSD: Networking and Routing covers network configuration and routing for production OpenBSD systems. OpenBSD: Firewall and pf – Fundamentals & Syntax explains the packet filter firewall and policy design.


Software and Versions Used

This guide applies to:

  • OpenBSD: 7.8, 7.9
  • Context: Server and desktop
  • As of: June 2026

Michael Schwarzer

has worked with technical systems, their dependencies, and interfaces for more than four decades – today with a particular focus on resilient systems, digital independence, and the controlled use of AI.
More about Michael →

Open Knowledge Needs People

We develop ideas, share knowledge, and create open-source software and tools. With your support, you help us continue this work for years to come.
Get involved and support our work →
OpenBSD: Security Architecture and Protection Mechanisms
← Next Article OpenBSD: Networking and Routing
OpenBSD: Security Architecture and Protection Mechanisms
Previous Article → OpenBSD: Package Management and Software Installation