<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>*bsd</title>
    <link>https://scrolls.anduin.net/bsd/</link>
    <description>Not A Monoculture</description>
    <pubDate>Wed, 26 Aug 2026 08:33:03 +0000</pubDate>
    <item>
      <title>FreeBSD: Wrapped jails</title>
      <link>https://scrolls.anduin.net/bsd/freebsd-wrapped-jails</link>
      <description>&lt;![CDATA[As my first post here, I&#39;ll try to describe, and perhaps rationalise, how I prefer to run most of my jails.&#xA;&#xA;Background&#xA;In case anyone reading this doesn&#39;t already know, FreeBSD is a Unix-like operating system tracing its heritage back to the original Berkeley Software Distribution (BSD) from the late 70s. Fast forward to the early &#39;00s and the release of FreeBSD 4.0, and we got a new toy to play with: Jails.&#xA;&#xA;In many ways, jails were the precursor and at least partially the inspiration for several other implementations of OS-level virtualization, with Solaris and Docker) being fairly well-known examples. While the implementations differ significantly, the basic idea is the same: Virtualize OS services and resources, but not the hardware, thus achieveing much more frugal virtualization than full-blown VMs.&#xA;&#xA;Basics&#xA;Historically, FreeBSD jails were quite simple in their implementation: Aside from separating process spaces (grouping processes and preventing a jail from seeing processes in other jails or on the host) and a rudimentary IP address delegation mechanism, it was essentially (though perhaps not technically) an extension of the chroot mechanism that was first introduced in BSD as early as 1979 - admittedly in a much different shape than today.&#xA;&#xA;Today, this has changed quite a bit, with perhaps the most interesting change being the introduction of VIMAGE, a framework for virtualizing kernel subsystems, and the VNET facility built using VIMAGE, which provides a separate, virtualized network stack to jails.&#xA;&#xA;Add some other killer FreeBSD- and jail-specific features like ZFS and hierarchical, or nested, jails, the PF firewall, and you have the building blocks for some really interesting things!&#xA;&#xA;The problem with classic jails&#xA;Classic jails are nice because the lack of a network stack can be considered a security feature: The processes in the jail, even those running as root, are completely isolated from touching the network stack, which means they cannot do anything useful with ifconfig(8), can&#39;t modify routing tables, and are generally strictly limited to the IP address(es) you assign to the jail.&#xA;&#xA;Unfortunately, this comes with some drawbacks: Running ifconfig in the jail will reveal much about the network configuration on the host: It will basically give you the full output for the interface(s) on which the jail has IPs, minus the other IPs that are defined there. Consider the following:&#xA;&#xA;$  ifconfig                                                                                                             &#xA;ix0: flags=1008943UP,BROADCAST,RUNNING,PROMISC,SIMPLEX,MULTICAST,LOWERUP metric 0 mtu 1500&#xA;options=a538b9RXCSUM,VLANMTU,VLANHWTAGGING,JUMBOMTU,VLANHWCSUM,WOLUCAST,WOLMCAST,WOLMAGIC,VLANHWFILTER,VLANHWTSO,RXCSUMIPV6,HWSTATS&#xA;        ether ac:1f:6b:62:17:1c&#xA;        media: Ethernet autoselect (10Gbase-T full-duplex)&#xA;        status: active&#xA;ix1: flags=8802BROADCAST,SIMPLEX,MULTICAST metric 0 mtu 1500&#xA;options=4e53fbbRXCSUM,TXCSUM,VLANMTU,VLANHWTAGGING,JUMBOMTU,VLANHWCSUM,TSO4,TSO6,LRO,WOLUCAST,WOLMCAST,WOLMAGIC,VLANHWFILTER,VLANHWTSO,RXCSUMIPV6,TXCSUMIPV6,HWSTATS,MEXTPG&#xA;        ether ac:1f:6b:62:17:1d&#xA;        media: Ethernet autoselect&#xA;        status: no carrier&#xA;lo0: flags=1008049UP,LOOPBACK,RUNNING,MULTICAST,LOWERUP metric 0 mtu 16384&#xA;options=680003RXCSUM,TXCSUM,LINKSTATE,RXCSUMIPV6,TXCSUMIPV6&#xA;        groups: lo&#xA;pflog0: flags=0 metric 0 mtu 33152&#xA;        options=0&#xA;        groups: pflog&#xA;ngeth0: flags=1008943UP,BROADCAST,RUNNING,PROMISC,SIMPLEX,MULTICAST,LOWERUP metric 0 mtu 1500&#xA;        options=28VLANMTU,JUMBOMTU&#xA;        ether 58:9c:fc:10:64:f9&#xA;        media: Ethernet autoselect (1000baseT full-duplex)&#xA;        status: active&#xA;bridge0: flags=1008843UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST,LOWERUP metric 0 mtu 1500&#xA;        options=10VLANHWTAGGING&#xA;        ether ac:1f:6b:62:17:1c&#xA;        inet 10.1.2.3 netmask 0xffffff00 broadcast 10.1.2.255&#xA;        id 00:00:00:00:00:00 priority 32768 hellotime 2 fwddelay 15&#xA;        maxage 20 holdcnt 6 proto rstp maxaddr 2000 timeout 1200&#xA;        root id 00:00:00:00:00:00 priority 32768 ifcost 0 port 0&#xA;        bridge flags=0&#xA;        member: epair2a flags=143LEARNING,DISCOVER,AUTOEDGE,AUTOPTP&#xA;                port 15 priority 128 path cost 2000 vlan protocol 802.1q&#xA;        member: epair1a flags=143LEARNING,DISCOVER,AUTOEDGE,AUTOPTP&#xA;                port 11 priority 128 path cost 2000 vlan protocol 802.1q&#xA;        member: epair0a flags=143LEARNING,DISCOVER,AUTOEDGE,AUTOPTP&#xA;                port 7 priority 128 path cost 2000 vlan protocol 802.1q&#xA;        member: ngeth0 flags=143LEARNING,DISCOVER,AUTOEDGE,AUTOPTP&#xA;                port 5 priority 128 path cost 20000 vlan protocol 802.1q&#xA;        member: ix0 flags=143LEARNING,DISCOVER,AUTOEDGE,AUTOPTP&#xA;                port 1 priority 128 path cost 2000 vlan protocol 802.1q&#xA;        groups: bridge&#xA;epair0a: flags=1008943UP,BROADCAST,RUNNING,PROMISC,SIMPLEX,MULTICAST,LOWERUP metric 0 mtu 1500&#xA;        description: jail1&#xA;        options=200019RXCSUM,VLANMTU,VLANHWTAGGING,RXCSUMIPV6&#xA;        ether 58:9c:fc:10:e6:88&#xA;        groups: epair&#xA;        media: Ethernet 10Gbase-T (10Gbase-T full-duplex)&#xA;        status: active&#xA;epair1a: flags=1008943UP,BROADCAST,RUNNING,PROMISC,SIMPLEX,MULTICAST,LOWERUP metric 0 mtu 1500&#xA;        description: jail2&#xA;        options=200019RXCSUM,VLANMTU,VLANHWTAGGING,RXCSUMIPV6&#xA;        ether 58:9c:fc:10:98:28&#xA;        groups: epair&#xA;        media: Ethernet 10Gbase-T (10Gbase-T full-duplex)&#xA;        status: active&#xA;epair2a: flags=1008943UP,BROADCAST,RUNNING,PROMISC,SIMPLEX,MULTICAST,LOWERUP metric 0 mtu 1500&#xA;        description: jail3&#xA;        options=200019RXCSUM,VLANMTU,VLANHWTAGGING,RXCSUMIPV6&#xA;        ether 58:9c:fc:10:22:09&#xA;        groups: epair&#xA;        media: Ethernet 10Gbase-T (10Gbase-T full-duplex)&#xA;        status: active&#xA;This ifconfig invocation is from within a classic jail running on the same host as a number of VNET jails. Despite not being able to modify anything, I can still see a lot of detail about the host that, one would think, the jail should not be privy to: All the physical and virtual interfaces on the machine, including lo0 (localhost), the bridge0 interface and all the bridge members, the primary interface ix0 and its twin ix1, the pflog0 interface revealing we&#39;ve got pf running, ngeth0 showing there&#39;s netgraph(4) in use, etc.&#xA;&#xA;And perhaps worst of all: The individual epair interfaces for the VNET jails, including their descriptions which - in a well-assembled system - could give away details about what and who else exists on the host.&#xA;&#xA;Clearly, this is not the prison I was hoping for.&#xA;&#xA;The problem with VNET jails&#xA;Quite the opposite of classic jails, VNET jails provide a virtualised network stack. This can be connected to the outside world with a virtual cable, by way of the epair interface type, connecting it to a bridge interface on the host. Or you can assign separate physical interfaces, VLAN interfaces, or practically any other interface type. Either way, the jail controls that interface in practically every way, in addition to the routing table (or tables), packet filtering, and most other aspects of the network stack.&#xA;&#xA;This is obviously very powerful, and provides some features that classic jails are (sorely?) missing:&#xA;&#xA;An actual localhost interface! Classic jails don&#39;t have this, there are just some code shortcuts in the kernel that allow applications that bind to localhost to .. not fail, but there are several situations where this is far from optimal or even desired.&#xA;The ability to run stuff like nmap(1) from the jail, since it fiddles with routing tables and creates raw network packets.&#xA;Firewalling with pf(4) (or possibly others; I have not played with those). This includes basic filtering, NAT, rate limiting, and all the other goodness offered.&#xA;Several network-related sysctl(8) settings can be tuned on a per-jail basis, like datagram sizes, routing settings, TCP congestion control algorithm, etc.&#xA;&#xA;However: Do you really want all the processes in your jail to be this close to all those foot cannons? A simple misconfiguration, local privilege escalation bug, or simply fat-fingering something yourself can wreck havoc on the jail&#39;s network configuration - and there is a definite possibility of inflicting damage on other jails, the host, and even other devices on your network.&#xA;&#xA;The limitations of a classic jail suddenly start looking an awful lot like features, don&#39;t they?&#xA;&#xA;A possible solution&#xA;Without more ado (there&#39;s enough of that above), here&#39;s how we&#39;ve solved this at work: We &#34;wrap&#34; a classic jail inside a VNET jail.&#xA;&#xA;Here&#39;s how:&#xA;&#xA;1. Create the jail template&#xA;The easiest way is to fetch base.txz from the FreeBSD download servers, and estract it in a suitable directory:&#xA;&#xA;Create template directory. If you use ZFS, a dataset might be preferable:&#xA;mkdir -p /data/jails/template.151&#xA;&#xA;Fetch and extract&#xA;fetch -o - https://download.freebsd.org/ftp/releases/amd64/15.1-RELEASE/base.txz \&#xA;    | tar --unlink -xpzf - -C /data/jails/template.151&#xA;&#xA;Create a symlink for use in fstab(5)&#xA;ln -s /data/jails/template.151 /data/jails/current&#xA;&#xA;2. Create fstab files&#xA;For the parent and child jails, create fstab(5) files to nullfs(4)-mount the parts of the base you need into the jails. An added benefit is that this makes the base OS read-only inside the jails, which is probably a good thing:&#xA;&#xA;/etc/fstab.testjail.parent&#xA;/data/jails/current/etc /data/jails/testjail/etc nullfs ro&#xA;/data/jails/current/bin /data/jails/testjail/bin nullfs ro&#xA;/data/jails/current/lib /data/jails/testjail/lib nullfs ro&#xA;/data/jails/current/sbin /data/jails/testjail/sbin nullfs ro&#xA;/data/jails/current/libexec /data/jails/testjail/libexec nullfs ro&#xA;/data/jails/current/usr/sbin /data/jails/testjail/usr/sbin nullfs ro&#xA;&#xA;/etc/fstab.testjail&#xA;/data/jails/current/bin /data/jails/testjail/root/bin nullfs ro&#xA;/data/jails/current/lib /data/jails/testjail/root/lib nullfs ro&#xA;/data/jails/current/usr/share /data/jails/testjail/root/usr/share nullfs ro&#xA;/data/jails/current/libexec /data/jails/testjail/root/libexec nullfs ro&#xA;/data/jails/current/sbin /data/jails/testjail/root/sbin nullfs ro&#xA;/data/jails/current/usr/bin /data/jails/testjail/root/usr/bin nullfs ro&#xA;/data/jails/current/usr/include /data/jails/testjail/root/usr/include nullfs ro&#xA;/data/jails/current/usr/lib /data/jails/testjail/root/usr/lib nullfs ro&#xA;/data/jails/current/usr/libdata /data/jails/testjail/root/usr/libdata nullfs ro&#xA;/data/jails/current/usr/libexec /data/jails/testjail/root/usr/libexec nullfs ro&#xA;/data/jails/current/usr/sbin /data/jails/testjail/root/usr/sbin nullfs ro&#xA;&#xA;3. Create the directory structures for the jails&#xA;This is all greatly simplified, but it should give you a starting point. Note how the child jail lives in a subdirectory (/root) inside the parent jail:&#xA;&#xA;Create the parent jail directory structure&#xA;for dir in \&#xA;    root dev etc bin lib sbin libexec \&#xA;    usr/sbin /var/log /var/run /var/empty&#xA;do&#xA;    mkdir -p /data/jails/testjail/$dir&#xA;done&#xA;&#xA;Copy some files into the parent jail; necessary for pf to work&#xA;cp /data/jails/template.151/etc/pf.os /data/jails/testjail/etc/&#xA;cp /data/jails/template.151/etc/protocols /data/jails/testjail/etc/&#xA;&#xA;Create the child jail directory structure&#xA;for dir in \&#xA;    root dev bin lib sbin libexec tmp \&#xA;    usr/sbin usr/bin usr/include usr/lib usr/libdata \&#xA;    usr/libexec usr/share /usr/local/etc&#xA;do&#xA;    mkdir -p /data/jails/testjail/root/$dir&#xA;done&#xA;&#xA;Copy basic /etc and /var to the child jail&#xA;cp -Rp /data/jails/template.151/etc /data/jails/testjail/root/&#xA;cp -Rp /data/jails/template.151/var /data/jails/testjail/root/&#xA;&#xA;Using ZFS instead&#xA;The mkdir parts for the child jail could be modified to use ZFS (which is what I&#39;d do). This is a skeleton suggestion, but you&#39;d do well in forming your own strategy for how to approach this with ZFS. Personally I see no point using ZFS datasets for the parent jail, as it is largely ephemeral anyway:&#xA;&#xA;Create the child jail datasets (zfs &#39;create -p&#39; acts like &#39;mkdir -p&#39;)&#xA;for dir in \&#xA;    root usr/local usr/home tmp var etc&#xA;do&#xA;    zfs create -p data/jails/testjail/root/$dir&#xA;done&#xA;&#xA;Create directories in the datasets&#xA;for dir in \&#xA;    dev bin lib sbin libexec \&#xA;    usr/sbin usr/bin usr/include usr/lib usr/libdata \&#xA;    usr/libexec usr/share /usr/local/etc&#xA;do&#xA;    mkdir -p /data/jails/testjail/root/$dir&#xA;done&#xA;&#xA;One useful trick I&#39;ve learned is to keep /var/log inside the child jail in a ZFS dataset homed outside the child jail&#39;s path, and simply set its mountpoint to be inside the jail. In case you need to roll back the dataset(s) in the child jail, you can safely do so without erasing your logs. Security people and auditors may even come to appreciate this:&#xA;&#xA;Create the dataset; make sure it always has space to write logs even if the&#xA;rest of the datasets have run out of space:&#xA;zfs create -p \&#xA;    -o reservation=64M \&#xA;    -o mountpoint=/data/jails/testjail/root/var/log \&#xA;    data/jails/testjail/logs&#xA;&#xA;4. Start the jails&#xA;We start by creating the epair interfaces. Assuming there is already a bridge0 to attach the dangling cable^Winterface to:&#xA;ifa=&#34;$(ifconfig epair create)&#34;&#xA;ifb=&#34;${ifa%a}b&#34;&#xA;ifconfig bridge0 addm $ifa&#xA;ifconfig $ifa up&#xA;&#xA;Next, we start the parent jail. Note this is crude; a complete jail.conf entry should take care to unmount filesystems on jail shutdown, avoiding mounting things twice, and various other housekeeping:&#xA;Mount the parent filesystems&#xA;mount -a -F /etc/fstab.testjail.parent&#xA;&#xA;Start the jail - set children.max=1 to allow the child jail&#xA;jail -c path=/data/jails/testjail name=testjail children.max=1 vnet persist&#xA;&#xA;$ifa and $ifb is from the interface create script&#xA;ifconfig $ifb vnet testjail&#xA;jexec testjail ifconfig lo0 127.0.0.1/8&#xA;jexec testjail ifconfig $ifb 10.2.3.4/24&#xA;jexec testjail route add default 10.2.3.1&#xA;&#xA;Mount devfs in the jails&#xA;mount -t devfs -oruleset=4 . /data/jails/testjail/dev&#xA;mount -t devfs -oruleset=4 . /data/jails/testjail/root/dev&#xA;&#xA;Now that we have the parent jail running, starting the child jail is a matter of&#xA;Mount the child filesystems&#xA;mount -a -F /etc/fstab.testjail.parent&#xA;&#xA;Start the child jail&#xA;jexec testjail jail -c ip4=inherit path=/root name=child exec.start=&#34;/bin/sh /etc/rc autoboot&#34;&#xA;&#xA;At this point you should be able to jexec testjail.child su - and find yourself in a bare, but functional, jail. Run ifconfig to see that only network interfaces pertaining to testjail are visible, and confirm that you cannot make changes. List the jails with jls; it should show two jails - the parent and the child.&#xA;&#xA;Stopping the jails&#xA;Since we&#39;re cheating and doing everything from the command line here, stopping the jails takes a few steps:&#xA;&#xA;Stop the child jail by killing all the processes&#xA;jexec testjail.child kill -9 -1&#xA;&#xA;In case there are lingering TCP sockets preventing the child jail from dying,&#xA;use tcpdrop(8) to clean up:&#xA;jexec testjail tcpdrop -a&#xA;&#xA;Destroy the parent jail&#xA;jail -r testjail&#xA;&#xA;Destroy the epair interfaces (find the name using ifconfig if needed; both&#xA;the &#39;a&#39; and &#39;b&#39; of the pair should now be visible on the host:&#xA;ifconfig $ifa destroy&#xA;&#xA;Finally, unmount the filesystems&#xA;umount -f /data/jails/testjail/root/dev&#xA;umount -f /data/jails/testjail/dev&#xA;umount -F /etc/fstab.testjail -a&#xA;umount -F /etc/fstab.testjail.parent -a&#xA;&#xA;What now?&#xA;All of the above, while lengthy, is hardly elegant. Some additional steps may be desirable, e.g. to enable pf, creating a &#34;black hole&#34; around the child jail, and other niceties. A jail.conf taking care of these steps should also be created, which will require a helper script or two.&#xA;&#xA;Enabling the packet filter&#xA;In order to use pf, we need a pf.conf file, and some additional device nodes and optionally the pflog interface in the parent jail.&#xA;&#xA;The pf.conf file really only needs a single line for the purpose of this demonstration. We&#39;re adding log here for pflog to have any work to do. Place it in the root of the parent jail, like so:&#xA;&#xA;echo &#34;pass log all&#34;   /data/jails/testjail/pf.conf&#xA;&#xA;Then we need to unhide a couple of device nodes in the parent jail, load the rules, and start pflogd. This should be done after starting the parent jail, but before starting the child jail:&#xA;devfs -m /data/jails/testjail/dev rule apply path pf unhide&#xA;devfs -m /data/jails/testjail/dev rule apply path bpf unhide&#xA;jexec testjail pfctl -F all -vf /pf.conf &amp;&amp; pfctl -e&#xA;&#xA;Starting pflogd, excluding some particularly noisy traffic&#xA;jexec testjail pflogd &#39;not host 224.0.0.18&#39;&#xA;&#xA;At this point, if you plan to keep a configuration like this, you may want to make sure you rotate the pflogd files automatically, by adding this to /usr/local/etc/newsyslog.d/jails.conf (or anywhere else you prefer):&#xA;/data/jails/testjail/var/log/pflog root:wheel 640 7  $D0 B /data/jails/nextcloud/var/run/pflogd.pid&#xA;Ask me how I know. :-P&#xA;&#xA;The Black Hole&#xA;One of the more &#34;interesting&#34; possibilities with this approach is that after the child jail has been started, there really is no reason to keep the parent jail around. Of course it needs to exist, but most of the filesystems (i.e binaries and libraries) are really not necessary. While nowhere near an actual black hole (you can still access the child jail&#39;s files), it has a non-zero value as an obfuscation.&#xA;&#xA;Following startup of the child jail and all the parent jail services (pflogd, etc.), you can simply unmount all the parent filesystems:&#xA;umount -F /etc/fstab.testjail.parent&#xA;&#xA;However, a side effect of this is that you&#39;ll no longer be able to modify the running configuration (update pf rules, etc.). You can no longer simply jexec into the jail - no libraries or binaries are available in their expected locations.&#xA;&#xA;To get around this, you can use some variation of the following:&#xA;LDLIBRARYPATH=/root/lib jexec testjail /root/libexec/ld-elf.so.1 /root/sbin/pfctl -F all -f /pf.conf&#xA;This will set LDLIBRARYPATH to the child jail&#39;s path, and invoke ld-elf.so.1 directly (with the binary as argument), since it doesn&#39;t exist in /libexec any longer.&#xA;&#xA;Be aware, however, that doing this will expose you to potentially tainted files in the child jail. An alternate approach would be to mount and unmount the parent filesystems, e.g.&#xA;mount -F /etc/fstab.testjail.parent -a&#xA;jexec testjail command&#xA;umount -F /etc/fstab.testjail.parent -a&#xA;Then again, since these base OS directories are read-only mounted from the template, this should be a minimal risk. If you&#39;re attempting to run programs/libraries that live in read/write-mounted directories in the child jail, however - beware.&#xA;&#xA;Putting it all together (sort of)&#xA;What&#39;s missing now is of course the jail.conf entry. With this in place, you can use the regular&#xA;service jail start [name] and&#xA;service jail stop [name]&#xA;commands to manage your jails, and you can have them automatically start on system boot.&#xA;&#xA;Correctly assembling such a configuration is a fairly involved task, but I&#39;ll leave you with an example that should help you get started. Good luck!&#xA;&#xA;/etc/jail.conf&#xA;Some defaults&#xA;The default devfs ruleset - this is a default /etc/defaults/devfs.rules&#xA;devfsruleset = 4;&#xA;We want the venerable rc(8) to manage the child jails&#xA;exec.start = &#34;/bin/sh /etc/rc autoboot&#34;;&#xA;Set a high securelevel for the child jails (see security(7))&#xA;$childsecurelevel = 4;&#xA;Disallow modifying hostnames after creating jails&#xA;allow.sethostname = 0;&#xA;Clean the environment&#xA;exec.clean;&#xA;&#xA;Our test jail starts here&#xA;testjail {&#xA;    # Set a variable for use later - a run directory for some housekeeping&#xA;    $rundir = &#34;/var/run/jails/testjail&#34;;&#xA;&#xA;    # Basic settings for the parent jail&#xA;    host.hostname = &#34;testjail.PARENT&#34;;&#xA;    path = &#34;/data/jails/testjail&#34;;&#xA;    vnet;&#xA;    # Setting a high value here, so we can use service jails (see rc.conf(5))&#xA;    children.max = 21;&#xA;&#xA;    # The fstab for the parent, will be automatically mounted on startup. Also&#xA;    # make sure we can mount filesystems inside the parent&#xA;    mount.fstab = &#34;/etc/fstab.bbs&#34;;&#xA;    allow.mount;&#xA;    allow.mount.devfs;&#xA;    # Allow inspecting ZFS snapshots from within the child jail (optional)&#xA;    zfs.mountsnapshot = 1;&#xA;    # In case you want PostgreSQL or something&#xA;    allow.sysvipc = 1;&#xA;    # Keep a console log for the jail&#xA;    exec.consolelog = &#34;/var/log/jail${name}console.log&#34;;&#xA;&#xA;    # Cleanup before starting the jail (In case a previous attempt failed)&#xA;    exec.prepare = &#34;umount -f -F /etc/fstab.testjail -a || :&#34;;&#xA;    exec.prepare += &#34;umount -f -F /etc/fstab.testjail.parent -a || :&#34;;&#xA;    exec.prepare += &#34;umount -f ${path}/root/dev || :&#34;;&#xA;    exec.prepare += &#34;umount -f ${path}/dev || :&#34;;&#xA;&#xA;    ### Starting the jail&#xA;    ## Pre-start commands&#xA;    # Create rundir on host&#xA;    exec.prestart = &#34;mkdir -p ${rundir}&#34;;&#xA;    # Create the epair interface; put the names of each end in files in $rundir&#xA;    exec.prestart += &#34;ifconfig epair create   ${rundir}/ifa&#34;&#xA;    exec.prestart += &#34;sed &#39;s/a$/b/&#39;  ${rundir}/ifa  ${rundir}/ifb&#34;&#xA;    # Mount parent jail filesystems&#xA;    exec.prestart += &#34;mount -F /etc/fstab.${name}.parent -a || :&#34;;&#xA;    # Mount devfs in parent jail&#xA;    exec.prestart += &#34;mount -t devfs -oruleset=${devfsruleset} . ${path}/dev&#34;;&#xA;    # Unhide pf and bpf device in parent jail&#xA;    exec.prestart += &#34;devfs -m ${path}/dev rule apply path pf unhide&#34;;&#xA;    exec.prestart += &#34;devfs -m ${path}/dev rule apply path bpf unhide&#34;;&#xA;&#xA;    ## Post-create commands (run after jail created, but before exec.start)&#xA;    # Mount devfs in the child jail with appropriate ruleset&#xA;    exec.created = &#34;mount -t devfs -oruleset=${devfsruleset} . ${path}/root/dev&#34;;&#xA;    # Assign jail-side interface to jail&#xA;    exec.created += &#34;ifconfig cat ${rundir}/ifb vnet ${name}&#34;;&#xA;    # Assign IP address to jail interface&#xA;    exec.created += &#34;jexec ${name} ifconfig $(cat ${rundir}/ifb) inet 10.2.3.4/24&#34;;&#xA;    # Add default route&#xA;    exec.created += &#34;jexec ${name} route add default 10.2.3.1&#34;;&#xA;&#xA;    ## Startup commands&#xA;    # Assign IP to lo0-interface&#xA;    exec.start = &#34;ifconfig lo0 127.0.0.1/8&#34;;&#xA;    # Load PF rules for parent jail and enable&#xA;    exec.start += &#34;pfctl -F all -vf /pf.conf &amp;&amp; pfctl -e&#34;;&#xA;    # Start pflogd&#xA;    exec.start += &#34;/sbin/pflogd &#39;not host 224.0.0.18&#39;&#34;;&#xA;    # Wait for ping to jail (interface plumbing); assuming this file is&#xA;    # autogenerated and the IP includes the netmask&#xA;    exec.start += &#34;ip=&#39;10.1.2.3/24&#39; ; ping -n -c 15 -t 20 -o \${ip%/}&#34;;&#xA;    # Set sysctls (if any are configured)&#xA;    exec.start += &#34;(test -s /sysctl.conf &amp;&amp; sysctl -f /sysctl.conf) || :&#34;;&#xA;&#xA;    ## Create child jail&#xA;    exec.start += &#34;jail -c ip4=inherit path=/root host.hostname=testjail \&#xA;                   name=child zfs.mountsnapshot=1 children.max=20 \&#xA;                   securelevel=${childsecurelevel} \&#xA;                   sysvmsg=\&#34;new\&#34; sysvsem=\&#34;new\&#34; sysvshm=\&#34;new\&#34; \&#xA;                   exec.start=\&#34;/bin/sh /etc/rc autoboot\&#34;&#34;;&#xA;&#xA;    ## Post-start commands&#xA;    # Unount parent jail filesystems&#xA;    exec.poststart += &#34;umount -F /etc/fstab.${name}.parent -a -f || :&#34;;&#xA;&#xA;    ### Stopping the jail&#xA;    ## Pre-stop commands&#xA;    # Mount parent jail filesystems&#xA;    exec.prestop = &#34;mount -F /etc/fstab.${name}.parent -a&#34;;&#xA;&#xA;    ## Shutdown commands&#xA;    # Shut down child jail cleanly, if possible&#xA;    exec.stop = &#34;jexec child /bin/sh /etc/rc.shutdown ; sleep 1&#34;;&#xA;    # Forcibly remove child jail, just in case&#xA;    exec.stop += &#34;jls -d name|chroot /root grep -q &#39;^${name}.child$&#39; &amp;&amp; jail -Rd ${name}.child || :&#34;;&#xA;    # Remove any lingering TCP sockets&#xA;    exec.stop += &#34;tcpdrop -a || :&#34;;&#xA;&#xA;    ## Post-stop commands&#xA;    # Unount parent jail filesystems&#xA;    exec.poststop = &#34;umount -F /etc/fstab.${name}.parent -a -f || :&#34;;&#xA;    # Unmount devfs&#xA;    exec.poststop += &#34;umount -f ${path}/root/dev || :&#34;;&#xA;    # Unount parent jail dev filesystem&#xA;    exec.poststop += &#34;umount -f ${path}/dev || :&#34;;&#xA;    # Finally, destroy jail interfaces&#xA;    exec.poststop += &#34;ifconfig cat ${rundir}/ifb destroy&#34;;&#xA;&#xA;    # Clean up rundir&#xA;    exec.poststop += &#34;rm -Rf ${rundir}&#34;;&#xA;&#xA;div style=&#34;display: flex; place-content: center;&#34;table style=&#34;width: 80%; border: none; font-size: 0.8em;&#34;&#xD;&#xA;tr style=&#34;border: none;&#34;&#xD;&#xA;td align=&#34;center&#34; style=&#34;border: none;&#34; colspan=&#34;2&#34;hr&#xD;&#xA;~ love over gold ~/td&#xD;&#xA;/tr&#xD;&#xA;tr style=&#34;border: none; background-color: inherit;&#34;&#xD;&#xA;td align=&#34;left&#34; style=&#34;border: none; padding-left: 2em;&#34;a href=&#34;http://floppy.museum&#34;The Floppy Museum/a/td&#xD;&#xA;td align=&#34;right&#34; style=&#34;border: none; padding-right: 2em;&#34;a href=&#34;http://floppy.museum/thispc.htm&#34;About the Museum/a/td&#xD;&#xA;/tr/table/div]]&gt;</description>
      <content:encoded><![CDATA[<p>As my first post here, I&#39;ll try to describe, and perhaps rationalise, how I prefer to run most of my jails.</p>

<h2 id="background">Background</h2>

<p>In case anyone reading this doesn&#39;t already know, <a href="https://en.wikipedia.org/wiki/FreeBSD" rel="nofollow">FreeBSD</a> is a <a href="https://en.wikipedia.org/wiki/Unix" rel="nofollow">Unix</a>-like operating system tracing its heritage back to the original <a href="https://en.wikipedia.org/wiki/Berkeley_Software_Distribution" rel="nofollow">Berkeley Software Distribution</a> (BSD) from the late 70s. Fast forward to the early &#39;00s and the release of FreeBSD 4.0, and we got a new toy to play with: <a href="https://en.wikipedia.org/wiki/FreeBSD_jail" rel="nofollow">Jails</a>.</p>

<p>In many ways, jails were the precursor and at least partially the inspiration for several other implementations of <a href="https://en.wikipedia.org/wiki/OS-level_virtualization" rel="nofollow">OS-level virtualization</a>, with <a href="https://en.wikipedia.org/wiki/Solaris_Containers" rel="nofollow">Solaris</a> and <a href="https://en.wikipedia.org/wiki/Docker_(software)" rel="nofollow">Docker</a> being fairly well-known examples. While the implementations differ significantly, the basic idea is the same: Virtualize OS services and resources, but not the hardware, thus achieveing much more frugal virtualization than full-blown VMs.</p>

<h2 id="basics">Basics</h2>

<p>Historically, FreeBSD jails were quite simple in their implementation: Aside from separating process spaces (grouping processes and preventing a jail from seeing processes in other jails or on the host) and a rudimentary IP address delegation mechanism, it was essentially (though perhaps not technically) an extension of the <a href="https://en.wikipedia.org/wiki/Chroot" rel="nofollow">chroot</a> mechanism that was first introduced in BSD as early as 1979 – admittedly in a much different shape than today.</p>

<p>Today, this has changed quite a bit, with perhaps the most interesting change being the introduction of <a href="https://wiki.freebsd.org/VIMAGE" rel="nofollow">VIMAGE</a>, a framework for virtualizing kernel subsystems, and the <a href="https://docs.freebsd.org/en/books/handbook/jails/#creating-vnet-jail" rel="nofollow">VNET</a> facility built using VIMAGE, which provides a separate, virtualized network stack to jails.</p>

<p>Add some other killer FreeBSD- and jail-specific features like <a href="https://docs.freebsd.org/en/books/handbook/zfs/" rel="nofollow">ZFS</a> and <a href="https://docs.freebsd.org/en/books/handbook/jails/#hierarchical-jails" rel="nofollow">hierarchical, or nested, jails</a>, the <a href="https://docs.freebsd.org/en/books/handbook/firewalls/#firewalls-pf" rel="nofollow">PF firewall</a>, and you have the building blocks for some really interesting things!</p>

<h2 id="the-problem-with-classic-jails">The problem with classic jails</h2>

<p>Classic jails are nice because the lack of a network stack can be considered a security feature: The processes in the jail, even those running as root, are completely isolated from touching the network stack, which means they cannot do anything useful with <code>ifconfig(8)</code>, can&#39;t modify routing tables, and are generally strictly limited to the IP address(es) you assign to the jail.</p>

<p>Unfortunately, this comes with some drawbacks: Running <code>ifconfig</code> in the jail will reveal much about the network configuration on the host: It will basically give you the full output for the interface(s) on which the jail has IPs, minus the other IPs that are defined there. Consider the following:</p>

<pre><code>$  ifconfig                                                                                                             
ix0: flags=1008943&lt;UP,BROADCAST,RUNNING,PROMISC,SIMPLEX,MULTICAST,LOWER_UP&gt; metric 0 mtu 1500
options=a538b9&lt;RXCSUM,VLAN_MTU,VLAN_HWTAGGING,JUMBO_MTU,VLAN_HWCSUM,WOL_UCAST,WOL_MCAST,WOL_MAGIC,VLAN_HWFILTER,VLAN_HWTSO,RXCSUM_IPV6,HWSTATS&gt;
        ether ac:1f:6b:62:17:1c
        media: Ethernet autoselect (10Gbase-T &lt;full-duplex&gt;)
        status: active
ix1: flags=8802&lt;BROADCAST,SIMPLEX,MULTICAST&gt; metric 0 mtu 1500
options=4e53fbb&lt;RXCSUM,TXCSUM,VLAN_MTU,VLAN_HWTAGGING,JUMBO_MTU,VLAN_HWCSUM,TSO4,TSO6,LRO,WOL_UCAST,WOL_MCAST,WOL_MAGIC,VLAN_HWFILTER,VLAN_HWTSO,RXCSUM_IPV6,TXCSUM_IPV6,HWSTATS,MEXTPG&gt;
        ether ac:1f:6b:62:17:1d
        media: Ethernet autoselect
        status: no carrier
lo0: flags=1008049&lt;UP,LOOPBACK,RUNNING,MULTICAST,LOWER_UP&gt; metric 0 mtu 16384
options=680003&lt;RXCSUM,TXCSUM,LINKSTATE,RXCSUM_IPV6,TXCSUM_IPV6&gt;
        groups: lo
pflog0: flags=0 metric 0 mtu 33152
        options=0
        groups: pflog
ngeth0: flags=1008943&lt;UP,BROADCAST,RUNNING,PROMISC,SIMPLEX,MULTICAST,LOWER_UP&gt; metric 0 mtu 1500
        options=28&lt;VLAN_MTU,JUMBO_MTU&gt;
        ether 58:9c:fc:10:64:f9
        media: Ethernet autoselect (1000baseT &lt;full-duplex&gt;)
        status: active
bridge0: flags=1008843&lt;UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST,LOWER_UP&gt; metric 0 mtu 1500
        options=10&lt;VLAN_HWTAGGING&gt;
        ether ac:1f:6b:62:17:1c
        inet 10.1.2.3 netmask 0xffffff00 broadcast 10.1.2.255
        id 00:00:00:00:00:00 priority 32768 hellotime 2 fwddelay 15
        maxage 20 holdcnt 6 proto rstp maxaddr 2000 timeout 1200
        root id 00:00:00:00:00:00 priority 32768 ifcost 0 port 0
        bridge flags=0&lt;&gt;
        member: epair2a flags=143&lt;LEARNING,DISCOVER,AUTOEDGE,AUTOPTP&gt;
                port 15 priority 128 path cost 2000 vlan protocol 802.1q
        member: epair1a flags=143&lt;LEARNING,DISCOVER,AUTOEDGE,AUTOPTP&gt;
                port 11 priority 128 path cost 2000 vlan protocol 802.1q
        member: epair0a flags=143&lt;LEARNING,DISCOVER,AUTOEDGE,AUTOPTP&gt;
                port 7 priority 128 path cost 2000 vlan protocol 802.1q
        member: ngeth0 flags=143&lt;LEARNING,DISCOVER,AUTOEDGE,AUTOPTP&gt;
                port 5 priority 128 path cost 20000 vlan protocol 802.1q
        member: ix0 flags=143&lt;LEARNING,DISCOVER,AUTOEDGE,AUTOPTP&gt;
                port 1 priority 128 path cost 2000 vlan protocol 802.1q
        groups: bridge
epair0a: flags=1008943&lt;UP,BROADCAST,RUNNING,PROMISC,SIMPLEX,MULTICAST,LOWER_UP&gt; metric 0 mtu 1500
        description: jail1
        options=200019&lt;RXCSUM,VLAN_MTU,VLAN_HWTAGGING,RXCSUM_IPV6&gt;
        ether 58:9c:fc:10:e6:88
        groups: epair
        media: Ethernet 10Gbase-T (10Gbase-T &lt;full-duplex&gt;)
        status: active
epair1a: flags=1008943&lt;UP,BROADCAST,RUNNING,PROMISC,SIMPLEX,MULTICAST,LOWER_UP&gt; metric 0 mtu 1500
        description: jail2
        options=200019&lt;RXCSUM,VLAN_MTU,VLAN_HWTAGGING,RXCSUM_IPV6&gt;
        ether 58:9c:fc:10:98:28
        groups: epair
        media: Ethernet 10Gbase-T (10Gbase-T &lt;full-duplex&gt;)
        status: active
epair2a: flags=1008943&lt;UP,BROADCAST,RUNNING,PROMISC,SIMPLEX,MULTICAST,LOWER_UP&gt; metric 0 mtu 1500
        description: jail3
        options=200019&lt;RXCSUM,VLAN_MTU,VLAN_HWTAGGING,RXCSUM_IPV6&gt;
        ether 58:9c:fc:10:22:09
        groups: epair
        media: Ethernet 10Gbase-T (10Gbase-T &lt;full-duplex&gt;)
        status: active
</code></pre>

<p>This <code>ifconfig</code> invocation is from within a <em>classic</em> jail running on the same host as a number of VNET jails. Despite not being able to modify anything, I can still see a lot of detail about the host that, one would think, the jail should not be privy to: All the physical and virtual interfaces on the machine, including <code>lo0</code> (localhost), the <code>bridge0</code> interface and all the bridge members, the primary interface <code>ix0</code> and its twin <code>ix1</code>, the <code>pflog0</code> interface revealing we&#39;ve got <code>pf</code> running, <code>ngeth0</code> showing there&#39;s <code>netgraph(4)</code> in use, etc.</p>

<p>And perhaps worst of all: The individual <code>epair</code> interfaces for the VNET jails, including their descriptions which – in a well-assembled system – could give away details about what and who else exists on the host.</p>

<p>Clearly, this is not the prison I was hoping for.</p>

<h2 id="the-problem-with-vnet-jails">The problem with VNET jails</h2>

<p>Quite the opposite of classic jails, VNET jails provide a virtualised network stack. This can be connected to the outside world with a virtual cable, by way of the <code>epair</code> interface type, connecting it to a bridge interface on the host. Or you can assign separate physical interfaces, VLAN interfaces, or practically any other interface type. Either way, the jail controls that interface in practically every way, in addition to the routing table (or tables), packet filtering, and most other aspects of the network stack.</p>

<p>This is obviously very powerful, and provides some features that classic jails are (sorely?) missing:</p>
<ul><li>An actual localhost interface! Classic jails don&#39;t have this, there are just some code shortcuts in the kernel that allow applications that bind to localhost to .. not fail, but there are several situations where this is far from optimal or even desired.</li>
<li>The ability to run stuff like <code>nmap(1)</code> from the jail, since it fiddles with routing tables and creates raw network packets.</li>
<li>Firewalling with <code>pf(4)</code> (or possibly others; I have not played with those). This includes basic filtering, NAT, rate limiting, and all the other goodness offered.</li>
<li>Several network-related <code>sysctl(8)</code> settings can be tuned on a per-jail basis, like datagram sizes, routing settings, TCP congestion control algorithm, etc.</li></ul>

<p>However: Do you really want all the processes in your jail to be this close to all those foot cannons? A simple misconfiguration, local privilege escalation bug, or simply fat-fingering something yourself can wreck havoc on the jail&#39;s network configuration – and there is a definite possibility of inflicting damage on other jails, the host, and even other devices on your network.</p>

<p>The limitations of a classic jail suddenly start looking an awful lot like features, don&#39;t they?</p>

<h2 id="a-possible-solution">A possible solution</h2>

<p>Without more ado (there&#39;s enough of that above), here&#39;s how we&#39;ve solved this at work: We “wrap” a classic jail inside a VNET jail.</p>

<p>Here&#39;s how:</p>

<h3 id="1-create-the-jail-template">1. Create the jail template</h3>

<p>The easiest way is to fetch <code>base.txz</code> from the FreeBSD download servers, and estract it in a suitable directory:</p>

<pre><code class="language-sh"># Create template directory. If you use ZFS, a dataset might be preferable:
mkdir -p /data/jails/template.151

# Fetch and extract
fetch -o - https://download.freebsd.org/ftp/releases/amd64/15.1-RELEASE/base.txz \
    | tar --unlink -xpzf - -C /data/jails/template.151

# Create a symlink for use in `fstab(5)`
ln -s /data/jails/template.151 /data/jails/current
</code></pre>

<h3 id="2-create-fstab-files">2. Create <code>fstab</code> files</h3>

<p>For the parent and child jails, create <code>fstab(5)</code> files to <code>nullfs(4)</code>-mount the parts of the base you need into the jails. An added benefit is that this makes the base OS read-only inside the jails, which is probably a good thing:</p>

<pre><code># /etc/fstab.testjail.parent
/data/jails/current/etc /data/jails/testjail/etc nullfs ro
/data/jails/current/bin /data/jails/testjail/bin nullfs ro
/data/jails/current/lib /data/jails/testjail/lib nullfs ro
/data/jails/current/sbin /data/jails/testjail/sbin nullfs ro
/data/jails/current/libexec /data/jails/testjail/libexec nullfs ro
/data/jails/current/usr/sbin /data/jails/testjail/usr/sbin nullfs ro
</code></pre>

<pre><code># /etc/fstab.testjail
/data/jails/current/bin /data/jails/testjail/root/bin nullfs ro
/data/jails/current/lib /data/jails/testjail/root/lib nullfs ro
/data/jails/current/usr/share /data/jails/testjail/root/usr/share nullfs ro
/data/jails/current/libexec /data/jails/testjail/root/libexec nullfs ro
/data/jails/current/sbin /data/jails/testjail/root/sbin nullfs ro
/data/jails/current/usr/bin /data/jails/testjail/root/usr/bin nullfs ro
/data/jails/current/usr/include /data/jails/testjail/root/usr/include nullfs ro
/data/jails/current/usr/lib /data/jails/testjail/root/usr/lib nullfs ro
/data/jails/current/usr/libdata /data/jails/testjail/root/usr/libdata nullfs ro
/data/jails/current/usr/libexec /data/jails/testjail/root/usr/libexec nullfs ro
/data/jails/current/usr/sbin /data/jails/testjail/root/usr/sbin nullfs ro
</code></pre>

<h3 id="3-create-the-directory-structures-for-the-jails">3. Create the directory structures for the jails</h3>

<p>This is all greatly simplified, but it should give you a starting point. Note how the child jail lives in a subdirectory (<code>/root</code>) inside the parent jail:</p>

<pre><code class="language-sh"># Create the parent jail directory structure
for dir in \
    root dev etc bin lib sbin libexec \
    usr/sbin /var/log /var/run /var/empty
do
    mkdir -p /data/jails/testjail/$dir
done

# Copy some files into the parent jail; necessary for pf to work
cp /data/jails/template.151/etc/pf.os /data/jails/testjail/etc/
cp /data/jails/template.151/etc/protocols /data/jails/testjail/etc/

# Create the child jail directory structure
for dir in \
    root dev bin lib sbin libexec tmp \
    usr/sbin usr/bin usr/include usr/lib usr/libdata \
    usr/libexec usr/share /usr/local/etc
do
    mkdir -p /data/jails/testjail/root/$dir
done

# Copy basic /etc and /var to the child jail
cp -Rp /data/jails/template.151/etc /data/jails/testjail/root/
cp -Rp /data/jails/template.151/var /data/jails/testjail/root/
</code></pre>

<h4 id="using-zfs-instead">Using ZFS instead</h4>

<p>The <code>mkdir</code> parts for the child jail could be modified to use ZFS (which is what I&#39;d do). This is a skeleton suggestion, but you&#39;d do well in forming your own strategy for how to approach this with ZFS. Personally I see no point using ZFS datasets for the parent jail, as it is largely ephemeral anyway:</p>

<pre><code class="language-sh"># Create the child jail datasets (zfs &#39;create -p&#39; acts like &#39;mkdir -p&#39;)
for dir in \
    root usr/local usr/home tmp var etc
do
    zfs create -p data/jails/testjail/root/$dir
done

# Create directories in the datasets
for dir in \
    dev bin lib sbin libexec \
    usr/sbin usr/bin usr/include usr/lib usr/libdata \
    usr/libexec usr/share /usr/local/etc
do
    mkdir -p /data/jails/testjail/root/$dir
done
</code></pre>

<p>One useful trick I&#39;ve learned is to keep <code>/var/log</code> inside the child jail in a ZFS dataset homed outside the child jail&#39;s path, and simply set its mountpoint to be inside the jail. In case you need to roll back the dataset(s) in the child jail, you can safely do so without erasing your logs. Security people and auditors may even come to appreciate this:</p>

<pre><code class="language-sh"># Create the dataset; make sure it always has space to write logs even if the
# rest of the datasets have run out of space:
zfs create -p \
    -o reservation=64M \
    -o mountpoint=/data/jails/testjail/root/var/log \
    data/jails/testjail/logs
</code></pre>

<h3 id="4-start-the-jails">4. Start the jails</h3>

<p>We start by creating the <code>epair</code> interfaces. Assuming there is already a <code>bridge0</code> to attach the dangling cable^Winterface to:</p>

<pre><code class="language-sh">if_a=&#34;$(ifconfig epair create)&#34;
if_b=&#34;${if_a%a}b&#34;
ifconfig bridge0 addm $if_a
ifconfig $if_a up
</code></pre>

<p>Next, we start the parent jail. Note this is crude; a complete <code>jail.conf</code> entry should take care to unmount filesystems on jail shutdown, avoiding mounting things twice, and various other housekeeping:</p>

<pre><code class="language-sh"># Mount the parent filesystems
mount -a -F /etc/fstab.testjail.parent

# Start the jail - set children.max=1 to allow the child jail
jail -c path=/data/jails/testjail name=testjail children.max=1 vnet persist

# $if_a and $if_b is from the interface create script
ifconfig $if_b vnet testjail
jexec testjail ifconfig lo0 127.0.0.1/8
jexec testjail ifconfig $if_b 10.2.3.4/24
jexec testjail route add default 10.2.3.1

# Mount devfs in the jails
mount -t devfs -oruleset=4 . /data/jails/testjail/dev
mount -t devfs -oruleset=4 . /data/jails/testjail/root/dev
</code></pre>

<p>Now that we have the parent jail running, starting the child jail is a matter of</p>

<pre><code class="language-sh"># Mount the child filesystems
mount -a -F /etc/fstab.testjail.parent

# Start the child jail
jexec testjail jail -c ip4=inherit path=/root name=child exec.start=&#34;/bin/sh /etc/rc autoboot&#34;
</code></pre>

<p>At this point you should be able to <code>jexec testjail.child su -</code> and find yourself in a bare, but functional, jail. Run <code>ifconfig</code> to see that only network interfaces pertaining to <code>testjail</code> are visible, and confirm that you cannot make changes. List the jails with <code>jls</code>; it should show two jails – the parent and the child.</p>

<h2 id="stopping-the-jails">Stopping the jails</h2>

<p>Since we&#39;re cheating and doing everything from the command line here, stopping the jails takes a few steps:</p>

<pre><code class="language-sh"># Stop the child jail by killing all the processes
jexec testjail.child kill -9 -1

# In case there are lingering TCP sockets preventing the child jail from dying,
# use tcpdrop(8) to clean up:
jexec testjail tcpdrop -a

# Destroy the parent jail
jail -r testjail

# Destroy the epair interfaces (find the name using ifconfig if needed; both
# the &#39;a&#39; and &#39;b&#39; of the pair should now be visible on the host:
ifconfig $if_a destroy

# Finally, unmount the filesystems
umount -f /data/jails/testjail/root/dev
umount -f /data/jails/testjail/dev
umount -F /etc/fstab.testjail -a
umount -F /etc/fstab.testjail.parent -a
</code></pre>

<h2 id="what-now">What now?</h2>

<p>All of the above, while lengthy, is hardly elegant. Some additional steps may be desirable, e.g. to enable <code>pf</code>, creating a “black hole” around the child jail, and other niceties. A <code>jail.conf</code> taking care of these steps should also be created, which will require a helper script or two.</p>

<h3 id="enabling-the-packet-filter">Enabling the packet filter</h3>

<p>In order to use <code>pf</code>, we need a <code>pf.conf</code> file, and some additional device nodes and optionally the pflog interface in the parent jail.</p>

<p>The <code>pf.conf</code> file really only needs a single line for the purpose of this demonstration. We&#39;re adding <code>log</code> here for <code>pflog</code> to have any work to do. Place it in the root of the parent jail, like so:</p>

<p><code>echo &#34;pass log all&#34; &gt; /data/jails/testjail/pf.conf</code></p>

<p>Then we need to unhide a couple of device nodes in the parent jail, load the rules, and start <code>pflogd</code>. This should be done after starting the parent jail, but before starting the child jail:</p>

<pre><code class="language-sh">devfs -m /data/jails/testjail/dev rule apply path pf unhide
devfs -m /data/jails/testjail/dev rule apply path bpf unhide
jexec testjail pfctl -F all -vf /pf.conf &amp;&amp; pfctl -e

# Starting pflogd, excluding some particularly noisy traffic
jexec testjail pflogd &#39;not host 224.0.0.18&#39;
</code></pre>

<p>At this point, if you plan to keep a configuration like this, you may want to make sure you rotate the <code>pflogd</code> files automatically, by adding this to <code>/usr/local/etc/newsyslog.d/jails.conf</code> (or anywhere else you prefer):
<code>/data/jails/testjail/var/log/pflog root:wheel 640 7 * $D0 B /data/jails/nextcloud/var/run/pflogd.pid</code>
Ask me how I know. :-P</p>

<h3 id="the-black-hole">The Black Hole</h3>

<p>One of the more “interesting” possibilities with this approach is that after the child jail has been started, there really is no reason to keep the parent jail around. Of course it needs to exist, but most of the filesystems (i.e binaries and libraries) are really not necessary. While nowhere near an actual black hole (you can still access the child jail&#39;s files), it has a non-zero value as an obfuscation.</p>

<p>Following startup of the child jail and all the parent jail services (<code>pflogd</code>, etc.), you can simply unmount all the parent filesystems:
<code>umount -F /etc/fstab.testjail.parent</code></p>

<p>However, a side effect of this is that you&#39;ll no longer be able to modify the running configuration (update <code>pf</code> rules, etc.). You can no longer simply <code>jexec</code> into the jail – no libraries or binaries are available in their expected locations.</p>

<p>To get around this, you can use some variation of the following:
<code>LD_LIBRARY_PATH=/root/lib jexec testjail /root/libexec/ld-elf.so.1 /root/sbin/pfctl -F all -f /pf.conf</code>
This will set <code>LD_LIBRARY_PATH</code> to the child jail&#39;s path, and invoke <code>ld-elf.so.1</code> directly (with the binary as argument), since it doesn&#39;t exist in <code>/libexec</code> any longer.</p>

<p>Be aware, however, that doing this will expose you to potentially tainted files in the child jail. An alternate approach would be to mount and unmount the parent filesystems, e.g.</p>

<pre><code class="language-sh">mount -F /etc/fstab.testjail.parent -a
jexec testjail &lt;command&gt;
umount -F /etc/fstab.testjail.parent -a
</code></pre>

<p>Then again, since these base OS directories are read-only mounted from the template, this should be a minimal risk. If you&#39;re attempting to run programs/libraries that live in read/write-mounted directories in the child jail, however – beware.</p>

<h2 id="putting-it-all-together-sort-of">Putting it all together (sort of)</h2>

<p>What&#39;s missing now is of course the <code>jail.conf</code> entry. With this in place, you can use the regular
<code>service jail start [&lt;name&gt;]</code> and
<code>service jail stop [&lt;name&gt;]</code>
commands to manage your jails, and you can have them automatically start on system boot.</p>

<p>Correctly assembling such a configuration is a fairly involved task, but I&#39;ll leave you with an example that should help you get started. Good luck!</p>

<pre><code class="language-sh"># /etc/jail.conf
## Some defaults
# The default devfs ruleset - this is a default /etc/defaults/devfs.rules
devfs_ruleset = 4;
# We want the venerable rc(8) to manage the child jails
exec.start = &#34;/bin/sh /etc/rc autoboot&#34;;
# Set a high securelevel for the child jails (see security(7))
$child_securelevel = 4;
# Disallow modifying hostnames after creating jails
allow.set_hostname = 0;
# Clean the environment
exec.clean;

## Our test jail starts here
testjail {
    # Set a variable for use later - a run directory for some housekeeping
    $rundir = &#34;/var/run/jails/testjail&#34;;

    # Basic settings for the parent jail
    host.hostname = &#34;testjail.PARENT&#34;;
    path = &#34;/data/jails/testjail&#34;;
    vnet;
    # Setting a high value here, so we can use service jails (see rc.conf(5))
    children.max = 21;

    # The fstab for the parent, will be automatically mounted on startup. Also
    # make sure we can mount filesystems inside the parent
    mount.fstab = &#34;/etc/fstab.bbs&#34;;
    allow.mount;
    allow.mount.devfs;
    # Allow inspecting ZFS snapshots from within the child jail (optional)
    zfs.mount_snapshot = 1;
    # In case you want PostgreSQL or something
    allow.sysvipc = 1;
    # Keep a console log for the jail
    exec.consolelog = &#34;/var/log/jail_${name}_console.log&#34;;

    # Cleanup before starting the jail (In case a previous attempt failed)
    exec.prepare = &#34;umount -f -F /etc/fstab.testjail -a || :&#34;;
    exec.prepare += &#34;umount -f -F /etc/fstab.testjail.parent -a || :&#34;;
    exec.prepare += &#34;umount -f ${path}/root/dev || :&#34;;
    exec.prepare += &#34;umount -f ${path}/dev || :&#34;;

    ### Starting the jail
    ## Pre-start commands
    # Create rundir on host
    exec.prestart = &#34;mkdir -p ${rundir}&#34;;
    # Create the epair interface; put the names of each end in files in $rundir
    exec.prestart += &#34;ifconfig epair create &gt; ${rundir}/if_a&#34;
    exec.prestart += &#34;sed &#39;s/a$/b/&#39; &lt; ${rundir}/if_a &gt; ${rundir}/if_b&#34;
    # Mount parent jail filesystems
    exec.prestart += &#34;mount -F /etc/fstab.${name}.parent -a || :&#34;;
    # Mount devfs in parent jail
    exec.prestart += &#34;mount -t devfs -oruleset=${devfs_ruleset} . ${path}/dev&#34;;
    # Unhide pf and bpf device in parent jail
    exec.prestart += &#34;devfs -m ${path}/dev rule apply path pf unhide&#34;;
    exec.prestart += &#34;devfs -m ${path}/dev rule apply path bpf unhide&#34;;

    ## Post-create commands (run after jail created, but before exec.start)
    # Mount devfs in the child jail with appropriate ruleset
    exec.created = &#34;mount -t devfs -oruleset=${devfs_ruleset} . ${path}/root/dev&#34;;
    # Assign jail-side interface to jail
    exec.created += &#34;ifconfig `cat ${rundir}/if_b` vnet ${name}&#34;;
    # Assign IP address to jail interface
    exec.created += &#34;jexec ${name} ifconfig $(cat ${rundir}/if_b) inet 10.2.3.4/24&#34;;
    # Add default route
    exec.created += &#34;jexec ${name} route add default 10.2.3.1&#34;;

    ## Startup commands
    # Assign IP to lo0-interface
    exec.start = &#34;ifconfig lo0 127.0.0.1/8&#34;;
    # Load PF rules for parent jail and enable
    exec.start += &#34;pfctl -F all -vf /pf.conf &amp;&amp; pfctl -e&#34;;
    # Start pflogd
    exec.start += &#34;/sbin/pflogd &#39;not host 224.0.0.18&#39;&#34;;
    # Wait for ping to jail (interface plumbing); assuming this file is
    # autogenerated and the IP includes the netmask
    exec.start += &#34;ip=&#39;10.1.2.3/24&#39; ; ping -n -c 15 -t 20 -o \${ip%/*}&#34;;
    # Set sysctls (if any are configured)
    exec.start += &#34;(test -s /sysctl.conf &amp;&amp; sysctl -f /sysctl.conf) || :&#34;;

    ## Create child jail
    exec.start += &#34;jail -c ip4=inherit path=/root host.hostname=testjail \
                   name=child zfs.mount_snapshot=1 children.max=20 \
                   securelevel=${child_securelevel} \
                   sysvmsg=\&#34;new\&#34; sysvsem=\&#34;new\&#34; sysvshm=\&#34;new\&#34; \
                   exec.start=\&#34;/bin/sh /etc/rc autoboot\&#34;&#34;;

    ## Post-start commands
    # Unount parent jail filesystems
    exec.poststart += &#34;umount -F /etc/fstab.${name}.parent -a -f || :&#34;;

    ### Stopping the jail
    ## Pre-stop commands
    # Mount parent jail filesystems
    exec.prestop = &#34;mount -F /etc/fstab.${name}.parent -a&#34;;

    ## Shutdown commands
    # Shut down child jail cleanly, if possible
    exec.stop = &#34;jexec child /bin/sh /etc/rc.shutdown ; sleep 1&#34;;
    # Forcibly remove child jail, just in case
    exec.stop += &#34;jls -d name|chroot /root grep -q &#39;^${name}.child$&#39; &amp;&amp; jail -Rd ${name}.child || :&#34;;
    # Remove any lingering TCP sockets
    exec.stop += &#34;tcpdrop -a || :&#34;;

    ## Post-stop commands
    # Unount parent jail filesystems
    exec.poststop = &#34;umount -F /etc/fstab.${name}.parent -a -f || :&#34;;
    # Unmount devfs
    exec.poststop += &#34;umount -f ${path}/root/dev || :&#34;;
    # Unount parent jail dev filesystem
    exec.poststop += &#34;umount -f ${path}/dev || :&#34;;
    # Finally, destroy jail interfaces
    exec.poststop += &#34;ifconfig `cat ${rundir}/if_b` destroy&#34;;

    # Clean up rundir
    exec.poststop += &#34;rm -Rf ${rundir}&#34;;
</code></pre>

<div style="display: flex; place-content: center;"><table style="width: 80%; border: none; font-size: 0.8em;">
<tr style="border: none;">
<td align="center" style="border: none;" colspan="2"><hr>
~ love over gold ~</td>
</tr>
<tr style="border: none; background-color: inherit;">
<td align="left" style="border: none; padding-left: 2em;"><a href="http://floppy.museum" rel="nofollow">The Floppy Museum</a></td>
<td align="right" style="border: none; padding-right: 2em;"><a href="http://floppy.museum/thispc.htm" rel="nofollow">About the Museum</a></td>
</tr></table></div>
]]></content:encoded>
      <guid>https://scrolls.anduin.net/bsd/freebsd-wrapped-jails</guid>
      <pubDate>Sat, 22 Aug 2026 21:31:51 +0000</pubDate>
    </item>
  </channel>
</rss>