Part 3 of 4 · Simplicity Is Security

Simplicity Is Security: The Limits of Understanding

A boundary matters most when our understanding fails.

By ·13 min read

OpenSSH accepts network traffic from people who haven’t authenticated yet. It must parse their messages, negotiate encryption, and determine whether they’re allowed to log in. It also needs access to a private host key to prove the server’s identity. Both operations are necessary. But does the code processing untrusted network traffic need possession of that private key?

OpenSSH’s answer was no.

Its privilege-separation architecture lets an unprivileged process handle network traffic while a separate, privileged process performs operations that require greater authority. The network-facing process can request a signature without receiving the private key. If an attacker compromises that process, the compromise doesn’t automatically grant the authority of the privileged process.1

This is the third of four essays in Simplicity Is Security. The first considered what software must not do. The second argued for removing capabilities we don’t need. This article admits software will still contain defects. We need to design for the mistakes we miss.

What auditing cannot promise

In The Humble Programmer, Dijkstra described the competent programmer as aware of the “strictly limited size of his own skull.”2 He wasn’t suggesting that programmers should avoid difficult problems. He was arguing that our methods, languages, and designs must account for the limits of human understanding.

Those limits become especially important in security.

A function may be short and locally straightforward while the consequences of its execution depend on thousands of lines elsewhere. A reviewer needs to understand not only what the function does, but what its dependencies do, what state surrounds it, and what authority the process possesses.

A small parsing function might be easy to review. But if it runs in a process that can read credentials, modify files, and open network connections, a defect in that parser may expose far more than the function’s interface suggests.

OpenBSD took code inspection very seriously. Its developers audited existing code, looked for recurring classes of mistakes, and revisited earlier work when they learned something new. Finding one defect could lead to a search for similar defects throughout the source tree.3 That work was valuable, but it also had limits.

In his 2015 presentation introducing pledge, Theo de Raadt described the difficulty of auditing software with “too much complexity to keep in mind.”3 OpenBSD’s response included exploit mitigations and restrictions on what programs could do. The point wasn’t that auditing had failed or should be abandoned. It was that finding defects and containing their consequences are different kinds of security work.

Inspection can remove many mistakes. A well-designed boundary can limit what surviving mistakes can do.

Boundaries for reasoning

Not all complexity is accidental.

A payment system needs to distinguish a new charge from a retry. A service supporting multiple organizations must enforce their separate permissions. An SSH server must implement a complicated protocol and authenticate users. Those obligations belong to the problem. We can make their implementations clearer, but we cannot simplify away what the software does.

Other complexity comes from how we’ve organized the implementation: duplicated rules, unrelated operations sharing mutable state, and design decisions exposed to components that don’t need to understand them.

David Parnas examined this distinction in his 1972 paper, On the Criteria To Be Used in Decomposing Systems into Modules.4 Using a text-indexing system, he compared conventional decomposition into processing steps with one organized around hiding design decisions. In the latter, details such as how lines were stored belonged to a module whose interface concealed that representation from the rest of the program. A change to the storage representation could then remain local instead of forcing changes throughout the system.

One of Parnas’s more interesting observations was that the two decompositions could produce identical runnable representations. Their differences exist primarily in how programmers reason about the system. That is a powerful kind of simplicity. It reduces the number of decisions a programmer must understand at once.

But it isn’t necessarily a security boundary. A function declared private doesn’t prevent an attacker who has gained control of its process from accessing memory outside the function. A well-designed module interface doesn’t revoke the process’s credentials or prevent it from making system calls. We can hide implementation details from programmers without hiding resources from compromised code.

That difference matters. A module boundary organizes understanding. A security boundary limits authority.

Privilege separation changes the consequences

OpenSSH provides a useful example of the distinction. In the privilege-separation design documented in 2003, the server divided its work between a privileged monitor and an unprivileged child process. Before authentication, the child handled network traffic while running with restricted user and group identities, confined to an empty filesystem directory.1

The child still needed privileged operations. It needed the server to authenticate key exchanges. It needed to validate credentials. After a user authenticated, the session could require operations such as creating a pseudo-terminal. Instead of retaining the authority to perform those operations, the child requested them from the monitor over a local socket. For example, the child could request a signature proving the server’s identity. The monitor performed the signing operation and returned the result. The private key never needed to cross into the network-facing process.

But separating the processes isn’t sufficient by itself. The monitor must assume that the child could be compromised. A request arriving over their shared socket is not inherently trustworthy merely because it came from another part of OpenSSH. The monitor must validate the request and determine whether that operation is permitted at the current stage of the protocol. Before authentication, only certain requests make sense. After authentication, different operations become legitimate.

