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 <dwalsh@redhat.com>
This commit is contained in:
Daniel J Walsh
2022-04-13 14:06:05 -04:00
parent 121dde6234
commit 80c0fceb24
17 changed files with 270 additions and 126 deletions

View File

@ -756,7 +756,7 @@ func AutocompleteNamespace(cmd *cobra.Command, args []string, toComplete string)
// -> same as AutocompleteNamespace with "auto", "keep-id" added // -> same as AutocompleteNamespace with "auto", "keep-id" added
func AutocompleteUserNamespace(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { func AutocompleteUserNamespace(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
results, directive := AutocompleteNamespace(cmd, args, toComplete) results, directive := AutocompleteNamespace(cmd, args, toComplete)
results = append(results, "auto", "keep-id") results = append(results, "auto", "keep-id", "nomap")
return results, directive return results, directive
} }

View File

@ -1225,6 +1225,15 @@ 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 unless an explicit mapping is set with the **--uidmap** and **--gidmap** options. Set the user namespace mode for the container. It defaults to the **PODMAN_USERNS** environment variable. An empty value ("") means user namespaces are disabled unless an explicit mapping is set with the **--uidmap** and **--gidmap** options.
Rootless user --userns=Key mappings:
Key | Host User | Container User
----------|---------------|---------------------
"" |$UID |0 (Default User account mapped to root user in container.)
keep-id |$UID |$UID (Map user account to same UID within container.)
auto |$UID | nil (Host User UID is not mapped into container.)
nomap |$UID | nil (Host User UID is not mapped into container.)
Valid _mode_ values are: Valid _mode_ values are:
**auto**[:_OPTIONS,..._]: automatically create a unique user namespace. **auto**[:_OPTIONS,..._]: automatically create a unique user namespace.
@ -1247,6 +1256,8 @@ Podman allocates unique ranges of UIDs and GIDs from the `containers` subordinat
**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.
**nomap**: creates a user namespace where the current rootless user's UID:GID are not mapped into the container. This option is ignored for containers created by the root user.
**ns:**_namespace_: run the container in the given existing user namespace. **ns:**_namespace_: run the container in the given existing user namespace.
**private**: create a new namespace for the container. **private**: create a new namespace for the container.

View File

@ -308,14 +308,30 @@ several times to map different ranges.
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. 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.
Rootless user --userns=Key mappings:
Key | Host User | Container User
----------|---------------|---------------------
"" |$UID |0 (Default User account mapped to root user in container.)
keep-id |$UID |$UID (Map user account to same UID within container.)
auto |$UID | nil (Host User UID is not mapped into container.)
nomap |$UID | nil (Host User UID is not mapped into container.)
Valid _mode_ values are: Valid _mode_ values are:
- *auto[:*_OPTIONS,..._*]*: automatically create a namespace. It is possible to specify these options to `auto`: - *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. - *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. - *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. - *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. - *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.
- *nomap*: creates a user namespace where the current rootless user's UID:GID are not mapped into the container. This option is ignored for containers created by the root user.
#### **--volume**, **-v**[=*[[SOURCE-VOLUME|HOST-DIR:]CONTAINER-DIR[:OPTIONS]]*] #### **--volume**, **-v**[=*[[SOURCE-VOLUME|HOST-DIR:]CONTAINER-DIR[:OPTIONS]]*]

View File

@ -1290,6 +1290,15 @@ When a user namespace is not in use, the UID and GID used within the container a
Set the user namespace mode for the container. It defaults to the **PODMAN_USERNS** environment variable. An empty value ("") means user namespaces are disabled unless an explicit mapping is set with the **--uidmap** and **--gidmap** options. Set the user namespace mode for the container. It defaults to the **PODMAN_USERNS** environment variable. An empty value ("") means user namespaces are disabled unless an explicit mapping is set with the **--uidmap** and **--gidmap** options.
Rootless user --userns=Key mappings:
Key | Host User | Container User
----------|---------------|---------------------
"" |$UID |0 (Default User account mapped to root user in container.)
keep-id |$UID |$UID (Map user account to same UID within container.)
auto |$UID | nil (Host User UID is not mapped into container.)
nomap |$UID | nil (Host User UID is not mapped into container.)
Valid _mode_ values are: Valid _mode_ values are:
**auto**[:_OPTIONS,..._]: automatically create a unique user namespace. **auto**[:_OPTIONS,..._]: automatically create a unique user namespace.
@ -1299,6 +1308,7 @@ The `--userns=auto` flag, requires that the user name `containers` and a range o
Example: `containers:2147483647:2147483648`. Example: `containers:2147483647:2147483648`.
Podman allocates unique ranges of UIDs and GIDs from the `containers` subordinate user ids. The size of the ranges is based on the number of UIDs required in the image. The number of UIDs and GIDs can be overridden with the `size` option. Podman allocates unique ranges of UIDs and GIDs from the `containers` subordinate user ids. The size of the ranges is based on the number of UIDs required in the image. The number of UIDs and GIDs can be overridden with the `size` option.
The rootless option `--userns=keep-id` uses all the subuids and subgids of the user. Using `--userns=auto` when starting new containers will not work as long as any containers exist that were started with `--userns=keep-id`. The rootless option `--userns=keep-id` uses all the subuids and subgids of the user. Using `--userns=auto` when starting new containers will not work as long as any containers exist that were started with `--userns=keep-id`.
Valid `auto` options: Valid `auto` options:
@ -1313,10 +1323,11 @@ The rootless option `--userns=keep-id` uses all the subuids and subgids of the u
**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.
**nomap**: creates a user namespace where the current rootless user's UID:GID are not mapped into the container. This option is ignored for containers created by the root user.
**ns:**_namespace_: run the container in the given existing user namespace. **ns:**_namespace_: run the container in the given existing user namespace.
**private**: create a new namespace for the container. **private**: create a new namespace for the container.
This option is incompatible with **--gidmap**, **--uidmap**, **--subuidname** and **--subgidname**. This option is incompatible with **--gidmap**, **--uidmap**, **--subuidname** and **--subgidname**.
#### **--uts**=*mode* #### **--uts**=*mode*

View File

@ -276,46 +276,47 @@ func ParseIDMapping(mode namespaces.UsernsMode, uidMapSlice, gidMapSlice []strin
if len(subUIDMap) > 0 || len(subGIDMap) > 0 { if len(subUIDMap) > 0 || len(subGIDMap) > 0 {
return nil, errors.New("cannot specify subuidmap or subgidmap with --userns=keep-id") return nil, errors.New("cannot specify subuidmap or subgidmap with --userns=keep-id")
} }
if rootless.IsRootless() { if !rootless.IsRootless() {
min := func(a, b int) int { return nil, errors.New("keep-id is only supported in rootless mode")
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
}
options.UIDMap, options.GIDMap = nil, nil
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.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
} }
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
}
options.UIDMap, options.GIDMap = nil, nil
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.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 // Simply ignore the setting and do not setup an inner namespace for root as it is a no-op
return &options, nil return &options, nil
} }

