From 3a0a727110c59332e1a0f5b4a5be311244668a8c Mon Sep 17 00:00:00 2001 From: Giuseppe Scrivano Date: Mon, 24 Feb 2020 17:38:06 +0100 Subject: userns: support --userns=auto automatically pick an empty range and create an user namespace for the container. Signed-off-by: Giuseppe Scrivano --- docs/source/markdown/podman-create.1.md | 6 ++ docs/source/markdown/podman-run.1.md | 6 +- libpod/container_internal.go | 40 ++++++++-- libpod/container_internal_linux.go | 14 ++++ libpod/container_internal_unsupported.go | 5 ++ libpod/storage.go | 5 ++ pkg/namespaces/namespaces.go | 54 ++++++++++++- pkg/spec/namespaces.go | 4 +- pkg/util/utils.go | 12 +++ test/e2e/run_userns_test.go | 131 +++++++++++++++++++++++++++++++ 10 files changed, 265 insertions(+), 12 deletions(-) diff --git a/docs/source/markdown/podman-create.1.md b/docs/source/markdown/podman-create.1.md index 38b95edc3..f0494ca7d 100644 --- a/docs/source/markdown/podman-create.1.md +++ b/docs/source/markdown/podman-create.1.md @@ -823,6 +823,7 @@ The following examples are all valid: Without this argument the command will be run as root in the container. +**--userns**=*auto*[:OPTIONS] **--userns**=*host* **--userns**=*keep-id* **--userns**=container:container @@ -831,6 +832,11 @@ Without this argument the command will be run as root in the container. Set the user namespace mode for the container. It defaults to the **PODMAN_USERNS** environment variable. An empty value means user namespaces are disabled. + +- `auto`: automatically create a namespace. It is possible to specify other options to `auto`. The supported options are + **size=SIZE** to specify an explicit size for the automatic user namespace. e.g. `--userns=auto:size=8192`. If `size` is not specified, `auto` will guess a size for the user namespace. + **uidmapping=HOST_UID:CONTAINER_UID:SIZE** to force a UID mapping to be present in the user namespace. + **gidmapping=HOST_UID:CONTAINER_UID:SIZE** to force a GID mapping to be present in the user namespace. - `container`: join the user namespace of the specified container. - `host`: run in the user namespace of the caller. This is the default if no user namespace options are set. The processes running in the container will have the same privileges on the host as any other process launched by the calling user. - `keep-id`: creates a user namespace where the current rootless user's UID:GID are mapped to the same values in the container. This option is ignored for containers created by the root user. diff --git a/docs/source/markdown/podman-run.1.md b/docs/source/markdown/podman-run.1.md index e8b7d56b7..b21eb9da9 100644 --- a/docs/source/markdown/podman-run.1.md +++ b/docs/source/markdown/podman-run.1.md @@ -862,10 +862,14 @@ Sets the username or UID used and optionally the groupname or GID for the specif Without this argument the command will be run as root in the container. -**--userns**=**host**|**keep-id**|**container:**_id_|**ns:**_namespace_ +**--userns**=**auto**|**host**|**keep-id**|**container:**_id_|**ns:**_namespace_ Set the user namespace mode for the container. It defaults to the **PODMAN_USERNS** environment variable. An empty value means user namespaces are disabled. +- **auto**: automatically create a namespace. It is possible to specify other options to `auto`. The supported options are + **size=SIZE** to specify an explicit size for the automatic user namespace. e.g. `--userns=auto:size=8192`. If `size` is not specified, `auto` will guess a size for the user namespace. + **uidmapping=HOST_UID:CONTAINER_UID:SIZE** to force a UID mapping to be present in the user namespace. + **gidmapping=HOST_UID:CONTAINER_UID:SIZE** to force a GID mapping to be present in the user namespace. - **host**: run in the user namespace of the caller. This is the default if no user namespace options are set. The processes running in the container will have the same privileges on the host as any other process launched by the calling user. - **keep-id**: creates a user namespace where the current rootless user's UID:GID are mapped to the same values in the container. This option is ignored for containers created by the root user. - **ns**: run the container in the given existing user namespace. diff --git a/libpod/container_internal.go b/libpod/container_internal.go index 4e18819b8..c930017a4 100644 --- a/libpod/container_internal.go +++ b/libpod/container_internal.go @@ -339,6 +339,29 @@ func (c *Container) syncContainer() error { return nil } +func (c *Container) setupStorageMapping(dest, from *storage.IDMappingOptions) { + if c.config.Rootfs != "" { + return + } + *dest = *from + if dest.AutoUserNs { + overrides := c.getUserOverrides() + dest.AutoUserNsOpts.PasswdFile = overrides.ContainerEtcPasswdPath + dest.AutoUserNsOpts.GroupFile = overrides.ContainerEtcGroupPath + if c.config.User != "" { + initialSize := uint32(0) + parts := strings.Split(c.config.User, ":") + for _, p := range parts { + s, err := strconv.ParseUint(p, 10, 32) + if err == nil && uint32(s) > initialSize { + initialSize = uint32(s) + } + } + dest.AutoUserNsOpts.InitialSize = initialSize + 1 + } + } +} + // Create container root filesystem for use func (c *Container) setupStorage(ctx context.Context) error { span, _ := opentracing.StartSpanFromContext(ctx, "setupStorage") @@ -398,14 +421,20 @@ func (c *Container) setupStorage(ctx context.Context) error { options.MountOpts = newOptions } - if c.config.Rootfs == "" { - options.IDMappingOptions = c.config.IDMappings - } + c.setupStorageMapping(&options.IDMappingOptions, &c.config.IDMappings) + containerInfo, err := c.runtime.storageService.CreateContainerStorage(ctx, c.runtime.imageContext, c.config.RootfsImageName, c.config.RootfsImageID, c.config.Name, c.config.ID, options) if err != nil { return errors.Wrapf(err, "error creating container storage") } + c.config.IDMappings.UIDMap = containerInfo.UIDMap + c.config.IDMappings.GIDMap = containerInfo.GIDMap + c.config.ProcessLabel = containerInfo.ProcessLabel + c.config.MountLabel = containerInfo.MountLabel + c.config.StaticDir = containerInfo.Dir + c.state.RunDir = containerInfo.RunDir + if len(c.config.IDMappings.UIDMap) != 0 || len(c.config.IDMappings.GIDMap) != 0 { if err := os.Chown(containerInfo.RunDir, c.RootUID(), c.RootGID()); err != nil { return err @@ -416,11 +445,6 @@ func (c *Container) setupStorage(ctx context.Context) error { } } - c.config.ProcessLabel = containerInfo.ProcessLabel - c.config.MountLabel = containerInfo.MountLabel - c.config.StaticDir = containerInfo.Dir - c.state.RunDir = containerInfo.RunDir - // Set the default Entrypoint and Command if containerInfo.Config != nil { if c.config.Entrypoint == nil { diff --git a/libpod/container_internal_linux.go b/libpod/container_internal_linux.go index a3f97f2a6..c40ad45b9 100644 --- a/libpod/container_internal_linux.go +++ b/libpod/container_internal_linux.go @@ -396,6 +396,20 @@ func (c *Container) generateSpec(ctx context.Context) (*spec.Spec, error) { } } + if c.config.IDMappings.AutoUserNs { + if err := g.AddOrReplaceLinuxNamespace(string(spec.UserNamespace), ""); err != nil { + return nil, err + } + g.ClearLinuxUIDMappings() + for _, uidmap := range c.config.IDMappings.UIDMap { + g.AddLinuxUIDMapping(uint32(uidmap.HostID), uint32(uidmap.ContainerID), uint32(uidmap.Size)) + } + g.ClearLinuxGIDMappings() + for _, gidmap := range c.config.IDMappings.GIDMap { + g.AddLinuxGIDMapping(uint32(gidmap.HostID), uint32(gidmap.ContainerID), uint32(gidmap.Size)) + } + } + g.SetRootPath(c.state.Mountpoint) g.AddAnnotation(annotations.Created, c.config.CreatedTime.Format(time.RFC3339Nano)) g.AddAnnotation("org.opencontainers.image.stopSignal", fmt.Sprintf("%d", c.config.StopSignal)) diff --git a/libpod/container_internal_unsupported.go b/libpod/container_internal_unsupported.go index 395271b2a..2a611c2d9 100644 --- a/libpod/container_internal_unsupported.go +++ b/libpod/container_internal_unsupported.go @@ -6,6 +6,7 @@ import ( "context" "github.com/containers/libpod/libpod/define" + "github.com/containers/libpod/pkg/lookup" spec "github.com/opencontainers/runtime-spec/specs-go" ) @@ -44,3 +45,7 @@ func (c *Container) copyOwnerAndPerms(source, dest string) error { func (c *Container) getOCICgroupPath() (string, error) { return "", define.ErrNotImplemented } + +func (c *Container) getUserOverrides() *lookup.Overrides { + return nil +} diff --git a/libpod/storage.go b/libpod/storage.go index d675f4ffe..34e40f699 100644 --- a/libpod/storage.go +++ b/libpod/storage.go @@ -8,6 +8,7 @@ import ( "github.com/containers/image/v5/types" "github.com/containers/libpod/libpod/define" "github.com/containers/storage" + "github.com/containers/storage/pkg/idtools" v1 "github.com/opencontainers/image-spec/specs-go/v1" "github.com/opentracing/opentracing-go" "github.com/pkg/errors" @@ -35,6 +36,8 @@ type ContainerInfo struct { Config *v1.Image ProcessLabel string MountLabel string + UIDMap []idtools.IDMap + GIDMap []idtools.IDMap } // RuntimeContainerMetadata is the structure that we encode as JSON and store @@ -166,6 +169,8 @@ func (r *storageService) CreateContainerStorage(ctx context.Context, systemConte logrus.Debugf("container %q has run directory %q", container.ID, containerRunDir) return ContainerInfo{ + UIDMap: options.UIDMap, + GIDMap: options.GIDMap, Dir: containerDir, RunDir: containerRunDir, Config: imageConfig, diff --git a/pkg/namespaces/namespaces.go b/pkg/namespaces/namespaces.go index 14453e7f4..2cb3c3f20 100644 --- a/pkg/namespaces/namespaces.go +++ b/pkg/namespaces/namespaces.go @@ -1,7 +1,11 @@ package namespaces import ( + "fmt" + "strconv" "strings" + + "github.com/containers/storage" ) const ( @@ -92,6 +96,54 @@ func (n UsernsMode) IsKeepID() bool { return n == "keep-id" } +// IsAuto indicates whether container uses the "auto" userns mode. +func (n UsernsMode) IsAuto() bool { + parts := strings.Split(string(n), ":") + return parts[0] == "auto" +} + +// GetAutoOptions returns a AutoUserNsOptions with the settings to setup automatically +// a user namespace. +func (n UsernsMode) GetAutoOptions() (*storage.AutoUserNsOptions, error) { + parts := strings.SplitN(string(n), ":", 2) + if parts[0] != "auto" { + return nil, fmt.Errorf("wrong user namespace mode") + } + options := storage.AutoUserNsOptions{} + if len(parts) == 1 { + return &options, nil + } + for _, o := range strings.Split(parts[1], ",") { + v := strings.SplitN(o, "=", 2) + if len(v) != 2 { + return nil, fmt.Errorf("invalid option specified: %q", o) + } + switch v[0] { + case "size": + s, err := strconv.ParseUint(v[1], 10, 32) + if err != nil { + return nil, err + } + options.Size = uint32(s) + case "uidmapping": + mapping, err := storage.ParseIDMapping([]string{v[1]}, nil, "", "") + if err != nil { + return nil, err + } + options.AdditionalUIDMappings = append(options.AdditionalUIDMappings, mapping.UIDMap...) + case "gidmapping": + mapping, err := storage.ParseIDMapping(nil, []string{v[1]}, "", "") + if err != nil { + return nil, err + } + options.AdditionalGIDMappings = append(options.AdditionalGIDMappings, mapping.GIDMap...) + default: + return nil, fmt.Errorf("unknown option specified: %q", v[0]) + } + } + return &options, nil +} + // IsPrivate indicates whether the container uses the a private userns. func (n UsernsMode) IsPrivate() bool { return !(n.IsHost() || n.IsContainer()) @@ -101,7 +153,7 @@ func (n UsernsMode) IsPrivate() bool { func (n UsernsMode) Valid() bool { parts := strings.Split(string(n), ":") switch mode := parts[0]; mode { - case "", privateType, hostType, "keep-id", nsType: + case "", privateType, hostType, "keep-id", nsType, "auto": case containerType: if len(parts) != 2 || parts[1] == "" { return false diff --git a/pkg/spec/namespaces.go b/pkg/spec/namespaces.go index 838d95c54..aebc90f68 100644 --- a/pkg/spec/namespaces.go +++ b/pkg/spec/namespaces.go @@ -277,7 +277,7 @@ func (c *UserConfig) ConfigureGenerator(g *generate.Generator) error { } func (c *UserConfig) getPostConfigureNetNS() bool { - hasUserns := c.UsernsMode.IsContainer() || c.UsernsMode.IsNS() || len(c.IDMappings.UIDMap) > 0 || len(c.IDMappings.GIDMap) > 0 + hasUserns := c.UsernsMode.IsContainer() || c.UsernsMode.IsNS() || c.UsernsMode.IsAuto() || len(c.IDMappings.UIDMap) > 0 || len(c.IDMappings.GIDMap) > 0 postConfigureNetNS := hasUserns && !c.UsernsMode.IsHost() return postConfigureNetNS } @@ -285,7 +285,7 @@ func (c *UserConfig) getPostConfigureNetNS() bool { // InNS returns true if the UserConfig indicates to be in a dedicated user // namespace. func (c *UserConfig) InNS(isRootless bool) bool { - hasUserns := c.UsernsMode.IsContainer() || c.UsernsMode.IsNS() || len(c.IDMappings.UIDMap) > 0 || len(c.IDMappings.GIDMap) > 0 + hasUserns := c.UsernsMode.IsContainer() || c.UsernsMode.IsNS() || c.UsernsMode.IsAuto() || len(c.IDMappings.UIDMap) > 0 || len(c.IDMappings.GIDMap) > 0 return isRootless || (hasUserns && !c.UsernsMode.IsHost()) } diff --git a/pkg/util/utils.go b/pkg/util/utils.go index 0c055745d..372c7c53b 100644 --- a/pkg/util/utils.go +++ b/pkg/util/utils.go @@ -327,6 +327,18 @@ func ParseIDMapping(mode namespaces.UsernsMode, uidMapSlice, gidMapSlice []strin HostGIDMapping: true, } + if mode.IsAuto() { + var err error + options.HostUIDMapping = false + options.HostGIDMapping = false + options.AutoUserNs = true + opts, err := mode.GetAutoOptions() + if err != nil { + return nil, err + } + options.AutoUserNsOpts = *opts + return &options, nil + } if mode.IsKeepID() { if len(uidMapSlice) > 0 || len(gidMapSlice) > 0 { return nil, errors.New("cannot specify custom mappings with --userns=keep-id") diff --git a/test/e2e/run_userns_test.go b/test/e2e/run_userns_test.go index e873f5abe..25f12ec2e 100644 --- a/test/e2e/run_userns_test.go +++ b/test/e2e/run_userns_test.go @@ -4,7 +4,10 @@ package integration import ( "fmt" + "io/ioutil" "os" + "os/user" + "strings" . "github.com/containers/libpod/test/utils" . "github.com/onsi/ginkgo" @@ -86,6 +89,134 @@ var _ = Describe("Podman UserNS support", func() { Expect(ok).To(BeTrue()) }) + It("podman --userns=auto", func() { + u, err := user.Current() + Expect(err).To(BeNil()) + name := u.Name + if name == "root" { + name = "containers" + } + + content, err := ioutil.ReadFile("/etc/subuid") + if err != nil { + Skip("cannot read /etc/subuid") + } + if !strings.Contains(string(content), name) { + Skip("cannot find mappings for the current user") + } + + m := make(map[string]string) + for i := 0; i < 5; i++ { + session := podmanTest.Podman([]string{"run", "--userns=auto", "alpine", "cat", "/proc/self/uid_map"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + l := session.OutputToString() + Expect(strings.Contains(l, "1024")).To(BeTrue()) + m[l] = l + } + // check for no duplicates + Expect(len(m)).To(Equal(5)) + }) + + It("podman --userns=auto:size=%d", func() { + u, err := user.Current() + Expect(err).To(BeNil()) + + name := u.Name + if name == "root" { + name = "containers" + } + + content, err := ioutil.ReadFile("/etc/subuid") + if err != nil { + Skip("cannot read /etc/subuid") + } + if !strings.Contains(string(content), name) { + Skip("cannot find mappings for the current user") + } + + session := podmanTest.Podman([]string{"run", "--userns=auto:size=500", "alpine", "cat", "/proc/self/uid_map"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + ok, _ := session.GrepString("500") + + session = podmanTest.Podman([]string{"run", "--userns=auto:size=3000", "alpine", "cat", "/proc/self/uid_map"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + ok, _ = session.GrepString("3000") + + session = podmanTest.Podman([]string{"run", "--userns=auto", "--user=2000:3000", "alpine", "cat", "/proc/self/uid_map"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + ok, _ = session.GrepString("3001") + + session = podmanTest.Podman([]string{"run", "--userns=auto", "--user=4000:1000", "alpine", "cat", "/proc/self/uid_map"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + ok, _ = session.GrepString("4001") + Expect(ok).To(BeTrue()) + }) + + It("podman --userns=auto:uidmapping=", func() { + u, err := user.Current() + Expect(err).To(BeNil()) + + name := u.Name + if name == "root" { + name = "containers" + } + + content, err := ioutil.ReadFile("/etc/subuid") + if err != nil { + Skip("cannot read /etc/subuid") + } + if !strings.Contains(string(content), name) { + Skip("cannot find mappings for the current user") + } + + session := podmanTest.Podman([]string{"run", "--userns=auto:uidmapping=0:0:1", "alpine", "cat", "/proc/self/uid_map"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + output := session.OutputToString() + Expect(output).To(MatchRegexp("\\s0\\s0\\s1")) + + session = podmanTest.Podman([]string{"run", "--userns=auto:size=8192,uidmapping=0:0:1", "alpine", "cat", "/proc/self/uid_map"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + ok, _ := session.GrepString("8191") + Expect(ok).To(BeTrue()) + }) + + It("podman --userns=auto:gidmapping=", func() { + u, err := user.Current() + Expect(err).To(BeNil()) + + name := u.Name + if name == "root" { + name = "containers" + } + + content, err := ioutil.ReadFile("/etc/subuid") + if err != nil { + Skip("cannot read /etc/subuid") + } + if !strings.Contains(string(content), name) { + Skip("cannot find mappings for the current user") + } + + session := podmanTest.Podman([]string{"run", "--userns=auto:gidmapping=0:0:1", "alpine", "cat", "/proc/self/gid_map"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + output := session.OutputToString() + Expect(output).To(MatchRegexp("\\s0\\s0\\s1")) + + session = podmanTest.Podman([]string{"run", "--userns=auto:size=8192,gidmapping=0:0:1", "alpine", "cat", "/proc/self/gid_map"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + ok, _ := session.GrepString("8191") + Expect(ok).To(BeTrue()) + }) + It("podman --userns=container:CTR", func() { ctrName := "userns-ctr" session := podmanTest.Podman([]string{"run", "-d", "--uidmap=0:0:1", "--uidmap=1:1:4998", "--name", ctrName, "alpine", "top"}) -- cgit v1.2.3-54-g00ecf