diff --git a/Dockerfile b/Dockerfile index 0bc97c0..969655d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -71,7 +71,6 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ libbsd0 libgps28 libasound2 libjack-jackd2-0 libpulse0 \ direwolf \ ca-certificates \ - libcap2-bin \ && rm -rf /var/lib/apt/lists/* /var/cache/apt/* /var/log/* # binaries @@ -89,16 +88,15 @@ EXPOSE 8080 8001 8002 # A non-root user for the running process. RUN useradd --system --no-create-home --shell /usr/sbin/nologin sim -# -rt-priority renices the router and its TNC children, which needs -# CAP_SYS_NICE — but the container runs as the non-root `sim` user, and -# docker's --cap-add only populates the *bounding* set (a non-root process -# doesn't inherit it). File capabilities on the binaries grant it to the -# process directly; still inert unless the container ALSO gets -# --cap-add SYS_NICE (file caps can't exceed the bounding set), and the -# binaries only use it when -rt-priority is passed. -RUN setcap cap_sys_nice+ep /usr/local/bin/sim-web \ - && setcap cap_sys_nice+ep /usr/local/bin/sim-router - +# NOTE on -rt-priority: it renices the router + TNC children, which needs +# CAP_SYS_NICE. Grant it at RUNTIME with `--cap-add SYS_NICE` (and run the +# container as root, or with that cap in the user-namespace) — NOT with file +# capabilities on the binaries. A `setcap cap_sys_nice+ep` here makes the +# binary REFUSE TO exec (EPERM "operation not permitted") on any host whose +# bounding set lacks the cap — e.g. an unprivileged LXC — breaking the image +# for everyone who isn't using -rt-priority. -rt-priority already degrades +# gracefully (logs a one-line warning and runs at normal priority) where the +# platform won't grant the cap, so the binary must stay capability-free. USER sim ENTRYPOINT ["/usr/local/bin/sim-web", "-addr", ":8080", "-config", "/etc/sim/network.yaml"] diff --git a/README.md b/README.md index e9e3678..c1373de 100644 --- a/README.md +++ b/README.md @@ -110,8 +110,9 @@ services: cap_add: [SYS_NICE] ``` +The capability is granted at **runtime** (`--cap-add` / `cap_add`), deliberately — the image carries no file capabilities on its binaries. (An earlier build set `cap_sys_nice+ep` on `sim-web`/`sim-router`; that made them refuse to `exec` with *"operation not permitted"* on any host whose bounding set lacks the cap — e.g. an unprivileged LXC — breaking the image even for runs that never pass `-rt-priority`. Runtime cap-add is the correct mechanism.) -> **Nested-container hosts:** on a host that is itself an unprivileged container (e.g. Docker inside an unprivileged Proxmox LXC), the kernel checks CAP_SYS_NICE against the *init* user namespace, so negative nice is unavailable to anything inside — even container root. The flag then logs its one-line warning and the sim runs at normal priority; everything else is unaffected. +> **Nested-container hosts:** on a host that is itself an unprivileged container (e.g. Docker inside an unprivileged Proxmox LXC), the kernel checks CAP_SYS_NICE against the *init* user namespace, so negative nice is unavailable to anything inside — even container root, even with `cap_add`. The flag then logs its one-line warning and the sim runs at normal priority; everything else is unaffected. ## Quick install (curl | sudo bash) diff --git a/internal/dockerfile/dockerfile_test.go b/internal/dockerfile/dockerfile_test.go new file mode 100644 index 0000000..1ece57c --- /dev/null +++ b/internal/dockerfile/dockerfile_test.go @@ -0,0 +1,52 @@ +package dockerfile + +// Guards against reintroducing file capabilities on the shipped binaries. +// +// A `setcap cap_sys_nice+ep` on sim-web/sim-router makes them refuse to exec +// ("operation not permitted") on any host whose capability bounding set lacks +// CAP_SYS_NICE — e.g. an unprivileged LXC — which broke the image for every +// run, including those that never pass -rt-priority. The capability must be +// granted at runtime (--cap-add SYS_NICE) instead, so the binaries stay +// capability-free. This caught a real production breakage (packethacking/net-sim +// — the lab net-sim crash-looped after the setcap landed). + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func TestDockerfileHasNoSetcapOnBinaries(t *testing.T) { + // Walk up to the repo root (where the Dockerfile lives). + wd, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + var dockerfile string + for d := wd; d != "/" && d != "."; d = filepath.Dir(d) { + p := filepath.Join(d, "Dockerfile") + if _, err := os.Stat(p); err == nil { + dockerfile = p + break + } + } + if dockerfile == "" { + t.Fatal("could not locate Dockerfile from " + wd) + } + b, err := os.ReadFile(dockerfile) + if err != nil { + t.Fatal(err) + } + // Allow the word inside a comment explaining why we don't do it; only fail + // on an actual RUN setcap ... line. + for _, line := range strings.Split(string(b), "\n") { + trimmed := strings.TrimSpace(line) + if strings.HasPrefix(trimmed, "#") { + continue + } + if strings.Contains(trimmed, "setcap") { + t.Fatalf("Dockerfile reintroduced file capabilities (%q) — these break exec on hosts lacking the cap in their bounding set; grant CAP_SYS_NICE at runtime via --cap-add instead", trimmed) + } + } +}