The 2003 design describes the monitor as a state machine, with the permitted requests changing as the connection progresses. The child’s claim that a user has authenticated cannot, by itself, authorize the privileged actions that follow.1

This makes the monitor’s interface critically important. If it accepts arbitrary commands, reads arbitrary paths, or returns arbitrary secrets, an attacker controlling the child may recover much of the authority that separation was supposed to remove. The protocol between them must be narrow enough to exclude operations the unprivileged process has no legitimate reason to request.

The apparent paradox

The numbers from the original implementation are revealing. According to the 2003 paper, privilege separation added approximately 3,000 lines of code to OpenSSH, increasing its source size by about 7%. The work also required changes to existing interfaces. Yet the resulting architecture ran approximately two-thirds of the code without special privilege, including third-party libraries. Considering OpenSSH’s own code separately, approximately three-quarters ran without special privilege.1

The project added code while reducing the amount of code entrusted with special privilege. The authors also reported that the separation forced them to improve subsystem interfaces, making the code easier to organize, understand, and audit.

But where is the simplicity in adding another process and a protocol?

The separation didn’t make parsing network messages or negotiating encryption simpler. The simplicity lies in this: the implementation grew, but the responsibilities became smaller and more secure. We still have privileged code requiring scrutiny, but it does much less. And the unprivileged code becomes easier to reason about. The authors of the paper concluded that the code was “better organized, more easily understood and audited, and less complex.”

Make the boundary enforceable

Privilege separation uses operating-system process boundaries to distinguish what different parts of a program may do. OpenBSD also provides mechanisms for restricting a process’s authority without necessarily dividing every operation into another process. Two particularly useful mechanisms are pledge and unveil.

With pledge, a process declares the classes of system operations it will need, which are called “promises”. Operations outside those promises become unavailable. A process can narrow its promises as execution proceeds, but it cannot restore promises it has given up.5 Most prohibited operations cause the process to be terminated immediately, although the interface also provides specified error behavior, including an error promise that requests ENOSYS instead.

unveil addresses a process’ filesystem access. A program identifies the paths it may use and the operations permitted on each. After establishing and locking those rules, it cannot simply open some other path because a defect or malicious input persuaded it to try.6 An access denied by unveil ordinarily produces an error rather than terminating the process.

Consider a worker whose job is to examine an uploaded file and write a result. It may need access to that input and a limited output location. It doesn’t inherently need to launch other programs, connect to arbitrary network addresses, or read the application’s credentials.

Restrictions established outside the worker’s parsing logic can remain effective even if that logic is compromised. This is a different promise from code review. A reviewer might establish that no known path through the parser opens a credential file. An operating-system restriction can deny that access even when execution takes a path the reviewer didn’t anticipate. The enforcement mechanism becomes part of the design rather than another convention the compromised code is expected to respect.

Simple enough to use

A security mechanism is effective only if developers actually use it. That makes the effort required to adopt a security boundary part of its design. OpenBSD provides an unusually striking example. Development of pledge began in 2015. By the release of OpenBSD 5.9 in March 2016, developers had adapted 453 of the operating system’s 707 base-system binaries to use it.7

Part of the explanation for this adoption rate is the interface. A program that needs basic I/O and read-only filesystem access can declare:

c
1pledge("stdio rpath", NULL);

The operating system then prevents the process from performing operations outside those categories, such as opening new network connections or executing other programs. The restrictions are enforced by the kernel, not by checks scattered throughout the application.

unveil, introduced in 2018, applies a similarly simple approach to filesystem access. Programs identify the paths they need and the operations permitted on those paths, then lock the configuration.

Other operating systems provide comparable protections through different designs. Linux has seccomp and Landlock. The former filters individual system calls and can examine their arguments, while the latter provides resource-access restrictions. FreeBSD’s Capsicum confines programs through capabilities associated with file descriptors. These mechanisms offer greater precision in some respects, but may also require more decisions and changes to the program’s structure.

That additional flexibility has value. A more expressive security mechanism can enforce restrictions that a simpler one cannot. But expressiveness also creates decisions developers must understand and implement correctly. In a 2025 qualitative usability study, seven developers experienced with seccomp independently constructed sandboxes for the same application. They arrived at different solutions and encountered difficulties understanding which system operations their policies needed to permit.8

Meanwhile, a 2026 study of 19 OpenBSD releases found that adoption of pledge and unveil continued growing over nearly a decade.9 The rapid initial adoption doesn’t establish that OpenBSD’s approach is universally better. The developers were working within a project they controlled, with common conventions and considerable freedom to modify existing programs. Third-party software presents different obstacles. But the result demonstrates something important. OpenBSD didn’t merely introduce a mechanism that could confine software. Its developers made that mechanism practical enough to apply throughout much of their own operating system.

