Vivian Voss

Compiled for Everyone but You

freebsd ports security sysadmin

Somewhere in the FreeBSD installer, after the disk layout and before the first reboot, there is a menu that asks what sort of machine this is going to be. Not the hostname. Whether unprivileged users should be able to see other people's processes. Whether the kernel message buffer stays readable. Whether process IDs come out in order or scattered.

Ten questions, each a checkbox, each landing in a configuration file you can read afterwards.

Most installers stopped asking. The reasonable explanation is that most users would not know the answers, and that is fair enough. The interesting part is what the asking implies about everything downstream.

The Question at Install Time

The hardening menu in bsdinstall offers to hide processes belonging to other user IDs, to do the same for group IDs, and to hide processes running inside jails. It offers to stop unprivileged users reading the kernel message buffer through dmesg, and to switch off process debugging facilities for them. It can randomise the process ID of anything newly created. It will clean /tmp at every boot, run syslogd with no network socket open at all, put a password prompt on the console, and forbid the destructive mode of DTrace.

None of that is switched on by default. Randomised PIDs are the clearest case: OpenBSD does it from the outset, FreeBSD puts the switch in front of you and lets you decide. Ticking the boxes leaves no hidden state behind, only a handful of lines in /etc/sysctl.conf and /etc/rc.conf that you can read six months later and understand.

The hardening menu, ten switches, all off by default hide processes of other user IDs hide processes of other group IDs hide processes running inside jails block unprivileged reading of the kernel message buffer disable process debugging for unprivileged users randomise the process ID of new processes clear the tmp filesystem at boot run syslogd with no network socket open put a password prompt on the console disallow the destructive mode of DTrace Every answer lands in sysctl.conf, rc.conf or loader.conf, where it stays readable.

The menu is set out in full, screenshot and all, in chapter sixteen of Integrated by Design, page 191, where the advice is short: enable all ten.

That is a small thing on its own. It stops being small when you notice the same posture running through the entire system.

A Recipe, Not a Binary

The FreeBSD Handbook explains the ports collection with a sentence worth reading twice. Packages, it says, are normally compiled with conservative options, because they have to run on the maximum number of systems. Compiling from the port lets you change those options.

That is not a complaint about binary packages. It is a description of what a binary package is: a build made for everybody, which necessarily means a build made for nobody in particular. Somebody chose the middle of the distribution curve, and they chose well, because the alternative is a package that breaks on a machine they never saw.

What ports do is keep the recipe as the primary object. make config opens the options menu for a port. make config-recursive gathers the dependencies so you configure the whole tree in one sitting. make showconfig tells you what you chose, and make rmconfig throws it away. The build is not a black box you accept, it is a document you can interrogate.

The same idea reaches further down than most people expect. The file src.conf governs the build of the base system itself, not just the kernel, and its WITHOUT_ switches remove components before they exist. Not built, not installed, not present. There is a wireless stack you can decline, a ZFS implementation you can decline, encryption support in ed you can decline. Attack surface that was never compiled needs neither patching nor monitoring, and it never comes up at the change advisory board.

And for third-party software the hardening switches live in one file. make.conf takes WITH_PIE, WITH_RELRO, WITH_BIND_NOW, WITH_FORTIFY, WITH_STACK_AUTOINIT and WITH_ZEROREGS. Stack smashing protection is already on. For the base system and the kernel the same mechanic lives in /etc/src.conf and /etc/src-env.conf, documented in the manual page.

Here is the detail that makes it a system rather than a gesture. Not every port survives being built as a position-independent executable, a PIE in the jargon, and the ports tree knows which ones. They carry a PIE_UNSAFE marker. The knowledge of what breaks sits in the package collection, maintained by the people who maintain the ports, not in your head and not in a wiki page somebody last touched in 2019. That is also precisely why the flags are not on by default, and the FreeBSD project says so plainly in its own quarterly report.

What the Kernel Already Has

Above the build options sits an arsenal that ships whether you use it or not.

Capsicum puts a process into capability mode, where it keeps only the file descriptors it already holds and can no longer reach the global namespace. The MAC framework carries mandatory access control as loadable policy modules. A securelevel takes capabilities away from root itself as the machine settles into operation. chflags schg makes a file immutable in a way that survives being root. Event auditing is in the base system, and mtree will fingerprint a filesystem and tell you later what moved.

Then there is veriexec, which has been in the tree as mac_veriexec since FreeBSD 10 and does the thing people usually assume requires a commercial product. A signature list of paths, hashes and file types is loaded into the kernel through /dev/veriexec, and the kernel checks contents at exec() and open(). It runs at four strictnesses: complain about a mismatch, refuse access on a mismatch, refuse to let a signed file be overwritten or deleted, and at the top refuse to touch anything unsigned at all. The loader can verify a signed kernel against UEFI key databases before any of that starts.

