summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGiuseppe Scrivano <gscrivan@redhat.com>2020-02-24 17:38:06 +0100
committerGiuseppe Scrivano <gscrivan@redhat.com>2020-04-06 16:32:36 +0200
commit3a0a727110c59332e1a0f5b4a5be311244668a8c (patch)
treeff1afd6d97f329718f15dd541aa95e721690fe65
parent5b853bb272a754a54fa78a3e619de0304864151f (diff)
downloadpodman-3a0a727110c59332e1a0f5b4a5be311244668a8c.tar.gz
podman-3a0a727110c59332e1a0f5b4a5be311244668a8c.tar.bz2
podman-3a0a727110c59332e1a0f5b4a5be311244668a8c.zip
userns: support --userns=auto
automatically pick an empty range and create an user namespace for the container. Signed-off-by: Giuseppe Scrivano <gscrivan@redhat.com>
-rw-r--r--docs/source/markdown/podman-create.1.md6
-rw-r--r--docs/source/markdown/podman-run.1.md6
-rw-r--r--libpod/container_internal.go40
-rw-r--r--libpod/container_internal_linux.go14
-rw-r--r--libpod/container_internal_unsupported.go5
-rw-r--r--libpod/storage.go5
-rw-r--r--pkg/namespaces/namespaces.go54
-rw-r--r--pkg/spec/namespaces.go4
-rw-r--r--pkg/util/utils.go12
-rw-r--r--test/e2e/run_userns_test.go131
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"})