How to Harden Services on FreeBSD with Capsicum
How to adapt a FreeBSD program for Capsicum by pre-opening resources, limiting descriptor rights, entering capability mode, and testing denial.
Capsicum turns file descriptors into explicit capabilities and lets a process irreversibly enter capability mode, where global namespaces such as arbitrary filesystem paths and new network destinations are unavailable. It is an application-compartmentalization API, not a switch an administrator can apply safely to an unmodified daemon. Programs must acquire required resources first, reduce descriptor rights, and delegate carefully scoped global services.
Design the compartment before calling cap_enter
Inventory everything the component needs after sandboxing: configuration files, data directories, listening sockets, DNS, syslog, time, random data, shared libraries, and child execution. Divide startup into two phases:
- privileged/resource acquisition and validation;
- restricted processing using only passed descriptors.
Capability mode does not make an existing descriptor harmless. A directory descriptor with broad lookup rights can expose a large subtree, and an already-open connected socket still reaches its peer. Close unused descriptors and apply cap_rights_limit(2) to every retained one.
Entering capability mode is irreversible for the process and inherited by descendants created with fork(2) or pdfork(2). Test error paths: code that tries to reopen a config file during reload will fail with ECAPMODE unless it was redesigned around a pre-opened descriptor or helper.
Build a minimal capability-mode program
This example opens one input file, limits it to read/stat, enters capability mode, proves a new global path is denied, and copies the input to already-open stdout:
#include <sys/capsicum.h>
#include <err.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int
main(int argc, char **argv)
{
cap_rights_t rights;
char buf[4096];
ssize_t n, off, written;
unsigned int mode;
if (argc != 2)
errx(64, "usage: %s input", argv[0]);
int fd = open(argv[1], O_RDONLY);
if (fd < 0)
err(1, "open %s", argv[1]);
cap_rights_init(&rights, CAP_READ, CAP_FSTAT);
if (cap_rights_limit(fd, &rights) < 0)
err(1, "cap_rights_limit");
if (cap_enter() < 0)
err(1, "cap_enter");
if (cap_getmode(&mode) < 0 || mode != 1)
errx(1, "not in capability mode");
errno = 0;
if (open("/etc/passwd", O_RDONLY) >= 0 || errno != ECAPMODE)
errx(1, "unexpected global namespace access");
while ((n = read(fd, buf, sizeof(buf))) > 0) {
for (off = 0; off < n; off += written) {
written = write(STDOUT_FILENO, buf + off,
(size_t)(n - off));
if (written < 0)
err(1, "write");
}
}
if (n < 0)
err(1, "read");
return (0);
}
Compile and test on FreeBSD:
cc -Wall -Wextra -O2 sandbox.c -o sandbox
./sandbox /etc/hosts
The test is meaningful because cap_getmode confirms mode inside the process and the prohibited open must fail with ECAPMODE. Do not remove the negative test after the first success; make it part of regression testing.
The example leaves stdout with its inherited rights. Production code should close unexpected descriptors and limit stdin/stdout/stderr as appropriate. It must also decide how to handle interrupted system calls, signals, and shutdown while preserving the program’s own error-reporting path.
Inspect descriptor capabilities externally
For a running adapted service, inspect descriptors and capability rights:
procstat -fC PID
-f displays descriptors and -C adds capability information. This helps detect a directory or socket retaining more rights than intended. It is not equivalent to the invalid pattern procstat -a PID | grep cap, and descriptor rights alone do not prove the process entered capability mode. Keep an internal cap_getmode assertion or expose a safe diagnostic from the application.
Use ktrace/tests carefully to exercise prohibited operations and verify they fail closed. Test reload, log rotation, DNS changes, signal handling, child processes, and shutdown, not only one successful request.
Use Casper for narrow global services
Some operations inherently require global namespaces. FreeBSD’s Casper framework provides capability channels for services such as DNS resolution, syslog, and other system lookups. An application obtains and limits a Casper service channel before or around sandbox entry, then asks that helper instead of opening global resources directly.
Use the service-specific library and limit APIs; an unrestricted Casper channel can reintroduce more authority than the compartment needs. For a network daemon, a common design has a privileged parent create/bind sockets and a capability-mode child process untrusted input. Pass only the accepted socket and required data descriptors.
Casper is not permission to ignore protocol validation. It narrows operating-system authority after memory corruption or logic compromise; it does not make malformed input safe.
Adapt a real service incrementally
Start with one parser or worker rather than moving the entire daemon into capability mode. Add tests that enumerate required operations. Pre-open configuration and data roots with openat(2)-style descriptor-relative access, then apply lookup and read/write rights no broader than necessary. Separate reload into a trusted supervisor that opens the new file and passes a descriptor.
Network clients need a broker or pre-connected socket because creating arbitrary outbound connections is a global namespace operation. Servers should bind/listen before sandboxing and pass accepted sockets to workers. Avoid passing a root directory descriptor or unrestricted management socket merely to make tests pass.
On every cap_rights_limit failure, abort compartment startup. Continuing unsandboxed after a hardening call fails converts a deployment error into silent loss of protection.
Combine Capsicum with independent controls
Capsicum limits authority, not CPU, RAM, process count, or disk quota. Use rctl(8) for resource limits, jails for broader OS/network namespace isolation, dedicated users for discretionary permissions, MAC policies where required, and PF for network policy. Patch and audit the service normally.
Do not claim a package supports Capsicum because libcasper exists on the host. Verify upstream source, release documentation, negative tests, and running descriptor rights. A library cannot sandbox a daemon that never calls it.
The successful outcome is an application architecture where untrusted input reaches a worker holding only the exact descriptors and operations it needs. cap_enter() is the final boundary; the real security work is deciding what authority crosses it.
Related:
- Capsicum Beyond the Basics: Building Capability-Mode Services From Scratch
- How to Set Up Automated Security Audits on FreeBSD
Sources: