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_test.go b/cmd/nerdctl/container_run_test.go index 7e8bfd77a46..ae78f10d49f 100644 --- a/cmd/nerdctl/container_run_test.go +++ b/cmd/nerdctl/container_run_test.go @@ -468,3 +468,20 @@ func TestRunWithTtyAndDetached(t *testing.T) { withTtyContainer := base.InspectContainer(withTtyContainerName) assert.Equal(base.T, 0, withTtyContainer.State.ExitCode) } + +func TestRunUserNSHost(t *testing.T) { + t.Parallel() + base := testutil.NewBase(t) + + 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) + + 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_user.go b/pkg/cmd/container/run_user.go index 7bc9324694b..204c320a85c 100644 --- a/pkg/cmd/container/run_user.go +++ b/pkg/cmd/container/run_user.go @@ -19,10 +19,14 @@ package container import ( "context" "fmt" + "os/user" "strconv" "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 generateUserOpts(user string) ([]oci.SpecOpts, error) { @@ -68,3 +72,80 @@ func withAdditionalUmask(umask uint32) oci.SpecOpts { return 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 + } + + 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 + } +}