Whether you want a machine on which nothing unsigned may execute is a real question with real operational costs. The point is that answering yes does not require leaving the operating system.

Keeping the Decision

A build option you cannot keep is a hobby. This is where the design either holds together or falls apart, and it is worth being precise, because the obvious answer is wrong.

Build a port by hand with make install and it lands in the package database like any other package, so pkg audit keeps checking it against the VuXML vulnerability database. What it does not do is survive contact with pkg upgrade, which will happily replace your carefully configured build with the official binary. The Handbook is blunt about this and advises against mixing the two.

The answer is Poudriere, and the reason it works is structural. Poudriere builds packages in clean jails from the same ports tree, taking per-jail and per-port configuration from files under /usr/local/etc/poudriere.d that are combined with the system make.conf at build time. poudriere options sets the choices, poudriere bulk builds the set, and the result is a package repository your machines install from.

The official FreeBSD package repository is built the same way, from the same tree, with the same tool. You are not stepping outside the distribution to get your own options. You are running the distribution's own machinery with your own configuration file, and you can sign the repository you produce.

The Same Problem, Answered Differently

Gentoo arrives at a similar place by another route. Its hardened profile turns on stack smashing protection with a lowered buffer threshold, position-independent executables, RELRO, immediate binding, FORTIFY_SOURCE=3, libstdc++ assertions and stack clash protection, with control-flow protection behind a USE flag. Most of it is implemented through the compiler's specs file, not by patching sources, and the operator adjusts it through CFLAGS and LDFLAGS, or per package with filter-flags in an ebuild. Different mechanism, same premise: the recipe is the thing you own.

Debian and Fedora answer a different question, and they answer it well. Debian sets hardening flags through dpkg-buildflags, documented in its wiki, and a maintainer widens them with DEB_BUILD_MAINT_OPTIONS in debian/rules. Fedora carries its flags centrally, with rpmbuild, mock and fedpkg producing packages against Koji. These are excellent tools, and they belong to the person maintaining the package.

Who sets the hardening flags FreeBSD THE OPERATOR make.conf and src.conf; Poudriere keeps them Gentoo THE OPERATOR hardened profile, CFLAGS, filter-flags per package Debian THE MAINTAINER dpkg-buildflags, DEB_BUILD_MAINT_OPTIONS in debian/rules Fedora THE MAINTAINER central macros, rpmbuild and mock against Koji One audited binary set for an enormous unknown fleet is a different task, and it is done well. A design decision, not an omission.

That is the whole of the difference, and it is a design decision, not an omission. Their job is to produce one audited binary set that runs correctly on an enormous and unknown fleet, and to push a security fix through it in hours. Recompiling several thousand packages per site would make that job impossible. The Debian hardening documentation, consistently, describes no way for an operator to change the hardening of an installed official package, because within that design there is no reason there should be one.

The Limit

Several things push back, and they deserve saying properly.

Compiling is not free. It costs machine hours and it costs attention, and a security fix that arrives as a binary in twenty minutes beats one you compile for three hours. For most fleets that trade is correct, and the distributions that made it are not being lazy.

The flexibility cuts. Set WITH_PIE globally, pull in a port that cannot take it, and you have not hardened a machine, you have broken one. The PIE_UNSAFE marker helps and does not absolve you. Mixing hand-built ports with binary packages invites exactly the mess the Handbook warns about, and Poudriere is a service to run, not a checkbox.

The comparison also has a floor under it. Modern distribution defaults are good, and much of what needed doing by hand fifteen years ago is now simply on. And nothing here is exclusive in the sense of being impossible elsewhere: Linux has SELinux and AppArmor for mandatory access control, seccomp for confinement, auditd, chattr +i. The honest difference is not existence. It is where the knowledge lives, and what it costs to acquire. One manual page and one installer menu, against a set of separate projects each carrying its own configuration language, its own documentation and its own degree of polish.

The Point

An installer that asks ten questions is not being fussy. It is admitting that the answers depend on what the machine is for, and that the person putting it there is the one who knows.

Everything that follows repeats the admission. The recipe stays readable, the options stay inspectable, the components you do not want are never compiled, the flags live in a file you can grep, the things that break under those flags are recorded by the people who maintain them, and the tool that keeps your decisions is the same tool the project uses to build its own packages.

None of that makes a machine secure. It makes the decisions visible, which is a smaller claim and a more useful one.

The conservative build is the right default, and somewhere in your estate is a machine whose job has been clear for years and whose build has never once reflected it.