Skip to content

66-ns

66-ns (namespace) declaratively builds a Linux namespace (sandbox) and execs a program inside it.

Interface

66-ns [ -h ] [ -z ] [ -v verbosity ] [ -d notif ] [ -o ns_options ] [ -e element ] [ -r rule ] prog...

66-ns sets up a mount namespace (and, on demand, pid, net, ipc, uts, cgroup and user namespaces) from a list of typed elements given on the command line and/or in rule files, then execs prog inside it. It gives a fine-grained control over what prog and its children can see, limiting their exposure to the host. It can be used to start a service inside a namespace or directly from a terminal.

A mount namespace (CLONE_NEWNS) is always created. By default the host root file system is bind-mounted into the namespace, so without any element the namespace is a faithful copy of the host.

When a pid namespace is requested (-o unshare=pid), 66-ns becomes pid 1 inside the namespace and reaps the processes it contains, which makes it suitable to supervise a daemon that forks itself.

By default 66-ns must be run as root. The user namespace (-o unshare=user) enables rootless mode, where an unprivileged user can build the namespace—see Rootless mode.

Exit codes

  • 0 : success.
  • 100 : wrong usage (invalid option, malformed element, bad rule file, ...).
  • 111 : a system call failed.
  • 128 + n : prog was terminated by signal n.
  • otherwise : the exit status code of prog.

