From 80c0fceb24b70a85f3f2ca8be29f4a131c0881d4 Mon Sep 17 00:00:00 2001 From: Daniel J Walsh Date: Wed, 13 Apr 2022 14:06:05 -0400 Subject: Add support for --userns=nomap From a security point of view, it would be nice to be able to map a rootless usernamespace that does not use your own UID within the container. This would add protection against a hostile process escapping the container and reading content in your homedir. Signed-off-by: Daniel J Walsh --- pkg/domain/infra/runtime_libpod.go | 67 +++++++++++------------ pkg/namespaces/namespaces.go | 7 ++- pkg/specgen/generate/namespaces.go | 26 +++++---- pkg/specgen/namespaces.go | 53 +++++++++++++++---- pkg/util/utils.go | 105 +++++++++++++++++++++++-------------- 5 files changed, 162 insertions(+), 96 deletions(-) (limited to 'pkg') diff --git a/pkg/domain/infra/runtime_libpod.go b/pkg/domain/infra/runtime_libpod.go index dffd90dbe..5fdc252e2 100644 --- a/pkg/domain/infra/runtime_libpod.go +++ b/pkg/domain/infra/runtime_libpod.go @@ -276,46 +276,47 @@ func ParseIDMapping(mode namespaces.UsernsMode, uidMapSlice, gidMapSlice []strin if len(subUIDMap) > 0 || len(subGIDMap) > 0 { return nil, errors.New("cannot specify subuidmap or subgidmap with --userns=keep-id") } - if rootless.IsRootless() { - min := func(a, b int) int { - if a < b { - return a - } - return b + if !rootless.IsRootless() { + return nil, errors.New("keep-id is only supported in rootless mode") + } + min := func(a, b int) int { + if a < b { + return a } + return b + } - uid := rootless.GetRootlessUID() - gid := rootless.GetRootlessGID() - - uids, gids, err := rootless.GetConfiguredMappings() - if err != nil { - return nil, errors.Wrapf(err, "cannot read mappings") - } - maxUID, maxGID := 0, 0 - for _, u := range uids { - maxUID += u.Size - } - for _, g := range gids { - maxGID += g.Size - } + uid := rootless.GetRootlessUID() + gid := rootless.GetRootlessGID() - options.UIDMap, options.GIDMap = nil, nil + uids, gids, err := rootless.GetConfiguredMappings() + if err != nil { + return nil, errors.Wrapf(err, "cannot read mappings") + } + maxUID, maxGID := 0, 0 + for _, u := range uids { + maxUID += u.Size + } + for _, g := range gids { + maxGID += g.Size + } - options.UIDMap = append(options.UIDMap, idtools.IDMap{ContainerID: 0, HostID: 1, Size: min(uid, maxUID)}) - options.UIDMap = append(options.UIDMap, idtools.IDMap{ContainerID: uid, HostID: 0, Size: 1}) - if maxUID > uid { - options.UIDMap = append(options.UIDMap, idtools.IDMap{ContainerID: uid + 1, HostID: uid + 1, Size: maxUID - uid}) - } + options.UIDMap, options.GIDMap = nil, nil - options.GIDMap = append(options.GIDMap, idtools.IDMap{ContainerID: 0, HostID: 1, Size: min(gid, maxGID)}) - options.GIDMap = append(options.GIDMap, idtools.IDMap{ContainerID: gid, HostID: 0, Size: 1}) - if maxGID > gid { - options.GIDMap = append(options.GIDMap, idtools.IDMap{ContainerID: gid + 1, HostID: gid + 1, Size: maxGID - gid}) - } + options.UIDMap = append(options.UIDMap, idtools.IDMap{ContainerID: 0, HostID: 1, Size: min(uid, maxUID)}) + options.UIDMap = append(options.UIDMap, idtools.IDMap{ContainerID: uid, HostID: 0, Size: 1}) + if maxUID > uid { + options.UIDMap = append(options.UIDMap, idtools.IDMap{ContainerID: uid + 1, HostID: uid + 1, Size: maxUID - uid}) + } - options.HostUIDMapping = false - options.HostGIDMapping = false + options.GIDMap = append(options.GIDMap, idtools.IDMap{ContainerID: 0, HostID: 1, Size: min(gid, maxGID)}) + options.GIDMap = append(options.GIDMap, idtools.IDMap{ContainerID: gid, HostID: 0, Size: 1}) + if maxGID > gid { + options.GIDMap = append(options.GIDMap, idtools.IDMap{ContainerID: gid + 1, HostID: gid + 1, Size: maxGID - gid}) } + + options.HostUIDMapping = false + options.HostGIDMapping = false // Simply ignore the setting and do not setup an inner namespace for root as it is a no-op return &options, nil } diff --git a/pkg/namespaces/namespaces.go b/pkg/namespaces/namespaces.go index a264a5a0f..bdea7c310 100644 --- a/pkg/namespaces/namespaces.go +++ b/pkg/namespaces/namespaces.go @@ -96,6 +96,11 @@ func (n UsernsMode) IsKeepID() bool { return n == "keep-id" } +// IsNoMap indicates whether container uses a mapping where the (uid, gid) on the host is not present in the namespace. +func (n UsernsMode) IsNoMap() bool { + return n == "nomap" +} + // IsAuto indicates whether container uses the "auto" userns mode. func (n UsernsMode) IsAuto() bool { parts := strings.Split(string(n), ":") @@ -158,7 +163,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, "auto": + case "", privateType, hostType, "keep-id", nsType, "auto", "nomap": case containerType: if len(parts) != 2 || parts[1] == "" { return false diff --git a/pkg/specgen/generate/namespaces.go b/pkg/specgen/generate/namespaces.go index 05c2d1741..d8d1ae652 100644 --- a/pkg/specgen/generate/namespaces.go +++ b/pkg/specgen/generate/namespaces.go @@ -165,21 +165,19 @@ func namespaceOptions(ctx context.Context, s *specgen.SpecGenerator, rt *libpod. // User switch s.UserNS.NSMode { case specgen.KeepID: - if rootless.IsRootless() { - toReturn = append(toReturn, libpod.WithAddCurrentUserPasswdEntry()) - - // If user is not overridden, set user in the container - // to user running Podman. - if s.User == "" { - _, uid, gid, err := util.GetKeepIDMapping() - if err != nil { - return nil, err - } - toReturn = append(toReturn, libpod.WithUser(fmt.Sprintf("%d:%d", uid, gid))) + if !rootless.IsRootless() { + return nil, errors.New("keep-id is only supported in rootless mode") + } + toReturn = append(toReturn, libpod.WithAddCurrentUserPasswdEntry()) + + // If user is not overridden, set user in the container + // to user running Podman. + if s.User == "" { + _, uid, gid, err := util.GetKeepIDMapping() + if err != nil { + return nil, err } - } else { - // keep-id as root doesn't need a user namespace - s.UserNS.NSMode = specgen.Host + toReturn = append(toReturn, libpod.WithUser(fmt.Sprintf("%d:%d", uid, gid))) } case specgen.FromPod: if pod == nil || infraCtr == nil { diff --git a/pkg/specgen/namespaces.go b/pkg/specgen/namespaces.go index 4412eff29..eaf2daad9 100644 --- a/pkg/specgen/namespaces.go +++ b/pkg/specgen/namespaces.go @@ -55,6 +55,10 @@ const ( // of the namespace itself. // Only used with the user namespace, invalid otherwise. KeepID NamespaceMode = "keep-id" + // NoMap indicates a user namespace to keep the owner uid out + // of the namespace itself. + // Only used with the user namespace, invalid otherwise. + NoMap NamespaceMode = "no-map" // Auto indicates to automatically create a user namespace. // Only used with the user namespace, invalid otherwise. Auto NamespaceMode = "auto" @@ -121,6 +125,11 @@ func (n *Namespace) IsKeepID() bool { return n.NSMode == KeepID } +// IsNoMap indicates the namespace is NoMap +func (n *Namespace) IsNoMap() bool { + return n.NSMode == NoMap +} + func (n *Namespace) String() string { if n.Value != "" { return fmt.Sprintf("%s:%s", n.NSMode, n.Value) @@ -133,7 +142,7 @@ func validateUserNS(n *Namespace) error { return nil } switch n.NSMode { - case Auto, KeepID: + case Auto, KeepID, NoMap: return nil } return n.validate() @@ -299,6 +308,9 @@ func ParseUserNamespace(ns string) (Namespace, error) { case ns == "keep-id": toReturn.NSMode = KeepID return toReturn, nil + case ns == "nomap": + toReturn.NSMode = NoMap + return toReturn, nil case ns == "": toReturn.NSMode = Host return toReturn, nil @@ -548,20 +560,41 @@ func SetupUserNS(idmappings *storage.IDMappingOptions, userns Namespace, g *gene 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 { + if err := privateUserNamespace(idmappings, g); 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") + case NoMap: + mappings, uid, gid, err := util.GetNoMapMapping() + if err != nil { + return user, err } - for _, uidmap := range idmappings.UIDMap { - g.AddLinuxUIDMapping(uint32(uidmap.HostID), uint32(uidmap.ContainerID), uint32(uidmap.Size)) + idmappings = mappings + g.SetProcessUID(uint32(uid)) + g.SetProcessGID(uint32(gid)) + user = fmt.Sprintf("%d:%d", uid, gid) + if err := privateUserNamespace(idmappings, g); err != nil { + return user, err } - for _, gidmap := range idmappings.GIDMap { - g.AddLinuxGIDMapping(uint32(gidmap.HostID), uint32(gidmap.ContainerID), uint32(gidmap.Size)) + case Private: + if err := privateUserNamespace(idmappings, g); err != nil { + return user, err } } return user, nil } + +func privateUserNamespace(idmappings *storage.IDMappingOptions, g *generate.Generator) error { + if err := g.AddOrReplaceLinuxNamespace(string(spec.UserNamespace), ""); err != nil { + return err + } + if idmappings == nil || (len(idmappings.UIDMap) == 0 && len(idmappings.GIDMap) == 0) { + return 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 nil +} diff --git a/pkg/util/utils.go b/pkg/util/utils.go index b89978601..aed9aebfc 100644 --- a/pkg/util/utils.go +++ b/pkg/util/utils.go @@ -347,55 +347,84 @@ func ParseSignal(rawSignal string) (syscall.Signal, error) { // GetKeepIDMapping returns the mappings and the user to use when keep-id is used func GetKeepIDMapping() (*stypes.IDMappingOptions, int, int, error) { + if !rootless.IsRootless() { + return nil, -1, -1, errors.New("keep-id is only supported in rootless mode") + } options := stypes.IDMappingOptions{ - HostUIDMapping: true, - HostGIDMapping: true, + HostUIDMapping: false, + HostGIDMapping: false, } - uid, gid := 0, 0 - if rootless.IsRootless() { - min := func(a, b int) int { - if a < b { - return a - } - return b + min := func(a, b int) int { + if a < b { + return a } + return b + } - uid = rootless.GetRootlessUID() - gid = rootless.GetRootlessGID() + uid := rootless.GetRootlessUID() + gid := rootless.GetRootlessGID() - uids, gids, err := rootless.GetConfiguredMappings() - if err != nil { - return nil, -1, -1, errors.Wrapf(err, "cannot read mappings") - } - maxUID, maxGID := 0, 0 - for _, u := range uids { - maxUID += u.Size - } - for _, g := range gids { - maxGID += g.Size - } - - options.UIDMap, options.GIDMap = nil, nil + uids, gids, err := rootless.GetConfiguredMappings() + if err != nil { + return nil, -1, -1, errors.Wrapf(err, "cannot read mappings") + } + if len(uids) == 0 || len(gids) == 0 { + return nil, -1, -1, errors.Wrapf(err, "keep-id requires additional UIDs or GIDs defined in /etc/subuid and /etc/subgid to function correctly") + } + maxUID, maxGID := 0, 0 + for _, u := range uids { + maxUID += u.Size + } + for _, g := range gids { + maxGID += g.Size + } - options.UIDMap = append(options.UIDMap, idtools.IDMap{ContainerID: 0, HostID: 1, Size: min(uid, maxUID)}) - options.UIDMap = append(options.UIDMap, idtools.IDMap{ContainerID: uid, HostID: 0, Size: 1}) - if maxUID > uid { - options.UIDMap = append(options.UIDMap, idtools.IDMap{ContainerID: uid + 1, HostID: uid + 1, Size: maxUID - uid}) - } + options.UIDMap, options.GIDMap = nil, nil - options.GIDMap = append(options.GIDMap, idtools.IDMap{ContainerID: 0, HostID: 1, Size: min(gid, maxGID)}) - options.GIDMap = append(options.GIDMap, idtools.IDMap{ContainerID: gid, HostID: 0, Size: 1}) - if maxGID > gid { - options.GIDMap = append(options.GIDMap, idtools.IDMap{ContainerID: gid + 1, HostID: gid + 1, Size: maxGID - gid}) - } + options.UIDMap = append(options.UIDMap, idtools.IDMap{ContainerID: 0, HostID: 1, Size: min(uid, maxUID)}) + options.UIDMap = append(options.UIDMap, idtools.IDMap{ContainerID: uid, HostID: 0, Size: 1}) + if maxUID > uid { + options.UIDMap = append(options.UIDMap, idtools.IDMap{ContainerID: uid + 1, HostID: uid + 1, Size: maxUID - uid}) + } - options.HostUIDMapping = false - options.HostGIDMapping = false + options.GIDMap = append(options.GIDMap, idtools.IDMap{ContainerID: 0, HostID: 1, Size: min(gid, maxGID)}) + options.GIDMap = append(options.GIDMap, idtools.IDMap{ContainerID: gid, HostID: 0, Size: 1}) + if maxGID > gid { + options.GIDMap = append(options.GIDMap, idtools.IDMap{ContainerID: gid + 1, HostID: gid + 1, Size: maxGID - gid}) } - // Simply ignore the setting and do not setup an inner namespace for root as it is a no-op + return &options, uid, gid, nil } +// GetNoMapMapping returns the mappings and the user to use when nomap is used +func GetNoMapMapping() (*stypes.IDMappingOptions, int, int, error) { + if !rootless.IsRootless() { + return nil, -1, -1, errors.New("nomap is only supported in rootless mode") + } + options := stypes.IDMappingOptions{ + HostUIDMapping: false, + HostGIDMapping: false, + } + uids, gids, err := rootless.GetConfiguredMappings() + if err != nil { + return nil, -1, -1, errors.Wrapf(err, "cannot read mappings") + } + if len(uids) == 0 || len(gids) == 0 { + return nil, -1, -1, errors.Wrapf(err, "nomap requires additional UIDs or GIDs defined in /etc/subuid and /etc/subgid to function correctly") + } + options.UIDMap, options.GIDMap = nil, nil + uid, gid := 0, 0 + for _, u := range uids { + options.UIDMap = append(options.UIDMap, idtools.IDMap{ContainerID: uid, HostID: uid + 1, Size: u.Size}) + uid += u.Size + } + for _, g := range gids { + options.GIDMap = append(options.GIDMap, idtools.IDMap{ContainerID: gid, HostID: gid + 1, Size: g.Size}) + gid += g.Size + } + return &options, 0, 0, nil +} + // ParseIDMapping takes idmappings and subuid and subgid maps and returns a storage mapping func ParseIDMapping(mode namespaces.UsernsMode, uidMapSlice, gidMapSlice []string, subUIDMap, subGIDMap string) (*stypes.IDMappingOptions, error) { options := stypes.IDMappingOptions{ @@ -415,7 +444,7 @@ func ParseIDMapping(mode namespaces.UsernsMode, uidMapSlice, gidMapSlice []strin options.AutoUserNsOpts = *opts return &options, nil } - if mode.IsKeepID() { + if mode.IsKeepID() || mode.IsNoMap() { options.HostUIDMapping = false options.HostGIDMapping = false return &options, nil -- cgit v1.2.3-54-g00ecf