From 55da34e5e0473c3f54c2b02b4044bb789cd2b73f Mon Sep 17 00:00:00 2001 From: Plamen Bardarov Date: Mon, 27 Apr 2026 17:07:56 +0300 Subject: [PATCH] Enable NoNewPrivileges for unprivileged container processes Add --no-new-privileges CLI flag that sets NoNewPrivileges=true in the OCI runtime spec for unprivileged containers and peas. This prevents privilege escalation via setuid binaries and file capabilities. The flag is propagated to all spawned processes (cf ssh, garden run) via BuildProcess. Privileged containers are unaffected. --- gqt/runner/runner.go | 1 + gqt/security_test.go | 105 +++++++++++++++++++++++++++++++ guardiancmd/command.go | 5 ++ rundmc/processes/builder.go | 1 + rundmc/processes/builder_test.go | 14 +++++ 5 files changed, 126 insertions(+) diff --git a/gqt/runner/runner.go b/gqt/runner/runner.go index 323b78f1a..c363c41f0 100644 --- a/gqt/runner/runner.go +++ b/gqt/runner/runner.go @@ -96,6 +96,7 @@ type GdnRunnerConfig struct { CleanupProcessDirsOnWait *bool `flag:"cleanup-process-dirs-on-wait"` DisablePrivilegedContainers *bool `flag:"disable-privileged-containers"` AppArmor string `flag:"apparmor"` + NoNewPrivileges *bool `flag:"no-new-privileges"` Tag string `flag:"tag"` NetworkPool string `flag:"network-pool"` ContainerdSocket string `flag:"containerd-socket"` diff --git a/gqt/security_test.go b/gqt/security_test.go index 11d96790f..b17ebbdd2 100644 --- a/gqt/security_test.go +++ b/gqt/security_test.go @@ -160,6 +160,111 @@ var _ = Describe("Security", func() { }) }) + Describe("NoNewPrivileges", func() { + Context("when the --no-new-privileges flag is set", func() { + BeforeEach(func() { + config.NoNewPrivileges = boolptr(true) + }) + + Context("when running processes in unprivileged containers", func() { + var ( + container garden.Container + err error + ) + + JustBeforeEach(func() { + container, err = client.Create(garden.ContainerSpec{}) + Expect(err).NotTo(HaveOccurred()) + }) + + It("should set NoNewPrivileges on the process", func() { + buffer := gbytes.NewBuffer() + + _, err = container.Run(garden.ProcessSpec{ + Path: "grep", + Args: []string{"NoNewPrivs", "/proc/self/status"}, + }, garden.ProcessIO{ + Stdout: io.MultiWriter(GinkgoWriter, buffer), + Stderr: GinkgoWriter, + }) + Expect(err).NotTo(HaveOccurred()) + + Eventually(buffer).Should(gbytes.Say(`NoNewPrivs:\s+1`)) + }) + + Context("when running a pea", func() { + var peaRootfs string + + BeforeEach(func() { + peaRootfs = createPeaRootfsTar() + }) + + AfterEach(func() { + Expect(os.RemoveAll(filepath.Dir(peaRootfs))).To(Succeed()) + }) + + It("should set NoNewPrivileges on the process", func() { + buffer := gbytes.NewBuffer() + + _, err = container.Run(garden.ProcessSpec{ + Path: "grep", + Args: []string{"NoNewPrivs", "/proc/self/status"}, + Image: garden.ImageRef{URI: peaRootfs}, + }, garden.ProcessIO{ + Stdout: io.MultiWriter(GinkgoWriter, buffer), + Stderr: GinkgoWriter, + }) + Expect(err).NotTo(HaveOccurred()) + + Eventually(buffer).Should(gbytes.Say(`NoNewPrivs:\s+1`)) + }) + }) + }) + + Context("when running processes in privileged containers", func() { + It("should not set NoNewPrivileges", func() { + container, err := client.Create(garden.ContainerSpec{ + Privileged: true, + }) + Expect(err).NotTo(HaveOccurred()) + + buffer := gbytes.NewBuffer() + + _, err = container.Run(garden.ProcessSpec{ + Path: "grep", + Args: []string{"NoNewPrivs", "/proc/self/status"}, + }, garden.ProcessIO{ + Stdout: io.MultiWriter(GinkgoWriter, buffer), + Stderr: GinkgoWriter, + }) + Expect(err).NotTo(HaveOccurred()) + + Eventually(buffer).Should(gbytes.Say(`NoNewPrivs:\s+0`)) + }) + }) + }) + + Context("when the --no-new-privileges flag is not set", func() { + It("should not set NoNewPrivileges on processes in unprivileged containers", func() { + container, err := client.Create(garden.ContainerSpec{}) + Expect(err).NotTo(HaveOccurred()) + + buffer := gbytes.NewBuffer() + + _, err = container.Run(garden.ProcessSpec{ + Path: "grep", + Args: []string{"NoNewPrivs", "/proc/self/status"}, + }, garden.ProcessIO{ + Stdout: io.MultiWriter(GinkgoWriter, buffer), + Stderr: GinkgoWriter, + }) + Expect(err).NotTo(HaveOccurred()) + + Eventually(buffer).Should(gbytes.Say(`NoNewPrivs:\s+0`)) + }) + }) + }) + Describe("ptrace in seccomp allow rules", func() { It("should allow the ptrace syscall without CAP_SYS_PTRACE", func() { container, err := client.Create(garden.ContainerSpec{ diff --git a/guardiancmd/command.go b/guardiancmd/command.go index 78e930ca6..ea7df75f8 100644 --- a/guardiancmd/command.go +++ b/guardiancmd/command.go @@ -117,6 +117,7 @@ type CommonCommand struct { DefaultGraceTime time.Duration `long:"default-grace-time" description:"Default time after which idle containers should expire."` DestroyContainersOnStartup bool `long:"destroy-containers-on-startup" description:"Clean up all the existing containers on startup."` ApparmorProfile string `long:"apparmor" description:"Apparmor profile to use for unprivileged container processes"` + NoNewPrivileges bool `long:"no-new-privileges" description:"Set NoNewPrivileges on unprivileged container processes"` } `group:"Container Lifecycle"` Bin struct { @@ -527,6 +528,10 @@ func (cmd *CommonCommand) wireContainerizer( } unprivilegedBundle.Spec.Linux.Seccomp = seccomp + if cmd.Containers.NoNewPrivileges { + unprivilegedBundle.Spec.Process.NoNewPrivileges = true + } + if cmd.Containers.ApparmorProfile != "" { unprivilegedBundle = unprivilegedBundle.WithApparmorProfile(cmd.Containers.ApparmorProfile) } diff --git a/rundmc/processes/builder.go b/rundmc/processes/builder.go index 25f2f66f2..39069143f 100644 --- a/rundmc/processes/builder.go +++ b/rundmc/processes/builder.go @@ -53,6 +53,7 @@ func (p *ProcBuilder) BuildProcess(bndl goci.Bndl, spec garden.ProcessSpec, user Rlimits: toRlimits(spec.Limits), Terminal: spec.TTY != nil, ApparmorProfile: bndl.Process().ApparmorProfile, + NoNewPrivileges: bndl.Process().NoNewPrivileges, } } diff --git a/rundmc/processes/builder_test.go b/rundmc/processes/builder_test.go index 29def9dbe..b7e1dd746 100644 --- a/rundmc/processes/builder_test.go +++ b/rundmc/processes/builder_test.go @@ -196,6 +196,20 @@ var _ = Describe("ProcBuilder", func() { Expect(preparedProc.ApparmorProfile).To(Equal("default-profile")) }) + It("passes NoNewPrivileges from the bundle", func() { + Expect(preparedProc.NoNewPrivileges).To(BeFalse()) + }) + + Context("when the bundle has NoNewPrivileges set", func() { + BeforeEach(func() { + bndl.Spec.Process.NoNewPrivileges = true + }) + + It("propagates NoNewPrivileges to the process", func() { + Expect(preparedProc.NoNewPrivileges).To(BeTrue()) + }) + }) + It("passes the UID, GID and SGIDs", func() { Expect(preparedProc.User.UID).To(Equal(uint32(1))) Expect(preparedProc.User.GID).To(Equal(uint32(2)))