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, and LibreSSL replaces OpenSSL.

OpenBSD builds security into both the kernel and userland through deliberate design decisions. Every daemon runs with the minimum required privileges in separate processes. The pledge() and unveil() system calls restrict programs to defined operations and filesystem access. Memory pages are either writable or executable—never both at the same time. The kernel also provides security levels that restrict even the root account.

System Configuration introduces doas as the privilege escalation tool and sysctl for kernel parameters. Package Management explains package signatures using signify(1). This article describes the security architecture that protects those tools and how OpenBSD systematically reduces its attack surface.

Secure by Default – Design Philosophy

After installation, OpenBSD enables only SSH and cron. All other services remain disabled. The pf firewall blocks incoming connections by default. This approach reduces attack vectors without requiring additional configuration.

The project performs continuous code audits. Every line of code is examined for security issues, including buffer overflows, format string vulnerabilities, and race conditions. Unsafe code is either removed or rewritten. The developers consistently favor simple, verifiable implementations over feature-rich alternatives.

This philosophy influences every design decision. LibreSSL replaced OpenSSL after Heartbleed. doas replaced sudo with roughly 400 lines of code instead of 4,000. httpd provides the essential functionality of a web server without complex modules. Less code means fewer bugs.

Privilege Separation – Process Isolation

OpenBSD daemons are divided into multiple processes with different privilege levels. The privileged parent process runs as root and performs only critical operations:

  • Bind ports below 1024
  • Open files requiring elevated privileges

Child processes run as unprivileged users and handle network traffic or client requests.

The SSH daemon sshd demonstrates this concept. A privileged monitor process runs as root and authenticates users. After a successful login, it starts an unprivileged session process running as the authenticated user. Compromising the session process affects only that user’s session—not the entire system.

$ 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 process hierarchy consists of three layers. The listener process ([listener]) runs as root and waits for incoming connections. The 0 of 10-100 startups field indicates how many child processes are currently active. After successful authentication, a privileged session process (sshd-session: michael [priv]) runs as root to establish the user context. The actual session process (sshd-session: michael@ttyp0) then runs as the logged-in user. Compromising that process affects only the current session.

The httpd web server follows the same model. A root process binds ports 80 and 443. Worker processes run as www and serve static content. CGI scripts execute in separate slowcgi processes, again as an unprivileged user.

This architecture prevents privilege escalation. An attacker who compromises a worker process gains only the privileges of the www user—not root privileges. Communication between processes takes place through IPC sockets using well-defined message protocols.

pledge() – Restricting System Calls

The pledge() system call limits the available kernel operations to a defined set. Programs explicitly declare which capabilities they require. The kernel rejects all other system calls and terminates the process with SIGABRT if a violation occurs.

The syntax specifies the 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 are:

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

The list of available promises evolves with each OpenBSD release. In OpenBSD 7.8, tmppath allowed temporary files in /tmp. Beginning with OpenBSD 7.9, that promise was removed and now returns EINVAL. The replacement is "rpath wpath cpath" combined with unveil("/tmp", "rwc"). The complete, version-specific list is available from:

$ man 2 pledge

The httpd web server uses pledge() extensively. After initialization and binding its network ports, the worker process reduces its privileges:

pledge("stdio rpath inet", NULL);

Workers can read files and handle network connections—nothing more. They cannot write files, start new processes, or modify kernel parameters. Even if a worker is compromised, its capabilities remain tightly constrained.

ℹ️ Info
pledge() is irreversible. Once called, a program cannot remove the restrictions. It can only make them stricter by calling pledge() again with a smaller set of promises.

Most programs in the base system use pledge(). The ls command requires only stdio and rpath. The vi editor additionally needs wpath and cpath to modify files. SSH clients use stdio inet dns tty proc exec for terminal interaction and remote command execution.

unveil() – Filesystem Access Restrictions

The unveil() system call restricts filesystem access to explicitly approved paths. The first call removes visibility of the entire filesystem. From that point on, only the specified path with its associated permissions remains accessible. Additional calls can grant access to further paths.

The syntax specifies both the path and its 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 grants read access to /etc/httpd.conf and /var/www. The final unveil(NULL, NULL) call permanently disables any further unveil() calls, freezing the configuration. Access to non-approved paths fails with EACCES if the permissions do not allow the operation, or ENOENT if no matching unveil() rule exists for the requested path.

Available permissions are:

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

The mail transfer agent smtpd uses unveil() differently for its individual processes. The network listener requires access only to configuration files:

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

The queue manager requires write access to the mail spool directory:

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

This separation isolates the individual components. A compromised listener cannot manipulate mail. A compromised queue manager cannot access configuration files or TLS certificates.

ℹ️ Info
After unveil(NULL, NULL), no further unveil() calls are possible. OpenBSD 7.9 explicitly recommends locking unveil() once configuration is complete. A program that leaves its filesystem view open unnecessarily increases its attack surface.

unveil() combined with pledge() creates layered restrictions. pledge("stdio rpath") permits file access, while unveil() determines which files are actually accessible. A program using both system calls runs inside a precisely defined sandbox without external dependencies.

Securelevels – Kernel Security Levels