Options

  • -h, --help : print the help and exit.

  • -z, --color : enable color. If 66-ns is not launched from a terminal, color is automatically disabled and the option has no effect.

  • -v, --verbosity number : set the verbosity level of the command, from 0 to 5.

    • 0 : only print error messages.
    • 1 : also print informative messages. This is the default.
    • 2 : also print warning messages.
    • 3 : also print tracing messages.
    • 4 : also print the function name and line of the messages.
    • 5 : also display the sequence of the current process function by function.
  • -d, --notify number : notify readiness on the file descriptor number. The notification happens right before prog is executed; it guarantees that the namespace is fully configured and ready to use. It does not guarantee that prog itself started successfully. This is mostly useful when 66-ns is used under a service supervisor.

  • -o, --options ns_options : a comma-separated list of namespace options—see Namespace options. This option can be passed multiple times.

  • -e, --element element : a colon-separated element to handle—see Element options. This option can be passed multiple times. An element given here takes precedence over the same path found in a rule file. The first field must be an absolute path. An element can be a directory, a file, a fifo, a symlink, a socket, a character special file or a block special file.

  • -r, --rule path : the rule file to apply—see Rule file. This option can be passed multiple times.

  • -p, --pidfile path : path, interpreted inside the namespace, of a file where the supervised daemon writes the process id of its real main process. Optional: when given, supervision becomes authoritative (the named pid is treated as the service's main); when omitted, supervision still works for the common cases on its own. Providing it never breaks anything—see Supervision. Only meaningful with -o unshare=pid; passing it without is an error.

After the options, at least one operand prog is required: the command (and its arguments) to execute inside the namespace.

Namespace options

The -o option is a comma-separated list of the following keywords:

  • flag= flag : the final mount-point propagation of the namespace root, where flag is private, slave, unbindable or shared, corresponding to MS_PRIVATE, MS_SLAVE, MS_UNBINDABLE or MS_SHARED respectively—see mount(2). If not set, MS_SHARED is used.

  • unshare= flags : a colon-separated list of namespaces to unshare, where each flag is one of:

    • pid : CLONE_NEWPID. 66-ns becomes pid 1 inside the namespace and reaps its children.
    • net : CLONE_NEWNET. The loopback interface is automatically brought up (127.0.0.1/8, lo up) inside the new network namespace.
    • ipc : CLONE_NEWIPC.
    • uts : CLONE_NEWUTS.
    • cgroup : CLONE_NEWCGROUP (an error is raised if the kernel does not support it).
    • user : CLONE_NEWUSER. Enables rootless mode—see Rootless mode.
    • all : a shortcut for net, ipc, uts and cgroup. It does not include pid or user, which must be requested explicitly.

    The mount namespace CLONE_NEWNS is always implied. Example: -o unshare=pid:net:user.

  • hostname= name : set the namespace hostname to name. This implies unshare=uts automatically.

  • uid= number : in rootless mode, the inner uid the caller is mapped to. Default 0.

  • gid= number : in rootless mode, the inner gid the caller is mapped to. Default 0.

  • nonewprivileges : set PR_SET_NO_NEW_PRIVS so that prog and its children can never gain new privileges through execve() (e.g. setuid/setgid bits or filesystem capabilities).

  • newsession : call setsid() before exec, detaching prog from its controlling terminal and making it a new session leader.

Element options

An element is a colon-separated series of fields. The first field must be the absolute path of the element to handle; the remaining fields are key=value pairs in any order. For example:

66-ns -e /etc:options=ro -e /etc/resolv.conf:type=clone:options=rw -e /dev:type=tmpfs:options=nosuid,strictatime,noexec,mode=755,size=4m,nr_inodes=64k -e /proc:type=proc -e /sys:type=recursive:options=ro sh -i

The fields are:

  • element path : the path of the element to handle. Mandatory, set first, and must be an absolute path.

  • target= target : the mount destination of the element inside the namespace. If not set, target is the same as the element path. target must be an absolute path.

  • type= type : where type is tmpfs, hidden, recursive, clone, proc, dev or sys—see Type specifications. If not set, a bind mount (MS_BIND) is performed.

  • options= options : a comma-separated list of mount options—see mount(8). Recognised kernel flags (e.g. ro, nosuid, nodev, noexec, relatime, bind, rbind, shared ...) are folded into the mount flags; any remaining options (e.g. mode=755, size=4m) are passed through as filesystem data. The remount and move options are ignored.

  • create= yes|no : if no and the target does not exist, 66-ns stops and exits 111. If yes, 66-ns tries to create the target if missing (and exits 111 if creation fails). Default yes.

  • ignore= yes|no : if yes and the source element does not exist, the element is skipped and the setup continues. If no, a missing source makes 66-ns exit 111. Default no.

Type specifications

  • bind (default) : bind-mount the source onto the target. If the target is already a mount point, a remount is performed instead, preserving the mounted filesystem type and options.

  • tmpfs : mount a fresh tmpfs on the element, ignoring any bind/rbind flag in options=. If the target is already a mount point it is first unmounted recursively (in rootless mode it is over-mounted instead, because locked mounts cannot be unmounted). prog cannot see anything that was below the target.

  • hidden : make the element—and everything below it—inaccessible inside the namespace. A read-only placeholder (mode 0000, MS_NOSUID, MS_NODEV) is bind-mounted over the target. It is not possible to nest another element below a hidden one (e.g. hiding /etc blocks a later /etc/resolv.conf element).

  • recursive : recursively bind-mount the source onto the target (MS_BIND | MS_REC), so sub-mounts of the source are carried into the namespace.

  • clone : recreate the element node inside the namespace (file, directory, fifo, symlink, socket, character or block special file), preserving mode and ownership on a best-effort basis. It cannot be used on a mount point. The options= field is ignored.

  • proc : mount a fresh proc filesystem (MS_NOSUID | MS_NODEV | MS_NOEXEC) instead of bind-mounting the host's. Valid only for the target /proc. In rootless mode without a pid namespace, the host /proc is recursively bind-mounted instead. The options= field is ignored.

  • dev : mount a fresh devtmpfs (mode 755) instead of bind-mounting the host's. Valid only for the target /dev. In rootless mode, a tmpfs is mounted and the core device nodes (null, zero, full, random, urandom, tty) are bind-mounted from the host. The options= field is ignored. Only /dev itself is handled; sub-mounts such as /dev/pts are not.

  • sys : mount a fresh sysfs instead of bind-mounting the host's. Valid only for the target /sys. In rootless mode without a network namespace, the host /sys is recursively bind-mounted instead. The options= field is ignored.

Rootless mode

Requesting the user namespace with -o unshare=user enables rootless mode: an unprivileged user can build and enter the namespace without being root. The caller's effective uid/gid are mapped to the inner uid=/gid= values (default 0), setgroups is denied, and the uid/gid maps are written by the parent once the child exists.

Because an unprivileged process cannot unmount locked mounts nor mount real proc/dev/sys filesystems, the tmpfs, proc, dev and sys types fall back to the safe behaviours described in Type specifications.

Supervision

This section explains what happens when 66-ns is asked for a pid namespace (-o unshare=pid). It is long on purpose: getting supervision right is subtle, and the design choices only make sense once the underlying problem is clear.

The problem

A process supervisor (66-supervise, but the reasoning holds for any of them) works by being the parent of the process it watches: it fork()s a child, runs the service in it, and learns of its death through SIGCHLD/wait(). To stop the service it signals that child; to restart it, it forks it again. The whole model rests on one assumption: the process I forked is the service.

Two things break that assumption.

  1. Many daemons "daemonize". The process the supervisor started is only a launcher: it fork()s, the parent exits, the child setsid()s and fork()s again, and the real daemon — now reparented away — keeps running with a different pid. The supervisor's child is dead, so the supervisor thinks the service stopped, while it is in fact alive and unsupervised. (This is the classic problem other tools solve by demanding a pid file.)

  2. A pid namespace forces a resident wrapper. 66-ns exists to put the service in a namespace (mounts, optionally pid/net/…). For a pid namespace this changes everything: the namespace needs a pid 1 (an init) that stays alive — pid 1 reaps orphans, and the kernel keeps the namespace alive as long as pid 1 lives. So 66-ns cannot do the usual trick of exec()ing into the daemon and getting out of the way; it has to stay resident as pid 1. Once it is resident, it has no choice but to be a faithful proxy: everything the supervisor would have seen and done to the daemon directly, 66-ns must reproduce.

So the job of 66-ns under -o unshare=pid is to be a transparent 1:1 proxy between the outer supervisor and the real daemon, whatever the daemon does with fork().

Why a pid namespace (and not a pid file alone)

Being pid 1 of a pid namespace buys two guarantees the kernel gives for free, which is exactly what supervision needs:

  • Nothing escapes. Every process spawned inside the namespace stays in it. When pid 1 exits, the kernel sends SIGKILL to every remaining process in the namespace and destroys it. "Stop the service" therefore becomes "let pid 1 exit", and it is atomic and total — no orphaned worker, no leak, ever. This is teardown by construction.
  • Reparenting comes home. When a daemon double-forks, the surviving process is reparented to the namespace's pid 1 — i.e. to 66-ns — which can then wait() for it like any direct child. We do not need a pid file just to keep track of a backgrounded daemon (other supervisors do, because without a pid namespace the reparented daemon escapes to the system's pid 1).