Simplicity wasn’t a substitute for enforcement, but it helped make enforcement more commonplace.

What a sandbox cannot decide

Containment has limits, and some of them are easy to overlook.

First, restricting future operations doesn’t necessarily revoke authority the process already possesses. A worker might inherit an open credential file, an established network connection, or a writable descriptor. Preventing it from using new resources doesn’t do much for the resources it selected. The design must account for privileges the worker receives.

Second, an operating-system boundary doesn’t necessarily understand application policy. A process may be permitted to read a directory containing documents for several customers. The kernel doesn’t know which customer requested a particular document. A filesystem restriction isn’t a substitute for proper authorization and access control. The sandbox limits the operations available to the process. It doesn’t decide whether every permitted operation is appropriate in the application’s current state.

Third, permitted operations can still consume resources. A confined worker can loop indefinitely, allocate memory, or overwhelm its monitor with requests the protocol allows. The original OpenSSH privilege-separation paper explicitly excluded resource-starvation attacks from its protection claim.1 CPU and memory limits, bounded requests, and appropriate recovery behavior are separate responsibilities.

None of these limitations invalidates containment. They identify what the containment boundary actually promises. A sandbox is not proof that the contained program is correct. It limits what resources an incorrect program can access.

What we can establish

Auditing, testing, formal verification, and containment address different questions. An audit can discover a missing check. A test can demonstrate whether a particular failure path is handled. A proof can establish a property under explicit assumptions. An enforced boundary can deny an operation even when a defect survives the other three. They complement one another.

The important design decision is not whether we can understand the entire system perfectly. We should make its behavior as understandable as we can, but we should also arrange its authority so that our incomplete understanding doesn’t become catastrophic failure. That’s the deeper sense in which simplicity contributes to security.

A well-designed module reduces what a programmer needs to understand. A well-designed privilege boundary reduces what compromised code is allowed to do. The latter may require additional machinery. What matters is whether that machinery limits the consequences of mistakes.

An audit asks whether we can find the bug. A sound boundary asks what the bug can do if we don’t.



  1. Niels Provos, Markus Friedl, and Peter Honeyman, Preventing Privilege Escalation, USENIX Security Symposium, 2003. Sections 3–4 describe the privileged monitor, unprivileged child, request protocol, and state-dependent authorization. Section 4.3 reports approximately 3,000 added lines of code, a 7% increase. Section 5 reports that approximately two-thirds of the measured code, including relevant third-party libraries, ran without special privilege; for OpenSSH’s own code the proportion was approximately three-quarters. The paper excludes resource-starvation attacks from its protection claims. These are historical measurements and architectural descriptions, not a description of every detail of current OpenSSH. See also OpenSSH’s README.privsep. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  2. Edsger W. Dijkstra, The Humble Programmer, EWD 340, 1972. Dijkstra’s discussion emphasizes human cognitive limits and the need to structure programming around them. ↩︎

  3. Theo de Raadt, pledge(): a new mitigation mechanism, slide 3, Hackfest 2015. The presentation discusses the limitations of auditing complex programs and the development of mitigations alongside audit work. See also OpenBSD’s security documentation, especially its discussion of the audit process. ↩︎ ↩︎

  4. D. L. Parnas, On the Criteria To Be Used in Decomposing Systems into Modules, Communications of the ACM 15(12), December 1972, pp. 1053–1058. The paper compares decomposition by processing steps with decomposition by information hiding and explains how two systems can have identical runnable representations while differing in interfaces, work assignments, and changeability. ↩︎

  5. OpenBSD, pledge(2). The manual describes restricting system operations through promises that can be narrowed but not restored. Most violations produce an uncatchable SIGABRT; the error promise requests ENOSYS instead, and some operations have individually specified error behavior. ↩︎

  6. OpenBSD, unveil(2). The interface restricts filesystem path access and supports locking the configured rules. Access failures include ENOENT when no applicable path rule qualifies and EACCES when the requested operation exceeds the path’s permissions. ↩︎

  7. OpenBSD, OpenBSD 5.9 release notes, March 29, 2016. The release documents 453 of 707 base-system binaries adapted to use pledge, plus 14 third-party ports. It also describes bugs and design problems discovered during adoption. ↩︎

  8. Maysara Alhindi and Joseph Hallett, Playing in the Sandbox: A Study on the Usability of Seccomp, Symposium on Usable Privacy and Security, 2025. ↩︎

  9. Jukka Ruohonen, Krzysztof Sierszecki, and Abhishek Tiwari, A Measurement Study on the Adoption of Pledges and Unveils in the OpenBSD Operating System, SANER 2026. The study examines releases from OpenBSD 5.9 through 7.7 and finds sustained growth in use of both mechanisms. ↩︎