diff --git a/cmd/nerdctl/container_create.go b/cmd/nerdctl/container_create.go index 788429086fd..6b72d4f9c26 100644 --- a/cmd/nerdctl/container_create.go +++ b/cmd/nerdctl/container_create.go @@ -236,6 +236,10 @@ func processContainerCreateOptions(cmd *cobra.Command) (opt types.ContainerCreat if err != nil { return } + opt.UserNS, err = cmd.Flags().GetString("userns") + if err != nil { + return + } // #endregion // #region for security flags diff --git a/cmd/nerdctl/container_run.go b/cmd/nerdctl/container_run.go index 1784161ed98..eb8627fd0fe 100644 --- a/cmd/nerdctl/container_run.go +++ b/cmd/nerdctl/container_run.go @@ -168,6 +168,7 @@ func setCreateFlags(cmd *cobra.Command) { cmd.Flags().StringP("user", "u", "", "Username or UID (format: [:])") cmd.Flags().String("umask", "", "Set the umask inside the container. Defaults to 0022") cmd.Flags().StringSlice("group-add", []string{}, "Add additional groups to join") + cmd.Flags().String("userns", "host", `Set the user namespace mode for the container (auto|host|keep-id|nomap). Defaults to "host"`) // #region security flags cmd.Flags().StringArray("security-opt", []string{}, "Security options") diff --git a/cmd/nerdctl/container_run_linux_test.go b/cmd/nerdctl/container_run_linux_test.go index 678ea108d46..916a5771217 100644 --- a/cmd/nerdctl/container_run_linux_test.go +++ b/cmd/nerdctl/container_run_linux_test.go @@ -358,3 +358,21 @@ func TestRunWithDetachKeys(t *testing.T) { container := base.InspectContainer(containerName) assert.Equal(base.T, container.State.Running, true) } + +func TestRunUserNSHost(t *testing.T) { + t.Parallel() + base := testutil.NewBase(t) + + // container uid should be 0 + uid := fmt.Sprintf("%d\n", 0) + base.Cmd("run", "--rm", "--userns=host", testutil.CommonImage, "id", "-u").AssertOutExactly(uid) +} + +func TestRunUserNSKeepID(t *testing.T) { + t.Parallel() + base := testutil.NewBase(t) + + // container uid should match host uid + uid := fmt.Sprintf("%d\n", os.Getuid()) + base.Cmd("run", "--rm", "--userns=keep-id", testutil.CommonImage, "id", "-u").AssertOutExactly(uid) +} diff --git a/pkg/api/types/container_types.go b/pkg/api/types/container_types.go index f742d8f5443..d6e0ef512fe 100644 --- a/pkg/api/types/container_types.go +++ b/pkg/api/types/container_types.go @@ -158,6 +158,8 @@ type ContainerCreateOptions struct { Umask string // GroupAdd specifies additional groups to join GroupAdd []string + // UserNS specifies the user namespace mode for the container + UserNS string // #endregion // #region for security flags diff --git a/pkg/cmd/container/create.go b/pkg/cmd/container/create.go index 88246e3d7b3..a185538b6f6 100644 --- a/pkg/cmd/container/create.go +++ b/pkg/cmd/container/create.go @@ -225,6 +225,12 @@ func Create(ctx context.Context, client *containerd.Client, args []string, netMa } opts = append(opts, umaskOpts...) + unsOpts, err := generateUserNSOpts(options.UserNS) + if err != nil { + return nil, nil, err + } + opts = append(opts, unsOpts...) + rtCOpts, err := generateRuntimeCOpts(options.GOptions.CgroupManager, options.Runtime) if err != nil { return nil, nil, err diff --git a/pkg/cmd/container/run_userns_linux.go b/pkg/cmd/container/run_userns_linux.go new file mode 100644 index 00000000000..25ec0096738 --- /dev/null +++ b/pkg/cmd/container/run_userns_linux.go @@ -0,0 +1,161 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package container + +import ( + "bufio" + "context" + "fmt" + "io" + "os" + "os/user" + + "github.com/containerd/containerd/containers" + "github.com/containerd/containerd/oci" + "github.com/containerd/nerdctl/pkg/rootlessutil" + "github.com/opencontainers/runtime-spec/specs-go" + "github.com/rootless-containers/rootlesskit/pkg/parent/idtools" +) + +func parseMappingsProc() (uidmap, gidmap []specs.LinuxIDMapping, err error) { + parseMappingProc := func(fn string) ([]specs.LinuxIDMapping, error) { + f, err := os.Open(fn) + if err != nil { + return nil, err + } + defer f.Close() + mappings := []specs.LinuxIDMapping{} + for buf := bufio.NewReader(f); ; { + line, _, err := buf.ReadLine() + if err != nil { + if err == io.EOF { + return mappings, nil + } + return nil, fmt.Errorf("failed to read line from %s: %w", fn, err) + } + if line == nil { + return mappings, nil + } + var cID, hID, size uint32 = 0, 0, 0 + if _, err := fmt.Sscanf(string(line), "%d %d %d", &cID, &hID, &size); err != nil { + return nil, fmt.Errorf("failed to parse %s: %w", line, err) + } + mappings = append(mappings, specs.LinuxIDMapping{ + ContainerID: cID, + HostID: hID, + Size: size, + }) + } + } + uidmap, err = parseMappingProc("/proc/self/uid_map") + if err != nil { + return nil, nil, err + } + gidmap, err = parseMappingProc("/proc/self/gid_map") + if err != nil { + return nil, nil, err + } + return uidmap, gidmap, nil +} + +func generateUserNSOpts(userns string) ([]oci.SpecOpts, error) { + switch userns { + case "host": + return []oci.SpecOpts{withResetUserNamespace()}, nil + case "keep-id": + min := func(a, b int) int { + if a < b { + return a + } + return b + } + + if !rootlessutil.IsRootless() { + uidmap, gidmap, err := parseMappingsProc() + if err != nil { + return nil, err + } + return []oci.SpecOpts{ + oci.WithUserNamespace(uidmap, gidmap), + oci.WithUIDGID(0, 0), + }, nil + } + + uid := rootlessutil.ParentEUID() + gid := rootlessutil.ParentEGID() + + u, err := user.LookupId(fmt.Sprintf("%d", uid)) + if err != nil { + return nil, err + } + uids, gids, err := idtools.GetSubIDRanges(uid, u.Username) + if err != nil { + return nil, err + } + + maxUID, maxGID := 0, 0 + for _, u := range uids { + maxUID += u.Length + } + for _, g := range gids { + maxGID += g.Length + } + + uidmap := []specs.LinuxIDMapping{{ + ContainerID: uint32(uid), + HostID: 0, + Size: 1, + }} + if len(uids) > 0 { + uidmap = append(uidmap, specs.LinuxIDMapping{ + ContainerID: 0, + HostID: 1, + Size: uint32(min(uid, maxUID)), + }) + } + + gidmap := []specs.LinuxIDMapping{{ + ContainerID: uint32(gid), + HostID: 0, + Size: 1, + }} + if len(gids) > 0 { + gidmap = append(gidmap, specs.LinuxIDMapping{ + ContainerID: 0, + HostID: 1, + Size: uint32(min(gid, maxGID)), + }) + } + return []oci.SpecOpts{ + oci.WithUserNamespace(uidmap, gidmap), + oci.WithUIDGID(uint32(uid), uint32(gid)), + }, nil + default: + return nil, fmt.Errorf("invalid UserNS Value:%s", userns) + } +} + +func withResetUserNamespace() oci.SpecOpts { + return func(_ context.Context, _ oci.Client, _ *containers.Container, s *oci.Spec) error { + for i, ns := range s.Linux.Namespaces { + if ns.Type == specs.UserNamespace { + s.Linux.Namespaces = append(s.Linux.Namespaces[:i], s.Linux.Namespaces[i+1:]...) + } + } + return nil + } +}