Conversation
A site's npm/bun dependency tree executes on the host — the checkout one-shot runs bun install (postinstall scripts) and the preview runs astro dev — all as the per-instance Unix user. Those users are what make the 0400 mode on an instance's deploy key, session secret and password hash actually isolate one instance from another, so a privilege escalation out of one of them defeats the model entirely. Unprivileged user + network namespaces are a standing kernel local-privesc surface for exactly that code, and a recurring CVE class rather than a single bug. A host running 6.18.38 was demonstrably exposed: it was missing the fix for CVE-2026-64581 (xfrm double-free reachable by an unprivileged user through user and network namespaces, fixed in 6.18.39), and an instance user could both create the namespaces and autoload xfrm inside one. RestrictNamespaces=true blocks that with seccomp, independently of the kernel version, so the class is closed rather than each bug being waited out. Set on the admin and preview units via the shared hardening set, and on the checkout unit directly - it does not take the shared set (that is scoped to the long-running units and is not validated against a package install), but it runs the least trusted code on the host and the namespace restriction applies cleanly. Also corrects nixos/README.md, which claimed "No untrusted code runs on the host: the site code is first-party, and the npm dependency tree builds on Netlify (build-on-push), not here." That contradicts this module's own header comment and is false - Netlify builds the public site, which is a different thing. A reassurance like that is the kind of sentence that stops someone bothering to harden, so it is a bug in its own right. Verified by driving it on a live host rather than reasoning about it: - vector open without it, blocked with it, with a positive control that enters a genuinely different user namespace (an earlier control "passed" only because `true` was not on the unit's PATH, which would have been a false negative) - bun install, astro dev and astro build (the Publish path) all succeed under the restriction; the preview serves normally - seccomp filter mode 2 confirmed on the running admin and preview processes - the working tree stays clean after astro build, so it does not trip the ff-only pull guard added in #42
euoia
force-pushed
the
harden/restrict-namespaces
branch
from
August 27, 2026 08:51
dc24d26 to
58a4dbe
Compare
nixos/README.md called preview routing the one open item and said the localhost dev server "isn't reachable as-is", with the safe exposure "to be finalized against a live instance". That has not been true for some time: the module implements previewHost, the nginx TLS vhost and the auth_request gate, its own header comment documents all three, and it is live and serving on every hosted instance. Verified rather than assumed: an unauthenticated request to a preview host returns the "preview needs an active editor session" page, not content, on all six live instances. This is the stale-doc trap the house rules warn about, in its worst form. A note saying something is a KNOWN LIMITATION reads as an instruction, so the next contributor builds the preview vhost that already exists rather than correcting the claim. Ordinary rot reads as out of date and gets checked; "this is the one open item" does not. Says what it now does, and says the claim went stale, so the correction is visible rather than silently swapped in.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes the kernel local-privesc class that the per-tenant isolation model is exposed to, independently of kernel version.
Why
A site's npm/bun dependency tree executes on the host — the checkout one-shot runs
bun install(postinstall scripts) and the preview runsastro dev— all as the per-instance Unix user. Those users are what make the 0400 mode on an instance's deploy key, session secret and password hash actually isolate one instance from another, so a privesc out of one of them defeats the whole model.Unprivileged user + network namespaces are a standing kernel privesc surface for exactly that code, and a recurring CVE class rather than one bug. A host running 6.18.38 was demonstrably exposed: it was missing the fix for CVE-2026-64581 (xfrm double-free, reachable by an unprivileged user through user and network namespaces, fixed in 6.18.39), and an instance user could both create the namespaces and autoload xfrm inside one:
RestrictNamespaces=trueblocks that with seccomp regardless of kernel version, so the class is closed rather than each bug waited out. Keeping the kernel current still matters — this is defence in depth, not a substitute.What changed
RestrictNamespaces = truein the sharedhardeningset → admin + preview units.RestrictNamespaces = trueon the checkout unit directly. It deliberately does not take the shared set (that is scoped to the long-running units and is not validated against a package install), but it runsbun install— the least trusted code on the host — and the namespace restriction applies cleanly.nixos/README.md.The README claim is worth a look on its own
It said:
That contradicts this module's own header comment, which has always said the dependency tree executes on the host. Netlify builds the public site, which is a different thing. I confirmed the module is the accurate one by running
bun installandastro buildon a host directly.A reassurance like that is the kind of sentence that stops someone bothering to harden, so it is a bug in its own right rather than cosmetic rot.
Verification — driven on a live host
Not reasoned about; applied with
/run/systemd/systemdrop-ins, exercised, then removed so the host matched its declared config again.4026532448vs host4026531837— genuinely openRestrictNamespaces=trueunshare: unshare failed: Operation not permittedRestrictNamespaces=~user nettruechosen as the broader one)bun install(checkout unit)Result=success ExecMainStatus=0astro dev(preview)astro build(the Publish path)9 page(s) built, exit 0Seccomp: 2(filter mode) on admin and previewastro buildAn earlier version of the positive control "passed" only because
truewas not on the unit's minimal PATH — the unshare had actually succeeded. That would have been a false negative in the direction that matters, so the control was rewritten to compare namespace inodes instead of relying on an exit code. Similarly, a first attempt to apply the drop-ins under/etc/systemd/systemsilently failed (read-only on NixOS) andsystemctl showreportedRestrictNamespaces=no— caught by checking the property rather than trusting the write.Deploying
Needs a flake pin bump in the ops repo to reach a host. Worth landing alongside a kernel update so both halves arrive together.
Follow-up, not in this PR
The checkout unit gets no other hardening at all, despite running the least trusted code — no
ProtectSystem,ProtectHome,PrivateTmpor the rest. Extending the shared set to it looks feasible (itsStateDirectorycovers the writes it needs) but wants its own testing round against a realbun install.