@@ -773,8 +773,9 @@ Docker: Seccomp Profile (AF_ALG)
773773kernel ``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
775775access — 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/ >`_
840857isolation.
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