diff --git a/cmd/podman/containers/create.go b/cmd/podman/containers/create.go index 8957361445..906ae44523 100644 --- a/cmd/podman/containers/create.go +++ b/cmd/podman/containers/create.go @@ -184,6 +184,9 @@ func createInit(c *cobra.Command) error { if c.Flag("cpu-quota").Changed && c.Flag("cpus").Changed { return errors.Errorf("--cpu-quota and --cpus cannot be set together") } + if c.Flag("pod").Changed && !strings.HasPrefix(c.Flag("pod").Value.String(), "new:") && c.Flag("userns").Changed { + return errors.Errorf("--userns and --pod cannot be set together") + } noHosts, err := c.Flags().GetBool("no-hosts") if err != nil { @@ -309,6 +312,12 @@ func createPodIfNecessary(s *specgen.SpecGenerator, netOpts *entities.NetOptions if len(podName) < 1 { return nil, errors.Errorf("new pod name must be at least one character") } + + userns, err := specgen.ParseUserNamespace(cliVals.UserNS) + if err != nil { + return nil, err + } + createOptions := entities.PodCreateOptions{ Name: podName, Infra: true, @@ -318,6 +327,7 @@ func createPodIfNecessary(s *specgen.SpecGenerator, netOpts *entities.NetOptions Cpus: cliVals.CPUS, CpusetCpus: cliVals.CPUSetCPUs, Pid: cliVals.PID, + Userns: userns, } // Unset config values we passed to the pod to prevent them being used twice for the container and pod. s.ContainerBasicConfig.Hostname = "" diff --git a/cmd/podman/pods/create.go b/cmd/podman/pods/create.go index abc47164b2..bf5b9e350c 100644 --- a/cmd/podman/pods/create.go +++ b/cmd/podman/pods/create.go @@ -48,6 +48,7 @@ var ( podIDFile string replace bool share string + userns string ) func init() { @@ -72,6 +73,10 @@ func init() { flags.StringVar(&createOptions.CGroupParent, cgroupParentflagName, "", "Set parent cgroup for the pod") _ = createCommand.RegisterFlagCompletionFunc(cgroupParentflagName, completion.AutocompleteDefault) + usernsFlagName := "userns" + flags.StringVar(&userns, usernsFlagName, os.Getenv("PODMAN_USERNS"), "User namespace to use") + _ = createCommand.RegisterFlagCompletionFunc(usernsFlagName, common.AutocompleteUserNamespace) + flags.BoolVar(&createOptions.Infra, "infra", true, "Create an infra container associated with the pod to share namespaces with") infraConmonPidfileFlagName := "infra-conmon-pidfile" @@ -178,6 +183,11 @@ func create(cmd *cobra.Command, args []string) error { } } + createOptions.Userns, err = specgen.ParseUserNamespace(userns) + if err != nil { + return err + } + if cmd.Flag("pod-id-file").Changed { podIDFD, err = util.OpenExclusiveFile(podIDFile) if err != nil && os.IsExist(err) { diff --git a/docs/source/markdown/podman-create.1.md b/docs/source/markdown/podman-create.1.md index 51f51c10a5..86cabb9a8a 100644 --- a/docs/source/markdown/podman-create.1.md +++ b/docs/source/markdown/podman-create.1.md @@ -1123,9 +1123,9 @@ Podman allocates unique ranges of UIDs and GIDs from the `containers` subpordina Valid `auto`options: - - *gidmapping*=_HOST_GID:CONTAINER_GID:SIZE_: to force a GID mapping to be present in the user namespace. + - *gidmapping*=_CONTAINER_GID:HOST_GID:SIZE_: to force a GID mapping to be present in the user namespace. - *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 estimate a size for the user namespace. - - *uidmapping*=_HOST_UID:CONTAINER_UID:SIZE_: to force a UID mapping to be present in the user namespace. + - *uidmapping*=_CONTAINER_UID:HOST_UID:SIZE_: to force a UID mapping to be present in the user namespace. **container:**_id_: join the user namespace of the specified container. diff --git a/docs/source/markdown/podman-pod-create.1.md b/docs/source/markdown/podman-pod-create.1.md index 441995cb65..04a23fb531 100644 --- a/docs/source/markdown/podman-pod-create.1.md +++ b/docs/source/markdown/podman-pod-create.1.md @@ -164,6 +164,19 @@ podman generates a UUID for each pod, and if a name is not assigned to the container with **--name** then a random string name will be generated for it. The name is useful any place you need to identify a pod. +#### **--userns**=*mode* + +Set the user namespace mode for all the containers in a pod. It defaults to the **PODMAN_USERNS** environment variable. An empty value ("") means user namespaces are disabled. + +Valid _mode_ values are: + +- *auto[:*_OPTIONS,..._*]*: automatically create a namespace. It is possible to specify these options to `auto`: + - *gidmapping=*_CONTAINER_GID:HOST_GID:SIZE_ to force a GID mapping to be present in the user namespace. + - *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 estimate a size for the user namespace. + - *uidmapping=*_CONTAINER_UID:HOST_UID:SIZE_ to force a UID mapping to be present in the user namespace. +- *host*: run in the user namespace of the caller. The processes running in the container will have the same privileges on the host as any other process launched by the calling user (default). +- *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. + ## EXAMPLES ``` diff --git a/docs/source/markdown/podman-run.1.md b/docs/source/markdown/podman-run.1.md index 38ed445829..6a7489609c 100644 --- a/docs/source/markdown/podman-run.1.md +++ b/docs/source/markdown/podman-run.1.md @@ -1181,9 +1181,9 @@ Podman allocates unique ranges of UIDs and GIDs from the `containers` subpordina Valid `auto`options: - - *gidmapping*=_HOST_GID:CONTAINER_GID:SIZE_: to force a GID mapping to be present in the user namespace. + - *gidmapping*=_CONTAINER_GID:HOST_GID:SIZE_: to force a GID mapping to be present in the user namespace. - *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 estimate a size for the user namespace. - - *uidmapping*=_HOST_UID:CONTAINER_UID:SIZE_: to force a UID mapping to be present in the user namespace. + - *uidmapping*=_CONTAINER_UID:HOST_UID:SIZE_: to force a UID mapping to be present in the user namespace. **container:**_id_: join the user namespace of the specified container. diff --git a/libpod/container.go b/libpod/container.go index f3f4b27b79..d5d5ef1a59 100644 --- a/libpod/container.go +++ b/libpod/container.go @@ -1020,8 +1020,8 @@ func (c *Container) RWSize() (int64, error) { } // IDMappings returns the UID/GID mapping used for the container -func (c *Container) IDMappings() (storage.IDMappingOptions, error) { - return c.config.IDMappings, nil +func (c *Container) IDMappings() storage.IDMappingOptions { + return c.config.IDMappings } // RootUID returns the root user mapping from container diff --git a/libpod/container_internal.go b/libpod/container_internal.go index 8ffcccf4c4..3f7a4807d4 100644 --- a/libpod/container_internal.go +++ b/libpod/container_internal.go @@ -367,6 +367,12 @@ func (c *Container) setupStorageMapping(dest, from *storage.IDMappingOptions) { return } *dest = *from + // If we are creating a container inside a pod, we always want to inherit the + // userns settings from the infra container. So clear the auto userns settings + // so that we don't request storage for a new uid/gid map. + if c.PodID() != "" && !c.IsInfra() { + dest.AutoUserNs = false + } if dest.AutoUserNs { overrides := c.getUserOverrides() dest.AutoUserNsOpts.PasswdFile = overrides.ContainerEtcPasswdPath diff --git a/libpod/container_internal_linux.go b/libpod/container_internal_linux.go index f30f622aca..6d2f7bddc6 100644 --- a/libpod/container_internal_linux.go +++ b/libpod/container_internal_linux.go @@ -659,7 +659,7 @@ func (c *Container) generateSpec(ctx context.Context) (*spec.Spec, error) { } } - if c.config.IDMappings.AutoUserNs { + if c.config.UserNsCtr == "" && c.config.IDMappings.AutoUserNs { if err := g.AddOrReplaceLinuxNamespace(string(spec.UserNamespace), ""); err != nil { return nil, err } @@ -1884,7 +1884,7 @@ func (c *Container) generateResolvConf() (string, error) { return "", err } - return filepath.Join(c.state.RunDir, "resolv.conf"), nil + return destPath, nil } // generateHosts creates a containers hosts file diff --git a/libpod/define/pod_inspect.go b/libpod/define/pod_inspect.go index a173048750..f91fd198d1 100644 --- a/libpod/define/pod_inspect.go +++ b/libpod/define/pod_inspect.go @@ -105,6 +105,8 @@ type InspectPodInfraConfig struct { CPUSetCPUs string `json:"cpuset_cpus,omitempty"` // Pid is the PID namespace mode of the pod's infra container PidNS string `json:"pid_ns,omitempty"` + // UserNS is the usernamespace that all the containers in the pod will join. + UserNS string `json:"userns,omitempty"` } // InspectPodContainerInfo contains information on a container in a pod. diff --git a/libpod/options.go b/libpod/options.go index 071b085e71..b94ef88baf 100644 --- a/libpod/options.go +++ b/libpod/options.go @@ -956,8 +956,9 @@ func WithUserNSFrom(nsCtr *Container) CtrCreateOption { } ctr.config.UserNsCtr = nsCtr.ID() - ctr.config.IDMappings = nsCtr.config.IDMappings - + if err := JSONDeepCopy(nsCtr.IDMappings(), &ctr.config.IDMappings); err != nil { + return err + } g := generate.Generator{Config: ctr.config.Spec} g.ClearLinuxUIDMappings() @@ -968,7 +969,6 @@ func WithUserNSFrom(nsCtr *Container) CtrCreateOption { for _, gidmap := range nsCtr.config.IDMappings.GIDMap { g.AddLinuxGIDMapping(uint32(gidmap.HostID), uint32(gidmap.ContainerID), uint32(gidmap.Size)) } - ctr.config.IDMappings = nsCtr.config.IDMappings return nil } } @@ -2423,6 +2423,24 @@ func WithVolatile() CtrCreateOption { } ctr.config.Volatile = true + + return nil + } +} + +// WithPodUserns sets the userns for the infra container in a pod. +func WithPodUserns(userns specgen.Namespace) PodCreateOption { + return func(pod *Pod) error { + if pod.valid { + return define.ErrPodFinalized + } + + if !pod.config.InfraContainer.HasInfraContainer { + return errors.Wrapf(define.ErrInvalidArg, "cannot configure pod userns as no infra container is being created") + } + + pod.config.InfraContainer.Userns = userns + return nil } } diff --git a/libpod/pod.go b/libpod/pod.go index 0fef7f6f3b..7df15df7b0 100644 --- a/libpod/pod.go +++ b/libpod/pod.go @@ -117,6 +117,7 @@ type InfraContainerConfig struct { Slirp4netns bool `json:"slirp4netns,omitempty"` NetworkOptions map[string][]string `json:"network_options,omitempty"` ResourceLimits *specs.LinuxResources `json:"resource_limits,omitempty"` + Userns specgen.Namespace `json:"userns,omitempty"` } // ID retrieves the pod's ID diff --git a/libpod/pod_api.go b/libpod/pod_api.go index 90d67dbb05..6fea2dfd86 100644 --- a/libpod/pod_api.go +++ b/libpod/pod_api.go @@ -593,6 +593,7 @@ func (p *Pod) Inspect() (*define.InspectPodData, error) { infraConfig.CPUQuota = p.CPUQuota() infraConfig.CPUSetCPUs = p.ResourceLim().CPU.Cpus infraConfig.PidNS = p.PidMode() + infraConfig.UserNS = p.config.InfraContainer.Userns.String() if len(p.config.InfraContainer.DNSServer) > 0 { infraConfig.DNSServer = make([]string, 0, len(p.config.InfraContainer.DNSServer)) diff --git a/libpod/runtime_pod_infra_linux.go b/libpod/runtime_pod_infra_linux.go index d4f8611183..49213032ed 100644 --- a/libpod/runtime_pod_infra_linux.go +++ b/libpod/runtime_pod_infra_linux.go @@ -8,7 +8,9 @@ import ( "github.com/containers/common/pkg/config" "github.com/containers/podman/v3/libpod/define" + "github.com/containers/podman/v3/pkg/namespaces" "github.com/containers/podman/v3/pkg/rootless" + "github.com/containers/podman/v3/pkg/specgen" "github.com/containers/podman/v3/pkg/util" v1 "github.com/opencontainers/image-spec/specs-go/v1" spec "github.com/opencontainers/runtime-spec/specs-go" @@ -110,9 +112,7 @@ func (r *Runtime) makeInfraContainer(ctx context.Context, p *Pod, imgName, rawIm options = append(options, WithNetworkOptions(p.config.InfraContainer.NetworkOptions)) } } - // PostConfigureNetNS should not be set since user namespace sharing is not implemented - // and rootless networking no longer supports post configuration setup - options = append(options, WithNetNS(p.config.InfraContainer.PortBindings, false, netmode, p.config.InfraContainer.Networks)) + options = append(options, WithNetNS(p.config.InfraContainer.PortBindings, !p.config.InfraContainer.Userns.IsHost(), netmode, p.config.InfraContainer.Networks)) } // For each option in InfraContainerConfig - if set, pass into @@ -158,11 +158,39 @@ func (r *Runtime) makeInfraContainer(ctx context.Context, p *Pod, imgName, rawIm g.Config.Linux.Namespaces = newNS } } + + for _, ctl := range r.config.Containers.DefaultSysctls { + sysctl := strings.SplitN(ctl, "=", 2) + if len(sysctl) < 2 { + return nil, errors.Errorf("invalid default sysctl %s", ctl) + } + + // Ignore net sysctls if --net=host + if p.config.InfraContainer.HostNetwork && strings.HasPrefix(sysctl[0], "net.") { + logrus.Infof("Sysctl %s=%s ignored in containers.conf, since Network Namespace set to host", sysctl[0], sysctl[1]) + continue + } + + g.AddLinuxSysctl(sysctl[0], sysctl[1]) + } + g.SetRootReadonly(true) g.SetProcessArgs(infraCtrCommand) logrus.Debugf("Using %q as infra container command", infraCtrCommand) + mapopt, err := util.ParseIDMapping(namespaces.UsernsMode(p.config.InfraContainer.Userns.String()), []string{}, []string{}, "", "") + if err != nil { + return nil, err + } + user, err := specgen.SetupUserNS(mapopt, p.config.InfraContainer.Userns, &g) + if err != nil { + return nil, err + } + if user != "" { + options = append(options, WithUser(user)) + } + g.RemoveMount("/dev/shm") if isRootless { g.RemoveMount("/dev/pts") @@ -210,14 +238,15 @@ func (r *Runtime) makeInfraContainer(ctx context.Context, p *Pod, imgName, rawIm options = append(options, WithRootFSFromImage(imgID, imgName, rawImageName)) options = append(options, WithName(containerName)) options = append(options, withIsInfra()) + options = append(options, WithIDMappings(*mapopt)) if len(p.config.InfraContainer.ConmonPidFile) > 0 { options = append(options, WithConmonPidFile(p.config.InfraContainer.ConmonPidFile)) } newRes := new(spec.LinuxResources) newRes.CPU = new(spec.LinuxCPU) newRes.CPU = p.ResourceLim().CPU - g.Config.Linux.Resources.CPU = newRes.CPU + return r.newContainer(ctx, g.Config, options...) } diff --git a/pkg/api/handlers/libpod/pods.go b/pkg/api/handlers/libpod/pods.go index 4dc8740e2c..ff105bc481 100644 --- a/pkg/api/handlers/libpod/pods.go +++ b/pkg/api/handlers/libpod/pods.go @@ -30,6 +30,12 @@ func PodCreate(w http.ResponseWriter, r *http.Request) { utils.Error(w, "failed to decode specgen", http.StatusInternalServerError, errors.Wrap(err, "failed to decode specgen")) return } + // parse userns so we get the valid default value of userns + psg.Userns, err = specgen.ParseUserNamespace(psg.Userns.String()) + if err != nil { + utils.Error(w, "failed to parse userns", http.StatusInternalServerError, errors.Wrap(err, "failed to parse userns")) + return + } pod, err := generate.MakePod(&psg, runtime) if err != nil { httpCode := http.StatusInternalServerError diff --git a/pkg/domain/entities/pods.go b/pkg/domain/entities/pods.go index 68e335f8d2..c66bf96fcd 100644 --- a/pkg/domain/entities/pods.go +++ b/pkg/domain/entities/pods.go @@ -122,6 +122,7 @@ type PodCreateOptions struct { Pid string Cpus float64 CpusetCpus string + Userns specgen.Namespace } type PodCreateReport struct { @@ -217,6 +218,7 @@ func (p *PodCreateOptions) ToPodSpecGen(s *specgen.PodSpecGenerator) error { s.CPUQuota = *cpuDat.Quota } } + s.Userns = p.Userns return nil } diff --git a/pkg/specgen/generate/kube/kube.go b/pkg/specgen/generate/kube/kube.go index fb7eb99a2d..d6d479c679 100644 --- a/pkg/specgen/generate/kube/kube.go +++ b/pkg/specgen/generate/kube/kube.go @@ -303,6 +303,8 @@ func ToSpecGen(ctx context.Context, opts *CtrSpecGenOptions) (*specgen.SpecGener if opts.NetNSIsHost { s.NetNS.NSMode = specgen.Host } + // Always set the userns to host since k8s doesn't have support for userns yet + s.UserNS.NSMode = specgen.Host // Add labels that come from kube if len(s.Labels) == 0 { diff --git a/pkg/specgen/generate/namespaces.go b/pkg/specgen/generate/namespaces.go index f41186ae41..1d7373093a 100644 --- a/pkg/specgen/generate/namespaces.go +++ b/pkg/specgen/generate/namespaces.go @@ -175,6 +175,11 @@ func namespaceOptions(ctx context.Context, s *specgen.SpecGenerator, rt *libpod. if pod == nil || infraCtr == nil { return nil, errNoInfra } + // Inherit the user from the infra container if it is set and --user has not + // been set explicitly + if infraCtr.User() != "" && s.User == "" { + toReturn = append(toReturn, libpod.WithUser(infraCtr.User())) + } toReturn = append(toReturn, libpod.WithUserNSFrom(infraCtr)) case specgen.FromContainer: userCtr, err := rt.LookupContainer(s.UserNS.Value) @@ -184,7 +189,10 @@ func namespaceOptions(ctx context.Context, s *specgen.SpecGenerator, rt *libpod. toReturn = append(toReturn, libpod.WithUserNSFrom(userCtr)) } - if s.IDMappings != nil { + // This wipes the UserNS settings that get set from the infra container + // when we are inheritting from the pod. So only apply this if the container + // is not being created in a pod. + if s.IDMappings != nil && pod == nil { toReturn = append(toReturn, libpod.WithIDMappings(*s.IDMappings)) } if s.User != "" { @@ -379,46 +387,8 @@ func specConfigureNamespaces(s *specgen.SpecGenerator, g *generate.Generator, rt } // User - switch s.UserNS.NSMode { - case specgen.Path: - if _, err := os.Stat(s.UserNS.Value); err != nil { - return errors.Wrap(err, "cannot find specified user namespace path") - } - if err := g.AddOrReplaceLinuxNamespace(string(spec.UserNamespace), s.UserNS.Value); err != nil { - return err - } - // runc complains if no mapping is specified, even if we join another ns. So provide a dummy mapping - g.AddLinuxUIDMapping(uint32(0), uint32(0), uint32(1)) - g.AddLinuxGIDMapping(uint32(0), uint32(0), uint32(1)) - case specgen.Host: - if err := g.RemoveLinuxNamespace(string(spec.UserNamespace)); err != nil { - return err - } - case specgen.KeepID: - var ( - err error - uid, gid int - ) - s.IDMappings, uid, gid, err = util.GetKeepIDMapping() - if err != nil { - return err - } - g.SetProcessUID(uint32(uid)) - g.SetProcessGID(uint32(gid)) - fallthrough - case specgen.Private: - if err := g.AddOrReplaceLinuxNamespace(string(spec.UserNamespace), ""); err != nil { - return err - } - if s.IDMappings == nil || (len(s.IDMappings.UIDMap) == 0 && len(s.IDMappings.GIDMap) == 0) { - return errors.Errorf("must provide at least one UID or GID mapping to configure a user namespace") - } - for _, uidmap := range s.IDMappings.UIDMap { - g.AddLinuxUIDMapping(uint32(uidmap.HostID), uint32(uidmap.ContainerID), uint32(uidmap.Size)) - } - for _, gidmap := range s.IDMappings.GIDMap { - g.AddLinuxGIDMapping(uint32(gidmap.HostID), uint32(gidmap.ContainerID), uint32(gidmap.Size)) - } + if _, err := specgen.SetupUserNS(s.IDMappings, s.UserNS, g); err != nil { + return err } // Cgroup @@ -474,7 +444,7 @@ func specConfigureNamespaces(s *specgen.SpecGenerator, g *generate.Generator, rt // GetNamespaceOptions transforms a slice of kernel namespaces // into a slice of pod create options. Currently, not all // kernel namespaces are supported, and they will be returned in an error -func GetNamespaceOptions(ns []string) ([]libpod.PodCreateOption, error) { +func GetNamespaceOptions(ns []string, netnsIsHost bool) ([]libpod.PodCreateOption, error) { var options []libpod.PodCreateOption var erroredOptions []libpod.PodCreateOption if ns == nil { @@ -486,7 +456,10 @@ func GetNamespaceOptions(ns []string) ([]libpod.PodCreateOption, error) { case "cgroup": options = append(options, libpod.WithPodCgroups()) case "net": - options = append(options, libpod.WithPodNet()) + // share the netns setting with other containers in the pod only when it is not set to host + if !netnsIsHost { + options = append(options, libpod.WithPodNet()) + } case "mnt": return erroredOptions, errors.Errorf("Mount sharing functionality not supported on pod level") case "pid": diff --git a/pkg/specgen/generate/pod_create.go b/pkg/specgen/generate/pod_create.go index aab29499ed..426cf1b6de 100644 --- a/pkg/specgen/generate/pod_create.go +++ b/pkg/specgen/generate/pod_create.go @@ -27,11 +27,16 @@ func createPodOptions(p *specgen.PodSpecGenerator, rt *libpod.Runtime) ([]libpod ) if !p.NoInfra { options = append(options, libpod.WithInfraContainer()) - nsOptions, err := GetNamespaceOptions(p.SharedNamespaces) + nsOptions, err := GetNamespaceOptions(p.SharedNamespaces, p.NetNS.IsHost()) if err != nil { return nil, err } options = append(options, nsOptions...) + // Use pod user and infra userns only when --userns is not set to host + if !p.Userns.IsHost() { + options = append(options, libpod.WithPodUser()) + options = append(options, libpod.WithPodUserns(p.Userns)) + } // Make our exit command storageConfig := rt.StorageConfig() @@ -154,5 +159,6 @@ func createPodOptions(p *specgen.PodSpecGenerator, rt *libpod.Runtime) ([]libpod if len(p.InfraConmonPidFile) > 0 { options = append(options, libpod.WithInfraConmonPidFile(p.InfraConmonPidFile)) } + return options, nil } diff --git a/pkg/specgen/namespaces.go b/pkg/specgen/namespaces.go index 76fa66bc72..2f4c488113 100644 --- a/pkg/specgen/namespaces.go +++ b/pkg/specgen/namespaces.go @@ -1,10 +1,16 @@ package specgen import ( + "fmt" + "os" "strings" "github.com/containers/podman/v3/pkg/cgroups" "github.com/containers/podman/v3/pkg/rootless" + "github.com/containers/podman/v3/pkg/util" + "github.com/containers/storage" + spec "github.com/opencontainers/runtime-spec/specs-go" + "github.com/opencontainers/runtime-tools/generate" "github.com/pkg/errors" ) @@ -103,6 +109,13 @@ func (n *Namespace) IsKeepID() bool { return n.NSMode == KeepID } +func (n *Namespace) String() string { + if n.Value != "" { + return fmt.Sprintf("%s:%s", n.NSMode, n.Value) + } + return string(n.NSMode) +} + func validateUserNS(n *Namespace) error { if n == nil { return nil @@ -323,3 +336,48 @@ func ParseNetworkString(network string) (Namespace, []string, map[string][]strin } return ns, cniNets, networkOptions, nil } + +func SetupUserNS(idmappings *storage.IDMappingOptions, userns Namespace, g *generate.Generator) (string, error) { + // User + var user string + switch userns.NSMode { + case Path: + if _, err := os.Stat(userns.Value); err != nil { + return user, errors.Wrap(err, "cannot find specified user namespace path") + } + if err := g.AddOrReplaceLinuxNamespace(string(spec.UserNamespace), userns.Value); err != nil { + return user, err + } + // runc complains if no mapping is specified, even if we join another ns. So provide a dummy mapping + g.AddLinuxUIDMapping(uint32(0), uint32(0), uint32(1)) + g.AddLinuxGIDMapping(uint32(0), uint32(0), uint32(1)) + case Host: + if err := g.RemoveLinuxNamespace(string(spec.UserNamespace)); err != nil { + return user, err + } + case KeepID: + mappings, uid, gid, err := util.GetKeepIDMapping() + if err != nil { + return user, err + } + idmappings = mappings + g.SetProcessUID(uint32(uid)) + g.SetProcessGID(uint32(gid)) + user = fmt.Sprintf("%d:%d", uid, gid) + fallthrough + case Private: + if err := g.AddOrReplaceLinuxNamespace(string(spec.UserNamespace), ""); err != nil { + return user, err + } + if idmappings == nil || (len(idmappings.UIDMap) == 0 && len(idmappings.GIDMap) == 0) { + return user, errors.Errorf("must provide at least one UID or GID mapping to configure a user namespace") + } + for _, uidmap := range idmappings.UIDMap { + g.AddLinuxUIDMapping(uint32(uidmap.HostID), uint32(uidmap.ContainerID), uint32(uidmap.Size)) + } + for _, gidmap := range idmappings.GIDMap { + g.AddLinuxGIDMapping(uint32(gidmap.HostID), uint32(gidmap.ContainerID), uint32(gidmap.Size)) + } + } + return user, nil +} diff --git a/pkg/specgen/podspecgen.go b/pkg/specgen/podspecgen.go index 02237afe90..893ebf6754 100644 --- a/pkg/specgen/podspecgen.go +++ b/pkg/specgen/podspecgen.go @@ -67,6 +67,10 @@ type PodBasicConfig struct { // Optional (defaults to private if unset). This sets the PID namespace of the infra container // This configuration will then be shared with the entire pod if PID namespace sharing is enabled via --share Pid Namespace `json:"pid,omitempty:"` + // Userns is used to indicate which kind of Usernamespace to enter. + // Any containers created within the pod will inherit the pod's userns settings. + // Optional + Userns Namespace `json:"userns,omitempty"` } // PodNetworkConfig contains networking configuration for a pod. diff --git a/test/e2e/play_kube_test.go b/test/e2e/play_kube_test.go index 66bfdefe7a..e3096d9323 100644 --- a/test/e2e/play_kube_test.go +++ b/test/e2e/play_kube_test.go @@ -1114,7 +1114,7 @@ var _ = Describe("Podman play kube", func() { }) It("podman play kube should share ipc,net,uts when shareProcessNamespace is set", func() { - SkipIfRootless("Requires root priviledges for sharing few namespaces") + SkipIfRootless("Requires root privileges for sharing few namespaces") err := writeYaml(sharedNamespacePodYaml, kubeYaml) Expect(err).To(BeNil()) diff --git a/test/e2e/pod_create_test.go b/test/e2e/pod_create_test.go index 4c6788b9da..f6f532ce99 100644 --- a/test/e2e/pod_create_test.go +++ b/test/e2e/pod_create_test.go @@ -4,6 +4,7 @@ import ( "fmt" "io/ioutil" "os" + "os/user" "path/filepath" "strconv" "strings" @@ -621,4 +622,223 @@ ENTRYPOINT ["sleep","99999"] Expect(podCreate).Should(ExitWithError()) }) + + It("podman pod create with --userns=keep-id", func() { + if os.Geteuid() == 0 { + Skip("Test only runs without root") + } + + podName := "testPod" + podCreate := podmanTest.Podman([]string{"pod", "create", "--userns", "keep-id", "--name", podName}) + podCreate.WaitWithDefaultTimeout() + Expect(podCreate).Should(Exit(0)) + + session := podmanTest.Podman([]string{"run", "--pod", podName, ALPINE, "id", "-u"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + uid := fmt.Sprintf("%d", os.Geteuid()) + ok, _ := session.GrepString(uid) + Expect(ok).To(BeTrue()) + + // Check passwd + session = podmanTest.Podman([]string{"run", "--pod", podName, ALPINE, "id", "-un"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + u, err := user.Current() + Expect(err).To(BeNil()) + ok, _ = session.GrepString(u.Name) + Expect(ok).To(BeTrue()) + + // root owns /usr + session = podmanTest.Podman([]string{"run", "--pod", podName, ALPINE, "stat", "-c%u", "/usr"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + Expect(session.OutputToString()).To(Equal("0")) + + // fail if --pod and --userns set together + session = podmanTest.Podman([]string{"run", "--pod", podName, "--userns", "keep-id", ALPINE, "id", "-u"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(125)) + }) + + It("podman pod create with --userns=keep-id can add users", func() { + if os.Geteuid() == 0 { + Skip("Test only runs without root") + } + + podName := "testPod" + podCreate := podmanTest.Podman([]string{"pod", "create", "--userns", "keep-id", "--name", podName}) + podCreate.WaitWithDefaultTimeout() + Expect(podCreate).Should(Exit(0)) + + ctrName := "ctr-name" + session := podmanTest.Podman([]string{"run", "--pod", podName, "-d", "--stop-signal", "9", "--name", ctrName, fedoraMinimal, "sleep", "600"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + + // container inside pod inherits user form infra container if --user is not set + // etc/passwd entry will look like 1000:*:1000:1000:container user:/:/bin/sh + exec1 := podmanTest.Podman([]string{"exec", ctrName, "cat", "/etc/passwd"}) + exec1.WaitWithDefaultTimeout() + Expect(exec1).Should(Exit(0)) + Expect(exec1.OutputToString()).To(ContainSubstring("container")) + + exec2 := podmanTest.Podman([]string{"exec", ctrName, "useradd", "testuser"}) + exec2.WaitWithDefaultTimeout() + Expect(exec2).Should(Exit(0)) + + exec3 := podmanTest.Podman([]string{"exec", ctrName, "cat", "/etc/passwd"}) + exec3.WaitWithDefaultTimeout() + Expect(exec3).Should(Exit(0)) + Expect(exec3.OutputToString()).To(ContainSubstring("testuser")) + }) + + It("podman pod create with --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++ { + podName := "testPod" + strconv.Itoa(i) + podCreate := podmanTest.Podman([]string{"pod", "create", "--userns=auto", "--name", podName}) + podCreate.WaitWithDefaultTimeout() + Expect(podCreate).Should(Exit(0)) + + session := podmanTest.Podman([]string{"run", "--pod", podName, ALPINE, "cat", "/proc/self/uid_map"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(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 pod create --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") + } + + podName := "testPod" + podCreate := podmanTest.Podman([]string{"pod", "create", "--userns=auto:size=500", "--name", podName}) + podCreate.WaitWithDefaultTimeout() + Expect(podCreate).Should(Exit(0)) + session := podmanTest.Podman([]string{"run", "--pod", podName, ALPINE, "cat", "/proc/self/uid_map"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + ok, _ := session.GrepString("500") + + podName = "testPod-1" + podCreate = podmanTest.Podman([]string{"pod", "create", "--userns=auto:size=3000", "--name", podName}) + podCreate.WaitWithDefaultTimeout() + Expect(podCreate).Should(Exit(0)) + session = podmanTest.Podman([]string{"run", "--pod", podName, ALPINE, "cat", "/proc/self/uid_map"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + ok, _ = session.GrepString("3000") + + Expect(ok).To(BeTrue()) + }) + + It("podman pod create --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") + } + + podName := "testPod" + podCreate := podmanTest.Podman([]string{"pod", "create", "--userns=auto:uidmapping=0:0:1", "--name", podName}) + podCreate.WaitWithDefaultTimeout() + Expect(podCreate).Should(Exit(0)) + session := podmanTest.Podman([]string{"run", "--pod", podName, ALPINE, "cat", "/proc/self/uid_map"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + output := session.OutputToString() + Expect(output).To(MatchRegexp("\\s0\\s0\\s1")) + + podName = "testPod-1" + podCreate = podmanTest.Podman([]string{"pod", "create", "--userns=auto:size=8192,uidmapping=0:0:1", "--name", podName}) + podCreate.WaitWithDefaultTimeout() + Expect(podCreate).Should(Exit(0)) + session = podmanTest.Podman([]string{"run", "--pod", podName, ALPINE, "cat", "/proc/self/uid_map"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + ok, _ := session.GrepString("8191") + Expect(ok).To(BeTrue()) + }) + + It("podman pod create --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") + } + + podName := "testPod" + podCreate := podmanTest.Podman([]string{"pod", "create", "--userns=auto:gidmapping=0:0:1", "--name", podName}) + podCreate.WaitWithDefaultTimeout() + Expect(podCreate).Should(Exit(0)) + session := podmanTest.Podman([]string{"run", "--pod", podName, ALPINE, "cat", "/proc/self/gid_map"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + output := session.OutputToString() + Expect(output).To(MatchRegexp("\\s0\\s0\\s1")) + + podName = "testPod-1" + podCreate = podmanTest.Podman([]string{"pod", "create", "--userns=auto:size=8192,gidmapping=0:0:1", "--name", podName}) + podCreate.WaitWithDefaultTimeout() + Expect(podCreate).Should(Exit(0)) + session = podmanTest.Podman([]string{"run", "--pod", podName, ALPINE, "cat", "/proc/self/gid_map"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + ok, _ := session.GrepString("8191") + Expect(ok).To(BeTrue()) + }) + })