How it is wired (the process tree)

With -o unshare=pid, a started service looks like this (pids shown as seen inside the namespace):

66-supervise                 (outside, the real supervisor: restart policy, logs, ./down …)
└─ 66-ns  (invoker)          the process 66-supervise forked and watches
   └─ pid 1  (66-ns init)    the mini-init: reaps, follows the main, forwards signals, tears down
      └─ pid 2  (your prog)  the program from the command line = the initial "main"

Each layer is a dumb mirror of the one below it:

  • pid 1 tracks the real main and decides the service's fate.
  • the invoker mirrors pid 1: it watches pid 1 and exits with pid 1's code, so 66-supervise sees a normal child exit and applies its restart policy as usual.

66-ns adds no supervision policy of its own: restart, ./down, readiness, timestamps, logs all remain 66-supervise's job. 66-ns only transports life, death and signals faithfully.

The two directions of transparency

Signals go down. A signal sent to the 66-ns invoker (by 66-supervise, e.g. through s6-svc, or by an admin) is forwarded to pid 1, which forwards it to the currently-tracked main. The forwarded set is the catchable control signals a supervisor/admin may legitimately send:

SIGTERM, SIGINT, SIGQUIT, SIGHUP, SIGUSR1, SIGUSR2, SIGALRM, SIGWINCH

SIGKILL and SIGSTOP cannot be caught and are not forwarded — but SIGKILL on the invoker still tears the service down (pid 1 dies, the kernel zaps the namespace). SIGCHLD is consumed internally (reaping); fault signals (SIGSEGV, …) are self-generated, not control, and are not forwarded.

Exit code goes up. When the service is over, pid 1 exits with the real main's exit status (128 + signal if it was killed by a signal), the invoker re-exits with the same code, and 66-supervise records it. The status the supervisor sees is the daemon's, not a wrapper's.

Following the main

Everything above hinges on one question: which process is the main right now? The kernel has no notion of "the service", so 66-ns answers it by watching deaths — and there is nothing to configure for the common cases. Two modes exist; you fall into the first by default and opt into the second only if you want a hard guarantee.