OpenBSD implements kernel security levels that restrict even the root account. Securelevels prevent modification of critical system components while the system is running. The system starts at Securelevel -1 and automatically raises the level during the boot process.

The individual levels define increasingly strict restrictions.

Securelevel -1: Permanent Insecure — all operations are permitted. init(8) does not automatically raise the securelevel. Developers use this level primarily for kernel debugging.

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

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

  • Opening /dev/mem and /dev/kmem
  • Writing to raw disk devices containing mounted filesystems
  • Removing system immutable and append-only file flags
  • Modifying security-related sysctl variables (hw.allowpowerdown, kern.allowkmem, kern.utc_offset)
  • Increasing the ddb.console and ddb.panic sysctl variables
  • Using gpioctl to access GPIO pins that were not configured during boot

Securelevel 2: Highly Secure — includes all restrictions from level 1, plus:

  • Raw disk devices remain read-only at all times, even when not mounted
  • The system clock cannot be set backward or adjusted close to an overflow
  • pf filtering and NAT rules become completely immutable

The current securelevel is displayed by sysctl:

$ sysctl kern.securelevel
kern.securelevel=1

The securelevel is raised automatically by rc(8) during boot:

$ doas sysctl kern.securelevel=1
kern.securelevel: 1 -> 1
🚨 Warning
Only init(8) can lower the securelevel—not root and not doas. Once the system has reached a higher securelevel, it cannot be lowered while running. Maintenance tasks requiring lower securelevels must therefore be performed in single-user mode.

The root account may raise the securelevel at any time. Once the system reaches level 1, only init(8) is permitted to lower it again. Even a compromised root account cannot modify kernel code or disable firewall rules.

/etc/rc.securelevel contains commands executed before rc(8) raises the securelevel, allowing administrators to set file flags or modify kernel parameters that later become immutable.

Because the securelevel can be manipulated through the in-kernel debugger ddb(4), production systems generally disable the debugger:

$ 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

Once Securelevel 1 has been reached, these values can no longer be increased. The configuration remains fixed for the lifetime of the running system.

W^X – Write XOR Execute Memory Protection

OpenBSD enforces W^X (Write XOR Execute) across the entire system. Every memory page is either writable or executable—never both at the same time. This prevents code injection attacks that write malicious code into writable memory and then execute it.

The memory layout strictly separates code from data. Text segments containing program code are marked executable. Data segments containing variables are writable. Both the stack and heap are writable but explicitly non-executable. Shared libraries follow the same model.

The mmap() system call enforces this rule. Programs cannot map memory with both PROT_WRITE and PROT_EXEC simultaneously.

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 approach: write first, then change the memory to executable

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

This sequence allows Just-In-Time compilers and self-modifying code under controlled conditions. After calling mprotect() with PROT_EXEC, the memory is no longer writable.

ℹ️ Info
Exception: Programs requiring JIT compilation—such as Firefox or LuaJIT—may use PROT_WRITE|PROT_EXEC if two conditions are met. The filesystem must be mounted with wxallowed, and the binary must be tagged with wxneeded at link time. Servers that do not run such software generally do not require this exception.

For diagnosing W^X violations, kern.wxabort can be useful:

$ sysctl kern.wxabort
kern.wxabort=0

The default value of 0 logs W^X violations without terminating the offending process. Production systems commonly use kern.wxabort=1 to abort such violations immediately:

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

W^X applies throughout the system, including both the kernel and userland. The OpenBSD kernel itself maintains separate regions for executable code and writable data. Kernel modules are rare in OpenBSD, and any such modules would also be required to comply with W^X.

Modern CPUs support W^X through hardware features. Intel processors provide the NX (No Execute) bit, while AMD processors implement EVP (Enhanced Virus Protection). OpenBSD consistently uses these hardware capabilities and falls back to software emulation on older hardware.

The implementation also protects the stack. Because the stack is explicitly non-executable, classic buffer overflow exploits become significantly more difficult. Attackers may overwrite stack data, but they cannot execute injected code there. Combined with stack protection mechanisms, this provides multiple layers of defense.

Cryptography Stack

OpenBSD integrates cryptography deeply into the operating system. LibreSSL replaces OpenSSL as the system TLS library. The arc4random() random number generator provides cryptographically secure randomness without manual seed management. Password hashing uses bcrypt with an adaptive work factor that automatically scales with available processing power.

LibreSSL – An OpenSSL Fork

Following the Heartbleed vulnerability in 2014, the OpenBSD project forked OpenSSL into LibreSSL. More than 90,000 lines of obsolete code were removed, including VMS support, outdated cryptographic algorithms, and redundant abstraction layers. LibreSSL focuses on modern systems and current security standards.

Integration into the base system provides a consistent update path. The TLS library and kernel components originate from the same source tree. Package management tools use the same LibreSSL installation as Apache or Postfix. There are no version conflicts between system TLS and application TLS.

LibreSSL also introduces proactive security improvements. TLS 1.3 became available earlier than in the OpenSSL stable branch. Weak cipher suites are disabled quickly. The API remains largely compatible with OpenSSL to simplify software porting, except for deprecated functions.