View File

@ -96,6 +96,11 @@ func (n UsernsMode) IsKeepID() bool {
return n == "keep-id" 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. // IsAuto indicates whether container uses the "auto" userns mode.
func (n UsernsMode) IsAuto() bool { func (n UsernsMode) IsAuto() bool {
parts := strings.Split(string(n), ":") parts := strings.Split(string(n), ":")
@ -158,7 +163,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, "auto": case "", privateType, hostType, "keep-id", nsType, "auto", "nomap":
case containerType: case containerType:
if len(parts) != 2 || parts[1] == "" { if len(parts) != 2 || parts[1] == "" {
return false return false

View File

@ -165,21 +165,19 @@ func namespaceOptions(ctx context.Context, s *specgen.SpecGenerator, rt *libpod.
// User // User
switch s.UserNS.NSMode { switch s.UserNS.NSMode {
case specgen.KeepID: case specgen.KeepID:
if rootless.IsRootless() { if !rootless.IsRootless() {
toReturn = append(toReturn, libpod.WithAddCurrentUserPasswdEntry()) 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 // If user is not overridden, set user in the container
// to user running Podman. // to user running Podman.
if s.User == "" { if s.User == "" {
_, uid, gid, err := util.GetKeepIDMapping() _, uid, gid, err := util.GetKeepIDMapping()
if err != nil { if err != nil {
return nil, err return nil, err
}
toReturn = append(toReturn, libpod.WithUser(fmt.Sprintf("%d:%d", uid, gid)))
} }
} else { toReturn = append(toReturn, libpod.WithUser(fmt.Sprintf("%d:%d", uid, gid)))
// keep-id as root doesn't need a user namespace
s.UserNS.NSMode = specgen.Host
} }
case specgen.FromPod: case specgen.FromPod:
if pod == nil || infraCtr == nil { if pod == nil || infraCtr == nil {

View File

@ -55,6 +55,10 @@ const (
// of the namespace itself. // of the namespace itself.
// Only used with the user namespace, invalid otherwise. // Only used with the user namespace, invalid otherwise.
KeepID NamespaceMode = "keep-id" 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. // Auto indicates to automatically create a user namespace.
// Only used with the user namespace, invalid otherwise. // Only used with the user namespace, invalid otherwise.
Auto NamespaceMode = "auto" Auto NamespaceMode = "auto"
@ -121,6 +125,11 @@ func (n *Namespace) IsKeepID() bool {
return n.NSMode == KeepID return n.NSMode == KeepID
} }
// IsNoMap indicates the namespace is NoMap
func (n *Namespace) IsNoMap() bool {
return n.NSMode == NoMap
}
func (n *Namespace) String() string { func (n *Namespace) String() string {
if n.Value != "" { if n.Value != "" {
return fmt.Sprintf("%s:%s", n.NSMode, n.Value) return fmt.Sprintf("%s:%s", n.NSMode, n.Value)
@ -133,7 +142,7 @@ func validateUserNS(n *Namespace) error {
return nil return nil
} }
switch n.NSMode { switch n.NSMode {
case Auto, KeepID: case Auto, KeepID, NoMap:
return nil return nil
} }
return n.validate() return n.validate()
@ -299,6 +308,9 @@ func ParseUserNamespace(ns string) (Namespace, error) {
case ns == "keep-id": case ns == "keep-id":
toReturn.NSMode = KeepID toReturn.NSMode = KeepID
return toReturn, nil return toReturn, nil
case ns == "nomap":
toReturn.NSMode = NoMap
return toReturn, nil
case ns == "": case ns == "":
toReturn.NSMode = Host toReturn.NSMode = Host
return toReturn, nil return toReturn, nil
@ -548,20 +560,41 @@ func SetupUserNS(idmappings *storage.IDMappingOptions, userns Namespace, g *gene
g.SetProcessUID(uint32(uid)) g.SetProcessUID(uint32(uid))
g.SetProcessGID(uint32(gid)) g.SetProcessGID(uint32(gid))
user = fmt.Sprintf("%d:%d", uid, gid) user = fmt.Sprintf("%d:%d", uid, gid)
fallthrough if err := privateUserNamespace(idmappings, g); err != nil {
case Private:
if err := g.AddOrReplaceLinuxNamespace(string(spec.UserNamespace), ""); err != nil {
return user, err return user, err
} }
if idmappings == nil || (len(idmappings.UIDMap) == 0 && len(idmappings.GIDMap) == 0) { case NoMap:
return user, errors.Errorf("must provide at least one UID or GID mapping to configure a user namespace") mappings, uid, gid, err := util.GetNoMapMapping()
if err != nil {
return user, err
} }
for _, uidmap := range idmappings.UIDMap { idmappings = mappings
g.AddLinuxUIDMapping(uint32(uidmap.HostID), uint32(uidmap.ContainerID), uint32(uidmap.Size)) 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 { case Private:
g.AddLinuxGIDMapping(uint32(gidmap.HostID), uint32(gidmap.ContainerID), uint32(gidmap.Size)) if err := privateUserNamespace(idmappings, g); err != nil {
return user, err
} }
} }
return user, nil 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
}

View File

@ -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 // GetKeepIDMapping returns the mappings and the user to use when keep-id is used
func GetKeepIDMapping() (*stypes.IDMappingOptions, int, int, error) { 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{ options := stypes.IDMappingOptions{
HostUIDMapping: true, HostUIDMapping: false,
HostGIDMapping: true, HostGIDMapping: false,
} }
uid, gid := 0, 0 min := func(a, b int) int {
if rootless.IsRootless() { if a < b {
min := func(a, b int) int { return a
if a < b {
return a
}
return b
} }
return b
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
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.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
uid := rootless.GetRootlessUID()
gid := rootless.GetRootlessGID()
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, options.GIDMap = nil, nil
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.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})
}
return &options, uid, gid, nil 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 // 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) { func ParseIDMapping(mode namespaces.UsernsMode, uidMapSlice, gidMapSlice []string, subUIDMap, subGIDMap string) (*stypes.IDMappingOptions, error) {
options := stypes.IDMappingOptions{ options := stypes.IDMappingOptions{
@ -415,7 +444,7 @@ func ParseIDMapping(mode namespaces.UsernsMode, uidMapSlice, gidMapSlice []strin
options.AutoUserNsOpts = *opts options.AutoUserNsOpts = *opts
return &options, nil return &options, nil
} }
if mode.IsKeepID() { if mode.IsKeepID() || mode.IsNoMap() {
options.HostUIDMapping = false options.HostUIDMapping = false
options.HostGIDMapping = false options.HostGIDMapping = false
return &options, nil return &options, nil

View File

@ -78,12 +78,18 @@ var _ = Describe("Podman UserNS support", func() {
It("podman --userns=keep-id", func() { It("podman --userns=keep-id", func() {
session := podmanTest.Podman([]string{"run", "--userns=keep-id", "alpine", "id", "-u"}) session := podmanTest.Podman([]string{"run", "--userns=keep-id", "alpine", "id", "-u"})
session.WaitWithDefaultTimeout() session.WaitWithDefaultTimeout()
if os.Geteuid() == 0 {
Expect(session).Should(Exit(125))
return
}
Expect(session).Should(Exit(0)) Expect(session).Should(Exit(0))
uid := fmt.Sprintf("%d", os.Geteuid()) uid := fmt.Sprintf("%d", os.Geteuid())
Expect(session.OutputToString()).To(ContainSubstring(uid)) Expect(session.OutputToString()).To(ContainSubstring(uid))
}) })
It("podman --userns=keep-id check passwd", func() { It("podman --userns=keep-id check passwd", func() {
SkipIfNotRootless("keep-id only works in rootless mode")
session := podmanTest.Podman([]string{"run", "--userns=keep-id", "alpine", "id", "-un"}) session := podmanTest.Podman([]string{"run", "--userns=keep-id", "alpine", "id", "-un"})
session.WaitWithDefaultTimeout() session.WaitWithDefaultTimeout()
Expect(session).Should(Exit(0)) Expect(session).Should(Exit(0))
@ -93,6 +99,7 @@ var _ = Describe("Podman UserNS support", func() {
}) })
It("podman --userns=keep-id root owns /usr", func() { It("podman --userns=keep-id root owns /usr", func() {
SkipIfNotRootless("keep-id only works in rootless mode")
session := podmanTest.Podman([]string{"run", "--userns=keep-id", "alpine", "stat", "-c%u", "/usr"}) session := podmanTest.Podman([]string{"run", "--userns=keep-id", "alpine", "stat", "-c%u", "/usr"})
session.WaitWithDefaultTimeout() session.WaitWithDefaultTimeout()
Expect(session).Should(Exit(0)) Expect(session).Should(Exit(0))
@ -100,6 +107,7 @@ var _ = Describe("Podman UserNS support", func() {
}) })
It("podman --userns=keep-id --user root:root", func() { It("podman --userns=keep-id --user root:root", func() {
SkipIfNotRootless("keep-id only works in rootless mode")
session := podmanTest.Podman([]string{"run", "--userns=keep-id", "--user", "root:root", "alpine", "id", "-u"}) session := podmanTest.Podman([]string{"run", "--userns=keep-id", "--user", "root:root", "alpine", "id", "-u"})
session.WaitWithDefaultTimeout() session.WaitWithDefaultTimeout()
Expect(session).Should(Exit(0)) Expect(session).Should(Exit(0))
@ -107,10 +115,7 @@ var _ = Describe("Podman UserNS support", func() {
}) })
It("podman run --userns=keep-id can add users", func() { It("podman run --userns=keep-id can add users", func() {
if os.Geteuid() == 0 { SkipIfNotRootless("keep-id only works in rootless mode")
Skip("Test only runs without root")
}
userName := os.Getenv("USER") userName := os.Getenv("USER")
if userName == "" { if userName == "" {
Skip("Can't complete test if no username available") Skip("Can't complete test if no username available")

View File

@ -160,6 +160,7 @@ var _ = Describe("Toolbox-specific testing", func() {
}) })
It("podman create --userns=keep-id --user root:root - entrypoint - entrypoint is executed as root", func() { It("podman create --userns=keep-id --user root:root - entrypoint - entrypoint is executed as root", func() {
SkipIfNotRootless("only meaningful when run rootless")
session := podmanTest.Podman([]string{"run", "--userns=keep-id", "--user", "root:root", ALPINE, session := podmanTest.Podman([]string{"run", "--userns=keep-id", "--user", "root:root", ALPINE,
"id"}) "id"})
session.WaitWithDefaultTimeout() session.WaitWithDefaultTimeout()
@ -168,6 +169,7 @@ var _ = Describe("Toolbox-specific testing", func() {
}) })
It("podman create --userns=keep-id + podman exec - correct names of user and group", func() { It("podman create --userns=keep-id + podman exec - correct names of user and group", func() {
SkipIfNotRootless("only meaningful when run rootless")
var session *PodmanSessionIntegration var session *PodmanSessionIntegration
var err error var err error
@ -199,6 +201,7 @@ var _ = Describe("Toolbox-specific testing", func() {
}) })
It("podman create --userns=keep-id - entrypoint - adding user with useradd and then removing their password", func() { It("podman create --userns=keep-id - entrypoint - adding user with useradd and then removing their password", func() {
SkipIfNotRootless("only meaningful when run rootless")
var session *PodmanSessionIntegration var session *PodmanSessionIntegration
var username string = "testuser" var username string = "testuser"
@ -238,6 +241,7 @@ var _ = Describe("Toolbox-specific testing", func() {
}) })
It("podman create --userns=keep-id + podman exec - adding group with groupadd", func() { It("podman create --userns=keep-id + podman exec - adding group with groupadd", func() {
SkipIfNotRootless("only meaningful when run rootless")
var session *PodmanSessionIntegration var session *PodmanSessionIntegration
var groupName string = "testgroup" var groupName string = "testgroup"
@ -268,6 +272,7 @@ var _ = Describe("Toolbox-specific testing", func() {
}) })
It("podman create --userns=keep-id - entrypoint - modifying existing user with usermod - add to new group, change home/shell/uid", func() { It("podman create --userns=keep-id - entrypoint - modifying existing user with usermod - add to new group, change home/shell/uid", func() {
SkipIfNotRootless("only meaningful when run rootless")
var session *PodmanSessionIntegration var session *PodmanSessionIntegration
var badHomeDir string = "/home/badtestuser" var badHomeDir string = "/home/badtestuser"
var badShell string = "/bin/sh" var badShell string = "/bin/sh"
@ -315,6 +320,7 @@ var _ = Describe("Toolbox-specific testing", func() {
}) })
It("podman run --privileged --userns=keep-id --user root:root - entrypoint - (bind)mounting", func() { It("podman run --privileged --userns=keep-id --user root:root - entrypoint - (bind)mounting", func() {
SkipIfNotRootless("only meaningful when run rootless")
var session *PodmanSessionIntegration var session *PodmanSessionIntegration
session = podmanTest.Podman([]string{"run", "--privileged", "--userns=keep-id", "--user", "root:root", ALPINE, session = podmanTest.Podman([]string{"run", "--privileged", "--userns=keep-id", "--user", "root:root", ALPINE,
@ -329,6 +335,7 @@ var _ = Describe("Toolbox-specific testing", func() {
}) })
It("podman create + start - with all needed switches for create - sleep as entry-point", func() { It("podman create + start - with all needed switches for create - sleep as entry-point", func() {
SkipIfNotRootless("only meaningful when run rootless")
var session *PodmanSessionIntegration var session *PodmanSessionIntegration
// These should be most of the switches that Toolbox uses to create a "toolbox" container // These should be most of the switches that Toolbox uses to create a "toolbox" container
@ -365,8 +372,8 @@ var _ = Describe("Toolbox-specific testing", func() {
}) })
It("podman run --userns=keep-id check $HOME", func() { It("podman run --userns=keep-id check $HOME", func() {
SkipIfNotRootless("only meaningful when run rootless")
var session *PodmanSessionIntegration var session *PodmanSessionIntegration
currentUser, err := user.Current() currentUser, err := user.Current()
Expect(err).To(BeNil()) Expect(err).To(BeNil())

View File

@ -273,9 +273,11 @@ echo $rand | 0 | $rand
# symptom only manifests on a fedora container image -- we have no # symptom only manifests on a fedora container image -- we have no
# reproducer on alpine. Checking directory ownership is good enough. # reproducer on alpine. Checking directory ownership is good enough.
@test "podman run : user namespace preserved root ownership" { @test "podman run : user namespace preserved root ownership" {
keep="--userns=keep-id"
is_rootless || keep=""
for priv in "" "--privileged"; do for priv in "" "--privileged"; do
for user in "--user=0" "--user=100"; do for user in "--user=0" "--user=100"; do
for keepid in "" "--userns=keep-id"; do for keepid in "" ${keep}; do
opts="$priv $user $keepid" opts="$priv $user $keepid"
for dir in /etc /usr;do for dir in /etc /usr;do
@ -290,6 +292,7 @@ echo $rand | 0 | $rand
# #6829 : add username to /etc/passwd inside container if --userns=keep-id # #6829 : add username to /etc/passwd inside container if --userns=keep-id
@test "podman run : add username to /etc/passwd if --userns=keep-id" { @test "podman run : add username to /etc/passwd if --userns=keep-id" {
skip_if_not_rootless "--userns=keep-id only works in rootless mode"
# Default: always run as root # Default: always run as root
run_podman run --rm $IMAGE id -un run_podman run --rm $IMAGE id -un
is "$output" "root" "id -un on regular container" is "$output" "root" "id -un on regular container"
@ -340,6 +343,7 @@ echo $rand | 0 | $rand
# #6991 : /etc/passwd is modifiable # #6991 : /etc/passwd is modifiable
@test "podman run : --userns=keep-id: passwd file is modifiable" { @test "podman run : --userns=keep-id: passwd file is modifiable" {
skip_if_not_rootless "--userns=keep-id only works in rootless mode"
run_podman run -d --userns=keep-id --cap-add=dac_override $IMAGE sh -c 'while ! test -e /tmp/stop; do sleep 0.1; done' run_podman run -d --userns=keep-id --cap-add=dac_override $IMAGE sh -c 'while ! test -e /tmp/stop; do sleep 0.1; done'
cid="$output" cid="$output"
@ -824,6 +828,9 @@ EOF
# CVE-2022-1227 : podman top joins container mount NS and uses nsenter from image # CVE-2022-1227 : podman top joins container mount NS and uses nsenter from image
@test "podman top does not use nsenter from image" { @test "podman top does not use nsenter from image" {
keepid="--userns=keep-id"
is_rootless || keepid=""
tmpdir=$PODMAN_TMPDIR/build-test tmpdir=$PODMAN_TMPDIR/build-test
mkdir -p $tmpdir mkdir -p $tmpdir
tmpbuilddir=$tmpdir/build tmpbuilddir=$tmpdir/build
@ -838,7 +845,7 @@ EOF
test_image="cve_2022_1227_test" test_image="cve_2022_1227_test"
run_podman build -t $test_image $tmpbuilddir run_podman build -t $test_image $tmpbuilddir
run_podman run -d --userns=keep-id $test_image top run_podman run -d ${keepid} $test_image top
ctr="$output" ctr="$output"
run_podman top $ctr huser,user run_podman top $ctr huser,user
run_podman rm -f -t0 $ctr run_podman rm -f -t0 $ctr

View File

@ -119,7 +119,9 @@ load helpers
echo "content" > $srcdir/hostfile echo "content" > $srcdir/hostfile
userid=$(id -u) userid=$(id -u)
run_podman run --user=$userid --userns=keep-id -d --name cpcontainer $IMAGE sleep infinity keepid="--userns=keep-id"
is_rootless || keepid=""
run_podman run --user=$userid ${keepid} -d --name cpcontainer $IMAGE sleep infinity
run_podman cp $srcdir/hostfile cpcontainer:/tmp/hostfile run_podman cp $srcdir/hostfile cpcontainer:/tmp/hostfile
run_podman exec cpcontainer stat -c "%u" /tmp/hostfile run_podman exec cpcontainer stat -c "%u" /tmp/hostfile
is "$output" "$userid" "copied file is chowned to the container user" is "$output" "$userid" "copied file is chowned to the container user"
@ -138,7 +140,9 @@ load helpers
userid=$(id -u) userid=$(id -u)
run_podman run --user="$userid" --userns=keep-id -d --name cpcontainer $IMAGE sleep infinity keepid="--userns=keep-id"
is_rootless || keepid=""
run_podman run --user=$userid ${keepid} -d --name cpcontainer $IMAGE sleep infinity
run_podman cp -a=false - cpcontainer:/tmp/ < "${tmpdir}/a.tar" run_podman cp -a=false - cpcontainer:/tmp/ < "${tmpdir}/a.tar"
run_podman exec cpcontainer stat -c "%u:%g" /tmp/a.txt run_podman exec cpcontainer stat -c "%u:%g" /tmp/a.txt
is "$output" "1042:1043" "copied file retains uid/gid from the tar" is "$output" "1042:1043" "copied file retains uid/gid from the tar"

View File

@ -87,6 +87,7 @@ load helpers
# #6829 : add username to /etc/passwd inside container if --userns=keep-id # #6829 : add username to /etc/passwd inside container if --userns=keep-id
@test "podman exec - with keep-id" { @test "podman exec - with keep-id" {
skip_if_not_rootless "--userns=keep-id only works in rootless mode"
# Multiple --userns options confirm command-line override (last one wins) # Multiple --userns options confirm command-line override (last one wins)
run_podman run -d --userns=private --userns=keep-id $IMAGE sh -c \ run_podman run -d --userns=private --userns=keep-id $IMAGE sh -c \
"echo READY;while [ ! -f /tmp/stop ]; do sleep 1; done" "echo READY;while [ ! -f /tmp/stop ]; do sleep 1; done"

View File

@ -182,13 +182,14 @@ EOF
run_podman volume rm $myvol run_podman volume rm $myvol
# Autocreated volumes should also work with keep-id if is_rootless; then
# All we do here is check status; podman 1.9.1 would fail with EPERM # Autocreated volumes should also work with keep-id
myvol=myvol$(random_string) # All we do here is check status; podman 1.9.1 would fail with EPERM
run_podman run --rm -v $myvol:/myvol:z --userns=keep-id $IMAGE \ myvol=myvol$(random_string)
run_podman run --rm -v $myvol:/myvol:z --userns=keep-id $IMAGE \
touch /myvol/myfile touch /myvol/myfile
run_podman volume rm $myvol
run_podman volume rm $myvol fi
} }

View File

@ -94,3 +94,17 @@ EOF
is ${output} ${secret_content} "Secrets should work with user namespace" is ${output} ${secret_content} "Secrets should work with user namespace"
run_podman secret rm ${test_name} run_podman secret rm ${test_name}
} }
@test "podman userns=nomap" {
skip_if_not_rootless "--userns=nomap only works in rootless mode"
ns_user=$(id -un)
baseuid=$(egrep "${ns_user}:" /etc/subuid | cut -f2 -d:)
test ! -z ${baseuid} || skip "no IDs allocated for user ${ns_user}"
test_name="test_$(random_string 12)"
run_podman run -d --userns=nomap $IMAGE sleep 100
cid=${output}
run_podman top ${cid} huser
is "${output}" "HUSER.*${baseuid}" "Container should start with baseuid from /etc/subuid not user UID"
run_podman rm -t 0 --force ${cid}
}

View File

@ -88,6 +88,7 @@ load helpers
# Issue #5466 - port-forwarding doesn't work with this option and -d # Issue #5466 - port-forwarding doesn't work with this option and -d
@test "podman networking: port with --userns=keep-id" { @test "podman networking: port with --userns=keep-id" {
skip_if_not_rootless "--userns=keep-id only works in rootless mode"
for cidr in "" "$(random_rfc1918_subnet).0/24"; do for cidr in "" "$(random_rfc1918_subnet).0/24"; do
myport=$(random_free_port 52000-52999) myport=$(random_free_port 52000-52999)
if [[ -z $cidr ]]; then if [[ -z $cidr ]]; then