Default mode (no pid file) — handles foreground and self-backgrounding daemons

As long as the tracked main lives, 66-ns just sits there (reaping any orphan that shows up). The moment the tracked main dies, it looks at how many of its own live children remain (a direct child of pid 1, not a zombie):

live children left interpretation action
0 the service is really gone exit with the main's code → the kernel tears the namespace down → s6 restarts
1 the daemon just re-forked itself; the survivor is the continuation adopt it as the new main and keep watching
≥ 2 ambiguous: several unrelated processes survive, 66-ns cannot tell which is the service tear down (a daemon in this shape needs a pid file — see below)

Follow a single daemon, myd, started as:

66-ns -o unshare=pid -o unshare=user -e /proc:type=proc myd …

Case A — myd stays in the foreground (e.g. myd --foreground). It is pid 2 and stays the main:

pid 1 (66-ns)
└─ pid 2  myd          ← the main, forever

Stop with s6-svc -d: SIGTERM reaches the invoker → pid 1 → kill(2)myd exits 143 → 0 children left → 66-ns exits 143. Nothing to configure.

Case B — myd daemonizes (classic double-fork). pid 2 is the launcher; it forks the real daemon and exits:

step 1            step 2 (pid 2 exited)
pid 1             pid 1
└─ pid 2 myd  →   └─ pid 4 myd     ← reparented to pid 1

When pid 2 dies, exactly one child (pid 4) is left → it is the continuation → 66-ns adopts it and keeps watching. Supervised correctly with no pid file, because pid 1 had a single child so the survivor is unambiguous. (Triple-forks resolve the same way, one survivor at a time.)

Case C — myd daemonizes but keeps a sibling alive (it backgrounds the real daemon and leaves a detached logger running). When the launcher exits, two unrelated processes survive:

pid 1
├─ pid 5  myd-logger   (a helper, not the service)
└─ pid 6  myd          (the real daemon)

