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>
This commit is contained in:
Giuseppe Scrivano
2020-02-24 17:38:06 +01:00
parent 5b853bb272
commit 3a0a727110
10 changed files with 265 additions and 12 deletions

View File

@ -823,6 +823,7 @@ The following examples are all valid:
Without this argument the command will be run as root in the container. Without this argument the command will be run as root in the container.
**--userns**=*auto*[:OPTIONS]
**--userns**=*host* **--userns**=*host*
**--userns**=*keep-id* **--userns**=*keep-id*
**--userns**=container:container **--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. 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. - `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. - `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. - `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.

View File

@ -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. 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. 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. - **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. - **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. - **ns**: run the container in the given existing user namespace.

View File

@ -339,6 +339,29 @@ func (c *Container) syncContainer() error {
return nil 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 // Create container root filesystem for use
func (c *Container) setupStorage(ctx context.Context) error { func (c *Container) setupStorage(ctx context.Context) error {
span, _ := opentracing.StartSpanFromContext(ctx, "setupStorage") span, _ := opentracing.StartSpanFromContext(ctx, "setupStorage")
@ -398,14 +421,20 @@ func (c *Container) setupStorage(ctx context.Context) error {
options.MountOpts = newOptions options.MountOpts = newOptions
} }
if c.config.Rootfs == "" { c.setupStorageMapping(&options.IDMappingOptions, &c.config.IDMappings)
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) 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 { if err != nil {
return errors.Wrapf(err, "error creating container storage") 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 len(c.config.IDMappings.UIDMap) != 0 || len(c.config.IDMappings.GIDMap) != 0 {
if err := os.Chown(containerInfo.RunDir, c.RootUID(), c.RootGID()); err != nil { if err := os.Chown(containerInfo.RunDir, c.RootUID(), c.RootGID()); err != nil {
return err 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 // Set the default Entrypoint and Command
if containerInfo.Config != nil { if containerInfo.Config != nil {
if c.config.Entrypoint == nil { if c.config.Entrypoint == nil {

View File

@ -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.SetRootPath(c.state.Mountpoint)
g.AddAnnotation(annotations.Created, c.config.CreatedTime.Format(time.RFC3339Nano)) g.AddAnnotation(annotations.Created, c.config.CreatedTime.Format(time.RFC3339Nano))
g.AddAnnotation("org.opencontainers.image.stopSignal", fmt.Sprintf("%d", c.config.StopSignal)) g.AddAnnotation("org.opencontainers.image.stopSignal", fmt.Sprintf("%d", c.config.StopSignal))

View File

@ -6,6 +6,7 @@ import (
"context" "context"
"github.com/containers/libpod/libpod/define" "github.com/containers/libpod/libpod/define"
"github.com/containers/libpod/pkg/lookup"
spec "github.com/opencontainers/runtime-spec/specs-go" 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) { func (c *Container) getOCICgroupPath() (string, error) {
return "", define.ErrNotImplemented return "", define.ErrNotImplemented
} }
func (c *Container) getUserOverrides() *lookup.Overrides {
return nil
}

View File

@ -8,6 +8,7 @@ import (
"github.com/containers/image/v5/types" "github.com/containers/image/v5/types"
"github.com/containers/libpod/libpod/define" "github.com/containers/libpod/libpod/define"
"github.com/containers/storage" "github.com/containers/storage"
"github.com/containers/storage/pkg/idtools"
v1 "github.com/opencontainers/image-spec/specs-go/v1" v1 "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/opentracing/opentracing-go" "github.com/opentracing/opentracing-go"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -35,6 +36,8 @@ type ContainerInfo struct {
Config *v1.Image Config *v1.Image
ProcessLabel string ProcessLabel string
MountLabel string MountLabel string
UIDMap []idtools.IDMap
GIDMap []idtools.IDMap
} }
// RuntimeContainerMetadata is the structure that we encode as JSON and store // 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) logrus.Debugf("container %q has run directory %q", container.ID, containerRunDir)
return ContainerInfo{ return ContainerInfo{
UIDMap: options.UIDMap,
GIDMap: options.GIDMap,
Dir: containerDir, Dir: containerDir,
RunDir: containerRunDir, RunDir: containerRunDir,
Config: imageConfig, Config: imageConfig,

View File

@ -1,7 +1,11 @@
package namespaces package namespaces
import ( import (
"fmt"
"strconv"
"strings" "strings"
"github.com/containers/storage"
) )
const ( const (
@ -92,6 +96,54 @@ func (n UsernsMode) IsKeepID() bool {
return n == "keep-id" 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. // IsPrivate indicates whether the container uses the a private userns.
func (n UsernsMode) IsPrivate() bool { func (n UsernsMode) IsPrivate() bool {
return !(n.IsHost() || n.IsContainer()) return !(n.IsHost() || n.IsContainer())
@ -101,7 +153,7 @@ func (n UsernsMode) IsPrivate() bool {
func (n UsernsMode) Valid() bool { func (n UsernsMode) Valid() bool {
parts := strings.Split(string(n), ":") parts := strings.Split(string(n), ":")
switch mode := parts[0]; mode { switch mode := parts[0]; mode {
case "", privateType, hostType, "keep-id", nsType: case "", privateType, hostType, "keep-id", nsType, "auto":
case containerType: case containerType:
if len(parts) != 2 || parts[1] == "" { if len(parts) != 2 || parts[1] == "" {
return false return false

View File

@ -277,7 +277,7 @@ func (c *UserConfig) ConfigureGenerator(g *generate.Generator) error {
} }
func (c *UserConfig) getPostConfigureNetNS() bool { 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() postConfigureNetNS := hasUserns && !c.UsernsMode.IsHost()
return postConfigureNetNS return postConfigureNetNS
} }
@ -285,7 +285,7 @@ func (c *UserConfig) getPostConfigureNetNS() bool {
// InNS returns true if the UserConfig indicates to be in a dedicated user // InNS returns true if the UserConfig indicates to be in a dedicated user
// namespace. // namespace.
func (c *UserConfig) InNS(isRootless bool) bool { 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()) return isRootless || (hasUserns && !c.UsernsMode.IsHost())
} }

View File

@ -327,6 +327,18 @@ func ParseIDMapping(mode namespaces.UsernsMode, uidMapSlice, gidMapSlice []strin
HostGIDMapping: true, 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 mode.IsKeepID() {
if len(uidMapSlice) > 0 || len(gidMapSlice) > 0 { if len(uidMapSlice) > 0 || len(gidMapSlice) > 0 {
return nil, errors.New("cannot specify custom mappings with --userns=keep-id") return nil, errors.New("cannot specify custom mappings with --userns=keep-id")

View File

@ -4,7 +4,10 @@ package integration
import ( import (
"fmt" "fmt"
"io/ioutil"
"os" "os"
"os/user"
"strings"
. "github.com/containers/libpod/test/utils" . "github.com/containers/libpod/test/utils"
. "github.com/onsi/ginkgo" . "github.com/onsi/ginkgo"
@ -86,6 +89,134 @@ var _ = Describe("Podman UserNS support", func() {
Expect(ok).To(BeTrue()) 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() { It("podman --userns=container:CTR", func() {
ctrName := "userns-ctr" ctrName := "userns-ctr"
session := podmanTest.Podman([]string{"run", "-d", "--uidmap=0:0:1", "--uidmap=1:1:4998", "--name", ctrName, "alpine", "top"}) session := podmanTest.Podman([]string{"run", "-d", "--uidmap=0:0:1", "--uidmap=1:1:4998", "--name", ctrName, "alpine", "top"})