Skip to content

Commit 0bc4861

Browse files
andypostclaude
andcommitted
docs(security): running application isolation under Docker
Add a "Running Isolation in a Container (Docker)" section to the security how-to covering the three gates an isolated app must clear under Docker -- the SYS_ADMIN capability, the seccomp profile, and AppArmor/SELinux -- with the syscalls Unit's isolation uses (unshare, mount, umount2, pivot_root, chroot, openat2) mapped to each gate and a diagnostic recipe for telling a capability, seccomp, or LSM denial apart. Document blocking AF_ALG (CVE-2026-31431) with the bundled seccomp-no-af-alg.json: how to pass or extract it, the intentional SCMP_ACT_ALLOW default, the host-level algif_aead module blacklist, and a verify step. Scope the profile honestly -- it denies AF_ALG only (not TIPC), and it filters the direct socket(2) call only, so a warning and the cross-LSM note flag that the socketcall(2) multiplexer still reaches AF_ALG on i386/s390x and via the x86-64 int 0x80 entry, closed by an AppArmor (deny network alg) / SELinux (alg_socket) rule or the module blacklist. Cross-reference the new section from the configuration index's isolation note. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017r3PHLb7YSsDTGmuJNpfPm
1 parent dd03e85 commit 0bc4861

2 files changed

Lines changed: 184 additions & 6 deletions

File tree

source/configuration/index.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3445,6 +3445,13 @@ if Unit's underlying OS supports them:
34453445
34463446
cgroup :nxt_hint:`mnt <The mount namespace>` :nxt_hint:`net <The network namespace>` pid ... :nxt_hint:`user <The credential namespace>` :nxt_hint:`uts <The uname namespace>`
34473447
3448+
.. note::
3449+
3450+
**isolation** relies on privileged namespace and mount syscalls, which
3451+
Docker's default container settings block. See
3452+
:ref:`Running Isolation in a Container (Docker) <security-isolation-docker>`
3453+
for the capability, seccomp, and AppArmor settings a container must grant.
3454+
34483455
The **isolation** application option
34493456
has the following members:
34503457

source/howto/security.rst

Lines changed: 177 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -773,8 +773,9 @@ Docker: Seccomp Profile (AF_ALG)
773773
kernel ``algif_aead`` component, reachable via the ``AF_ALG`` socket interface
774774
(domain 38). It affects kernels 4.14 and later and requires only local user
775775
access — including from within a container. FreeUnit images ship a seccomp
776-
profile that blocks ``socket(AF_ALG, ...)`` at the kernel level regardless of
777-
whether the host kernel is patched.
776+
profile that blocks the direct ``socket(AF_ALG, ...)`` call at the kernel level
777+
regardless of whether the host kernel is patched (see the caveat below for the
778+
``socketcall(2)`` multiplexer path).
778779

779780
**Actions**: Pass the bundled profile at ``docker run`` time:
780781

@@ -797,10 +798,26 @@ without cloning the repository:
797798
798799
.. note::
799800

800-
The profile uses ``defaultAction: SCMP_ACT_ALLOW`` with explicit deny
801-
rules for AF_ALG (38) and TIPC (40). This is intentional: libseccomp
802-
ORs multiple ``NE`` conditions on the same argument index, making an
803-
``SCMP_ACT_ERRNO``-default approach unreliable for this use case.
801+
The profile uses ``defaultAction: SCMP_ACT_ALLOW`` with a single explicit
802+
deny rule, for AF_ALG (38). The ``SCMP_ACT_ALLOW`` default is intentional:
803+
libseccomp ORs multiple ``NE`` conditions on the same argument index,
804+
making an ``SCMP_ACT_ERRNO``-default approach unreliable for this use
805+
case. The profile is deliberately single-purpose — passing it via
806+
``--security-opt`` replaces Docker's default profile entirely, so it
807+
blocks AF_ALG and nothing else.
808+
809+
.. warning::
810+
811+
The profile filters the direct ``socket(2)`` syscall only — it does **not**
812+
filter the ``socketcall(2)`` multiplexer. On i386, s390x, and other
813+
architectures that route socket creation through ``socketcall`` — and on
814+
x86-64 through the i386 ``int $0x80`` entry — a process can still open an
815+
``AF_ALG`` socket, so this seccomp profile alone does not fully block
816+
CVE-2026-31431 on those paths. Close them with an AppArmor (``deny network
817+
alg``) or SELinux (``alg_socket``) rule, or with the host-level workaround
818+
below, which disables the vulnerable ``algif_aead`` module outright and is
819+
effective regardless of architecture. See :ref:`security-isolation-docker`
820+
for the full cross-LSM analysis.
804821