arc4random() – Cryptographically Secure PRNG

The arc4random() function generates 32-bit random numbers without requiring manual seed management. The name stands for “A Replacement Call for Random.” Despite its historical name, the generator uses ChaCha20 rather than RC4. The kernel periodically reseeds the generator, and every fork(2) automatically triggers a new seed so that separate processes never produce identical random sequences.

Generate a random number between 0 and 99:

uint32_t random = arc4random_uniform(100);

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

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

Generate a random number within a given range:

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

The arc4random_uniform() function avoids modulo bias. A naive implementation using arc4random() % n produces uneven distributions whenever n is not a power of two. The C library provides an optimized implementation that guarantees an unbiased distribution.

The kernel gathers entropy from hardware sources such as interrupt timing, disk I/O, and network traffic. The pseudo-devices /dev/random and /dev/urandom provide identical randomness quality, unlike Linux. Applications can safely read from either device without blocking because the generator always maintains sufficient entropy.

bcrypt – Password Hashing

OpenBSD uses bcrypt for password hashing. The algorithm is based on Blowfish and employs an adaptive work factor. Higher work factors require more CPU time per hash, making brute-force attacks against stolen password databases significantly more expensive.

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

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

The format is straightforward. $2b$ identifies bcrypt. 10 is the work factor, meaning the algorithm performs 2^10 = 1024 iterations. The remaining string contains both the 128-bit salt and the resulting hash.

The encrypt(1) utility generates bcrypt hashes:

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

The work factor is selected automatically based on CPU performance. On physical hardware it typically falls between 10 and 12. Virtual machines may choose lower values because QEMU and KVM influence the CPU speed measurement.

ℹ️ Info
crypt(3) is deprecated. New applications should 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 considerably more difficult. Address Space Layout Randomization (ASLR) randomizes memory layouts. Return address protection detects attempts to overwrite function return addresses. Compiler-based mitigations also reduce the effectiveness of Return-Oriented Programming (ROP).

ASLR – Address Space Layout Randomization

ASLR places the stack, heap, shared libraries, and executable segments at randomized memory addresses. Exploits can no longer rely on hard-coded addresses because every process launch creates a different memory layout.

OpenBSD enables ASLR by default. The library_aslr variable in /etc/rc.conf controls shared library randomization during boot:

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

Position Independent Executables (PIE) are the compiler default. All base system programs are built as PIE, and packages in the Ports tree use PIE whenever possible.

Return Address Protection

OpenBSD protects function return addresses using randomly generated guard values. The Clang compiler automatically inserts a unique __retguard value into every protected function. Before the function returns, the generated code verifies that guard. Any modification indicates memory corruption, and the program terminates immediately.

This mechanism is stronger than traditional stack canaries, which rely on a single process-wide value. In OpenBSD, every protected function receives its own independent random guard.

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

The compiler automatically protects this function with its own __retguard value. The generated guards are visible in the compiled binary:

$ 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

Each entry corresponds to one protected function. An attacker attempting to overwrite a return address must also know the correct guard value, which is randomized every time the program starts.

OpenBSD enables return address protection for all userland programs. The kernel uses separate protection mechanisms because these checks are performance-critical in kernel code.

ROP Mitigation

Return-Oriented Programming (ROP) constructs exploits by chaining together existing instruction sequences, known as gadgets. While W^X prevents code injection, ROP attempts to reuse executable code that is already present in memory.

OpenBSD makes ROP significantly more difficult through several complementary mechanisms.

Library randomization: library_aslr places shared libraries at different addresses during every boot. Reliable ROP chains depend on fixed addresses, making randomization highly disruptive.

Return address protection: __retguard protects every function with its own random guard value. Any attempt to manipulate a return address is detected before control returns to the caller.

W^X: No memory region may be writable and executable at the same time. Injected code therefore cannot be executed directly.

The combination of W^X, library_aslr, and __retguard provides defense in depth. No individual mechanism is perfect, but together they substantially increase the effort required to build reliable exploits.

Security Through Simplicity

OpenBSD security is the result of proactive design rather than reactive patching. Small programs with clearly defined responsibilities contain fewer defects than feature-rich alternatives. Privilege separation isolates individual components. pledge() and unveil() restrict running processes. W^X and ASLR make memory corruption exploits significantly more difficult.

Continuous code audits identify vulnerabilities before they can be exploited. When audits reveal architectural weaknesses, the project rewrites code rather than accumulating patches. LibreSSL replaced OpenSSL, doas replaced sudo, and httpd replaced Apache for the requirements of the base system.

The upcoming Networking and Routing article explains network configuration and routing for production systems. The planned Firewall and pf article introduces packet filtering and firewall policy design. The security architecture described here provides the foundation for building reliable service infrastructure.


Software and Versions Used

This article is based on:

  • OpenBSD: 7.8, 7.9
  • Context: Server and Desktop
  • Reference: June 2026

Michael of the Dragons

develops books, software, and open frameworks around technical systems, digital independence, and durable software architectures.
More about Michael →
OpenBSD: Security Architecture and Protection Mechanisms
Previous Article → OpenBSD: Package Management and Software Installation