66-ns cannot tell pid 6 (the service) from pid 5 (the helper) — this is the same fundamental ambiguity every supervisor hits (systemd's MainPID, the pid files of dinit/finit). In default mode it tears down, so such a daemon cannot be supervised without help. That help is a pid file.

Honest limitation of default mode. Because a lone survivor is assumed to be the continuation, if a long-running main later dies leaving exactly one stray child (a stuck worker, a lingering helper), 66-ns would adopt that stray and stay "up" on it. It is uncommon, and the cure is the same one pid file below.

Authoritative mode (--pidfile) — a guarantee for daemons that cooperate

If the daemon can write a pid file, point 66-ns at it:

66-ns -o unshare=pid -o unshare=user -e /proc:type=proc --pidfile /run/myd.pid myd …

The path is interpreted inside the namespace (it is the path the daemon writes to). Now the pid named in that file is the main, authoritatively:

  • when the tracked main dies, 66-ns reads the pid file and follows the live process it names (this is how it picks up the real daemon after a double-fork);
  • when the pid file no longer names a live child, the service is over: 66-ns tears the namespace down — it never chases an unnamed survivor.

This makes every shape robust — including case C — and removes the default mode's lone-survivor caveat. A missing, empty or stale pid file simply means "no live main" → clean teardown → the supervisor restarts; it is never wrong.

Two things to know: the daemon must have written the pid file by the time its launcher exits (the standard daemonize behaviour), and --pidfile only makes sense with -o unshare=pid.

Rule of thumb: do nothing — the default handles foreground and self-backgrounding daemons. If your daemon can write a pid file and you want a guarantee (or it is a case-C daemon), add --pidfile. It is optional and never breaks anything; in the vast majority of cases you will not need it.

The only genuinely unsupported daemon is one that backgrounds itself, keeps a sibling alive, and offers no usable pid file: run it in the foreground.

Summary

daemon shape what you provide what 66-ns does robust?
foreground nothing pid 2 is the main; its death tears the namespace down
self-backgrounding (double-fork) nothing follows the reparented daemon automatically ✅ (rare lone-survivor caveat)
backgrounds + keeps a sibling nothing ambiguous at launch → teardown → restart loop ❌ — give a pid file
any of the above --pidfile <path> the named pid is the authoritative main; its death tears down, no chasing ✅ fully

In one line: -o unshare=pid makes 66-ns a transparent pid-1 proxy (signals down, exit code up, kernel-guaranteed teardown); the default needs no configuration for foreground and self-backgrounding daemons, and --pidfile is an optional, never-harmful upgrade to authoritative supervision.

Rule file

Building a namespace with -e options only can become tedious and unreadable, for example:

66-ns -e /dev:type=tmpfs:options=nosuid,strictatime,noexec,mode=755,size=4m,nr_inodes=64k -e /dev/pts:options=rw -e /dev/shm:options=rw -e /dev/hugepages:options=rw -e /dev/mqueue:options=rw -e /dev/ptmx:type=clone sh -i

A rule file solves this.

Invocation

A rule file is passed with -r and may be referenced by absolute path, relative path, or bare name. A bare name is looked up in the /usr/share/66/script/ns directory. This default directory is set at build time through the meson option ns-rule-dir.

The contributions/rule subdirectory of the 66-tools package ships a working set of rule files.

Syntax

A rule file uses the INI format: it is made of sections introduced by a [name] header, each containing a body of lines. Blank lines and lines whose first non-blank character is # are ignored.

  • A [/path] section, where path is an absolute path, describes one element. Its body is a list of key=value lines, where key is an element option (target, type, options, create, ignore). The section is equivalent to a single -e path:key=value:... argument. If the same path was already given by an earlier -e option or rule, the section is skipped.

  • The special [include] section lists, one per line, other rule files (by absolute path, relative path or bare name) to load recursively.

Example:

[include]
# pull in a shared base
system-strict.rule
/usr/lib/66/script/ns/private-dev.rule

[/etc/resolv.conf]
options=rw

Translation between a rule file and the -e option

This command line:

66-ns -e /dev:type=tmpfs:options=nosuid,strictatime,noexec,mode=755,size=4m,nr_inodes=64k -e /dev/pts:options=rw -e /dev/shm:options=rw -e /dev/hugepages:options=rw -e /dev/mqueue:options=rw -e /dev/ptmx:type=clone sh -i

is equivalent to:

66-ns -r dev-example.rule sh -i

where dev-example.rule contains:

[/dev]
type=tmpfs
options=nosuid,strictatime,noexec,mode=755,size=4m,nr_inodes=64k

[/dev/pts]
options=rw

[/dev/shm]
options=rw

[/dev/hugepages]
options=rw

[/dev/mqueue]
options=rw

[/dev/ptmx]
type=clone

Usage examples

Mount / as a read-only file system:

66-ns -e /:options=ro sh -i

Mount / as read-only except /dev, which is recursively mounted read-write:

66-ns -e /:options=ro -e /dev:type=recursive sh -i

Make /usr/lib/modules inaccessible:

66-ns -e /usr/lib/modules:type=hidden sh -i

Forbid privilege gain, mount a fresh /proc and make /etc read-only:

66-ns -o nonewprivileges -e /proc:type=proc -e /etc:options=ro sh -i

Hide all host processes and run a trivial pid 1 inside the namespace (66-ns is pid 1, sh is pid 2):

66-ns -o unshare=pid -e /proc:type=proc sh -i

Build a rootless namespace, mapping the caller to inner root, with its own pid and network namespaces:

66-ns -o unshare=user:pid:net -e /proc:type=proc sh -i

Apply a rule file:

66-ns -r myrule sh -i

Supervise a daemon in a pid namespace (see Supervision). A foreground daemon, the common and recommended shape — 66-ns is its faithful pid-1 proxy, no extra option needed:

66-ns -o unshare=pid -e /proc:type=proc mydaemon --foreground

Keep control of a daemon that forks itself (case B: it double-forks and the real daemon is reparented). 66-ns follows it automatically, still without a pid file. From a 66 service frontend:

[Main]
Type = classic
Description = "Keep control on the dhcpcd daemon which forks itself"

[Start]
Execute = ( 66-ns -o unshare=pid dhcpcd )

Authoritative supervision with a pid file (optional; required only for a case-C daemon that backgrounds itself while keeping a sibling alive, but it never hurts to add it for any daemon that writes one). The path is interpreted inside the namespace:

66-ns -o unshare=pid -e /proc:type=proc --pidfile /run/mydaemon.pid mydaemon