805822
**Host-level workaround** (unpatched kernels, applies outside Docker too):
806823

@@ -838,3 +855,157 @@ additional level of separation and containment for your apps, such as:
838855
`namespace <https://www.nginx.com/blog/application-isolation-nginx-unit/>`_ and
839856
`file system <https://www.nginx.com/blog/filesystem-isolation-nginx-unit/>`_
840857
isolation.
858+
859+
.. _security-isolation-docker:
860+
861+
.. nxt_details:: Running Isolation in a Container (Docker)
862+
:hash: sec-isolation-docker
863+
864+
To set up its :ref:`namespaces <configuration-proc-mgmt-isolation>`,
865+
**rootfs**, and mounts, Unit's **isolation** feature calls a handful of
866+
privileged syscalls in the app process: **unshare(2)** (for
867+
**CLONE_NEWUSER**, **CLONE_NEWPID**, **CLONE_NEWNET**, **CLONE_NEWUTS**,
868+
**CLONE_NEWNS**, and **CLONE_NEWCGROUP**), **mount(2)**, **umount2(2)**,
869+
`pivot_root(2)
870+
<https://man7.org/linux/man-pages/man2/pivot_root.2.html>`__,
871+
**chroot(2)** (used for a **rootfs** configured without a mount
872+
**namespace**), and **openat2(2)** (to resolve mount targets beneath
873+
**rootfs** without following symlinks out of it).
874+
875+
Under Docker's *default* container settings these operations fail with
876+
**EPERM**. Three independent layers gate them, and an **isolation**
877+
config with a **rootfs** must satisfy all three:
878+
879+
**1. Capability.** Default containers drop **CAP_SYS_ADMIN**, which
880+
**unshare(CLONE_NEW*)**, **mount**, **umount2**, and **pivot_root** all
881+
require; grant it with **--cap-add SYS_ADMIN**. A **rootfs** bind-mounts
882+
the language runtime, **procfs**, and **tmpfs** by default (the
883+
**automount** option), so **mount** and **openat2** are used even for a
884+
chroot-only **rootfs** — not only when a mount **namespace** is requested.
885+
886+
**2. Seccomp.** Docker's default profile denies most syscalls
887+
(``defaultAction: SCMP_ACT_ERRNO``) but allows **unshare**, **mount**,
888+
and **umount2** once the container holds **CAP_SYS_ADMIN**, plus
889+
**chroot** and **openat2** (the latter only on Docker 20.10.10 and
890+
newer). The one syscall it never allows is **pivot_root**, so a
891+
**rootfs** that pivots (one with a mount **namespace**,
892+
``"namespaces": {"mount": true}``) stays blocked. Two ways to unblock
893+
**pivot_root**:
894+
895+
- *Preferred* — copy Docker's default profile and add a **pivot_root**
896+
``allow`` rule, keeping its whole denylist intact. Check that the
897+
profile you copy carries the :ref:`AF_ALG mitigation
898+
<security-seccomp-docker>` (see below).
899+
- *Simplest* — run the bundled ``seccomp-no-af-alg.json`` itself. It
900+
permits every isolation syscall and denies AF_ALG, but because it is
901+
``defaultAction: SCMP_ACT_ALLOW`` it forfeits the rest of Docker's
902+
default seccomp denylist — so prefer it only where the capability and
903+
AppArmor layers already constrain the container.
904+
905+
Whether a copied default profile denies AF_ALG depends on the Docker
906+
version it came from. **Docker 29.4.2** and newer allow ``socket`` only
907+
for domains outside the 38–40 range — three rules, ``arg0 < 38``,
908+
``arg0 == 39``, ``arg0 > 40`` — which denies both AF_ALG (38) and
909+
AF_VSOCK (40). Older profiles allow ``socket`` under a single
910+
``arg0 != AF_VSOCK`` condition, which leaves AF_ALG reachable. If yours
911+
is the older shape, **replace** that ``socket`` rule with the three range
912+
rules; appending an AF_ALG deny rule beside it does *not* work, because
913+
seccomp evaluation is first-match-wins and the inherited allow rule still
914+
matches AF_ALG (`moby#52494
915+
<https://github.com/moby/moby/pull/52494>`__). Copying from a 29.4.2+
916+
profile avoids the edit entirely.
917+
918+
Avoid **--security-opt seccomp=unconfined** (turns off all filtering,
919+
AF_ALG deny included) and **--privileged** (every capability, no seccomp).
920+
921+
**3. AppArmor.** On hosts with AppArmor enabled — the default on Debian
922+
and Ubuntu — Docker also applies its **docker-default** profile, which
923+
mediates mount operations independently of capabilities and seccomp: it
924+
denies **mount** and grants no **pivot_root** rule (only **umount**), so
925+
both stay blocked. A **rootfs** therefore still fails with **EPERM** — at
926+
**mount** for its automounts, and, when it pivots, at **pivot_root** too.
927+
Allow **both** with a custom AppArmor profile (preferred), or relax the
928+
policy with **--security-opt apparmor=unconfined** (blunter, comparable to
929+
**seccomp=unconfined**). A **mount** or **pivot_root** denial while the
930+
capability and seccomp are already in place points at AppArmor.
931+
932+
Combining the three, the *simplest* working recipe for a pivoting
933+
**rootfs** — the bundled profile plus a relaxed AppArmor policy, with the
934+
trade-offs noted above — is:
935+
936+
.. code-block:: console
937+
938+
# docker run --cap-add SYS_ADMIN \
939+
--security-opt seccomp=pkg/docker/seccomp-no-af-alg.json \
940+
--security-opt apparmor=unconfined \
941+
ghcr.io/freeunitorg/freeunit:latest-minimal
942+
943+
For a hardened setup, swap in a Docker-default-derived seccomp profile
944+
(gate 2) and a custom AppArmor profile (gate 3) instead of the bundled
945+
profile and ``apparmor=unconfined``.
946+
947+
.. note::
948+
949+
On **Docker 29.4.3** and newer, relaxing AppArmor also relaxes part of
950+
the AF_ALG mitigation. 29.4.2 denied the ``socketcall(2)`` multiplexer
951+
in seccomp, but 29.4.3 reverted that (it broke i386 workloads) and
952+
moved AF_ALG coverage for that path to its AppArmor (``deny network
953+
alg``) and SELinux rules. Neither Docker's current default profile nor
954+
the bundled ``seccomp-no-af-alg.json`` filters ``socketcall``, so with
955+
**apparmor=unconfined** a process can reach AF_ALG through it —
956+
including from a 64-bit binary via the i386 ``int $0x80`` entry. A
957+
custom AppArmor profile that keeps ``deny network alg`` closes this;
958+
``apparmor=unconfined`` does not.
959+
960+
On SELinux hosts, don't assume the SELinux half covers it either: the
961+
``alg_socket`` rule applies only if the daemon runs with
962+
``selinux-enabled: true`` (in ``daemon.json`` or via
963+
``--selinux-enabled``), which is **not** the default. Without it, and
964+
with AppArmor absent or unconfined, the ``socketcall`` path to AF_ALG
965+
stays open whichever of these seccomp profiles you use.
966+
967+
A chroot-only **rootfs** drops the ``"namespaces": {"mount": true}``
968+
requirement, so **pivot_root** is never called. With the automounts left
969+
at their defaults it still uses **mount**, so it needs the same
970+
capability, a seccomp policy allowing **mount**/**openat2**, and, on
971+
AppArmor hosts, the AppArmor step above.
972+
973+
Turning *all three* automounts off, however, removes every **mount** —
974+
and with it every reason to widen the container:
975+
976+
.. code-block:: json
977+
978+
{
979+
"rootfs": ":nxt_ph:`/path/to/rootfs <Path to the prepared root file system>`",
980+
"automount": {
981+
"language_deps": false,
982+
"tmpfs": false,
983+
"procfs": false
984+
}
985+
}
986+
987+
That leaves **chroot(2)** as the only privileged call, and Docker's
988+
defaults already allow it: **CAP_SYS_CHROOT** is in the default capability
989+
set, and the default seccomp profile permits **chroot** for containers
990+
holding it. Such a **rootfs** needs no extra **docker run** flags —
991+
no **SYS_ADMIN**, no seccomp or AppArmor changes. The trade-off is that
992+
the **rootfs** must already contain the language runtime, since
993+
**language_deps** is what bind-mounts it in.
994+
995+
.. note::
996+
997+
For any **isolation** config that mounts or unshares — that is,
998+
everything above except the automounts-off case — **--cap-add
999+
SYS_ADMIN** is required regardless of the seccomp and AppArmor choices:
1000+
a permissive profile alone doesn't grant the capability, so the app
1001+
still fails with **EPERM** if only the seccomp or AppArmor option is
1002+
changed.
1003+
1004+
.. note::
1005+
1006+
This friction is specific to Docker's default confinement, not to
1007+
**isolation** itself. On bare metal — for example, Debian Trixie,
1008+
which leaves unprivileged user namespaces enabled by default — Unit's
1009+
**isolation** feature works with no special flags, since there's no
1010+
seccomp profile, AppArmor policy, or dropped capability standing in
1011+
the way.

0 commit comments

Comments
 (0)