summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/source/markdown/podman-create.1.md9
-rw-r--r--docs/source/markdown/podman-run.1.md11
-rw-r--r--go.mod2
-rw-r--r--go.sum4
-rw-r--r--libpod/container.go2
-rw-r--r--libpod/container_internal_linux.go36
-rw-r--r--libpod/options.go5
-rw-r--r--pkg/specgen/generate/container_create.go5
-rw-r--r--pkg/specgen/volumes.go13
-rw-r--r--pkg/util/mountOpts.go7
-rw-r--r--pkg/util/utils.go14
-rw-r--r--test/e2e/run_volume_test.go53
-rw-r--r--vendor/github.com/containers/common/pkg/chown/chown.go122
-rw-r--r--vendor/modules.txt3
14 files changed, 274 insertions, 12 deletions
diff --git a/docs/source/markdown/podman-create.1.md b/docs/source/markdown/podman-create.1.md
index 7782949a9..30cadf703 100644
--- a/docs/source/markdown/podman-create.1.md
+++ b/docs/source/markdown/podman-create.1.md
@@ -1036,6 +1036,7 @@ The _options_ is a comma delimited list and can be:
* [**no**]**dev**
* [**no**]**suid**
* [**O**]
+* [**U**]
The `CONTAINER-DIR` must be an absolute path such as `/src/docs`. The volume
will be mounted into the container at this directory.
@@ -1065,6 +1066,14 @@ You can add `:ro` or `:rw` suffix to a volume to mount it read-only or
read-write mode, respectively. By default, the volumes are mounted read-write.
See examples.
+ `Chowning Volume Mounts`
+
+By default, Podman does not change the owner and group of source volume directories mounted into containers. If a container is created in a new user namespace, the UID and GID in the container may correspond to another UID and GID on the host.
+
+The `:U` suffix tells Podman to use the correct host UID and GID based on the UID and GID within the container, to change recursively the owner and group of the source volume.
+
+**Warning** use with caution since this will modify the host filesystem.
+
`Labeling Volume Mounts`
Labeling systems like SELinux require that proper labels are placed on volume
diff --git a/docs/source/markdown/podman-run.1.md b/docs/source/markdown/podman-run.1.md
index 8c0b12e90..a633df94e 100644
--- a/docs/source/markdown/podman-run.1.md
+++ b/docs/source/markdown/podman-run.1.md
@@ -1111,6 +1111,7 @@ The _options_ is a comma delimited list and can be: <sup>[[1]](#Footnote1)</sup>
* [**no**]**dev**
* [**no**]**suid**
* [**O**]
+* [**U**]
The `CONTAINER-DIR` must be an absolute path such as `/src/docs`. The volume
will be mounted into the container at this directory.
@@ -1139,6 +1140,14 @@ container.
You can add **:ro** or **:rw** option to mount a volume in read-only or
read-write mode, respectively. By default, the volumes are mounted read-write.
+ `Chowning Volume Mounts`
+
+By default, Podman does not change the owner and group of source volume directories mounted into containers. If a container is created in a new user namespace, the UID and GID in the container may correspond to another UID and GID on the host.
+
+The `:U` suffix tells Podman to use the correct host UID and GID based on the UID and GID within the container, to change recursively the owner and group of the source volume.
+
+**Warning** use with caution since this will modify the host filesystem.
+
`Labeling Volume Mounts`
Labeling systems like SELinux require that proper labels are placed on volume
@@ -1450,6 +1459,8 @@ $ podman run -v /var/db:/data1 -i -t fedora bash
$ podman run -v data:/data2 -i -t fedora bash
$ podman run -v /var/cache/dnf:/var/cache/dnf:O -ti fedora dnf -y update
+
+$ podman run -d -e MYSQL_ROOT_PASSWORD=root --user mysql --userns=keep-id -v ~/data:/var/lib/mysql:z,U mariadb
```
Using **--mount** flags to mount a host directory as a container folder, specify
diff --git a/go.mod b/go.mod
index 83f82fc1a..8a7f8f4ba 100644
--- a/go.mod
+++ b/go.mod
@@ -68,6 +68,6 @@ require (
google.golang.org/appengine v1.6.6 // indirect
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 // indirect
k8s.io/api v0.0.0-20190620084959-7cf5895f2711
- k8s.io/apimachinery v0.20.3
+ k8s.io/apimachinery v0.20.4
k8s.io/client-go v0.0.0-20190620085101-78d2af792bab
)
diff --git a/go.sum b/go.sum
index 92c52686b..ebe85afe1 100644
--- a/go.sum
+++ b/go.sum
@@ -907,8 +907,8 @@ honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt
k8s.io/api v0.0.0-20190620084959-7cf5895f2711 h1:BblVYz/wE5WtBsD/Gvu54KyBUTJMflolzc5I2DTvh50=
k8s.io/api v0.0.0-20190620084959-7cf5895f2711/go.mod h1:TBhBqb1AWbBQbW3XRusr7n7E4v2+5ZY8r8sAMnyFC5A=
k8s.io/apimachinery v0.0.0-20190612205821-1799e75a0719/go.mod h1:I4A+glKBHiTgiEjQiCCQfCAIcIMFGt291SmsvcrFzJA=
-k8s.io/apimachinery v0.20.3 h1:P0heYNTI2km9gTUAb0PX5qRd8oHAaesICvkg13k97y4=
-k8s.io/apimachinery v0.20.3/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU=
+k8s.io/apimachinery v0.20.4 h1:vhxQ0PPUUU2Ns1b9r4/UFp13UPs8cw2iOoTjnY9faa0=
+k8s.io/apimachinery v0.20.4/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU=
k8s.io/client-go v0.0.0-20190620085101-78d2af792bab h1:E8Fecph0qbNsAbijJJQryKu4Oi9QTp5cVpjTE+nqg6g=
k8s.io/client-go v0.0.0-20190620085101-78d2af792bab/go.mod h1:E95RaSlHr79aHaX0aGSwcPNfygDiPKOVXdmivCIZT0k=
k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
diff --git a/libpod/container.go b/libpod/container.go
index 9841bddf7..ee6e243ac 100644
--- a/libpod/container.go
+++ b/libpod/container.go
@@ -236,6 +236,8 @@ type ContainerOverlayVolume struct {
Dest string `json:"dest"`
// Source specifies the source path of the mount.
Source string `json:"source,omitempty"`
+ // Options holds overlay volume options.
+ Options []string `json:"options,omitempty"`
}
// ContainerImageVolume is a volume based on a container image. The container
diff --git a/libpod/container_internal_linux.go b/libpod/container_internal_linux.go
index 43a345ea9..9c3e91a97 100644
--- a/libpod/container_internal_linux.go
+++ b/libpod/container_internal_linux.go
@@ -23,7 +23,9 @@ import (
"github.com/containernetworking/plugins/pkg/ns"
"github.com/containers/buildah/pkg/chrootuser"
"github.com/containers/buildah/pkg/overlay"
+ butil "github.com/containers/buildah/util"
"github.com/containers/common/pkg/apparmor"
+ "github.com/containers/common/pkg/chown"
"github.com/containers/common/pkg/config"
"github.com/containers/common/pkg/subscriptions"
"github.com/containers/common/pkg/umask"
@@ -356,13 +358,28 @@ func (c *Container) generateSpec(ctx context.Context) (*spec.Spec, error) {
return nil, err
}
- // Check if the spec file mounts contain the label Relabel flags z or Z.
- // If they do, relabel the source directory and then remove the option.
+ // Get host UID and GID based on the container process UID and GID.
+ hostUID, hostGID, err := butil.GetHostIDs(util.IDtoolsToRuntimeSpec(c.config.IDMappings.UIDMap), util.IDtoolsToRuntimeSpec(c.config.IDMappings.GIDMap), uint32(execUser.Uid), uint32(execUser.Gid))
+ if err != nil {
+ return nil, err
+ }
+
+ // Check if the spec file mounts contain the options z, Z or U.
+ // If they have z or Z, relabel the source directory and then remove the option.
+ // If they have U, chown the source directory and them remove the option.
for i := range g.Config.Mounts {
m := &g.Config.Mounts[i]
var options []string
for _, o := range m.Options {
switch o {
+ case "U":
+ if m.Type == "tmpfs" {
+ options = append(options, []string{fmt.Sprintf("uid=%d", execUser.Uid), fmt.Sprintf("gid=%d", execUser.Gid)}...)
+ } else {
+ if err := chown.ChangeHostPathOwnership(m.Source, true, int(hostUID), int(hostGID)); err != nil {
+ return nil, err
+ }
+ }
case "z":
fallthrough
case "Z":
@@ -427,6 +444,21 @@ func (c *Container) generateSpec(ctx context.Context) (*spec.Spec, error) {
if err != nil {
return nil, errors.Wrapf(err, "mounting overlay failed %q", overlayVol.Source)
}
+
+ // Check overlay volume options
+ for _, o := range overlayVol.Options {
+ switch o {
+ case "U":
+ if err := chown.ChangeHostPathOwnership(overlayVol.Source, true, int(hostUID), int(hostGID)); err != nil {
+ return nil, err
+ }
+
+ if err := chown.ChangeHostPathOwnership(contentDir, true, int(hostUID), int(hostGID)); err != nil {
+ return nil, err
+ }
+ }
+ }
+
g.AddMount(overlayMount)
}
diff --git a/libpod/options.go b/libpod/options.go
index 627ea8c57..6344e1acc 100644
--- a/libpod/options.go
+++ b/libpod/options.go
@@ -1429,8 +1429,9 @@ func WithOverlayVolumes(volumes []*ContainerOverlayVolume) CtrCreateOption {
for _, vol := range volumes {
ctr.config.OverlayVolumes = append(ctr.config.OverlayVolumes, &ContainerOverlayVolume{
- Dest: vol.Dest,
- Source: vol.Source,
+ Dest: vol.Dest,
+ Source: vol.Source,
+ Options: vol.Options,
})
}
diff --git a/pkg/specgen/generate/container_create.go b/pkg/specgen/generate/container_create.go
index 3b7112959..03697b353 100644
--- a/pkg/specgen/generate/container_create.go
+++ b/pkg/specgen/generate/container_create.go
@@ -247,8 +247,9 @@ func createContainerOptions(ctx context.Context, rt *libpod.Runtime, s *specgen.
var vols []*libpod.ContainerOverlayVolume
for _, v := range overlays {
vols = append(vols, &libpod.ContainerOverlayVolume{
- Dest: v.Destination,
- Source: v.Source,
+ Dest: v.Destination,
+ Source: v.Source,
+ Options: v.Options,
})
}
options = append(options, libpod.WithOverlayVolumes(vols))
diff --git a/pkg/specgen/volumes.go b/pkg/specgen/volumes.go
index 83634b4ef..d85d2bdd1 100644
--- a/pkg/specgen/volumes.go
+++ b/pkg/specgen/volumes.go
@@ -31,6 +31,8 @@ type OverlayVolume struct {
Destination string `json:"destination"`
// Source specifies the source path of the mount.
Source string `json:"source,omitempty"`
+ // Options holds overlay volume options.
+ Options []string `json:"options,omitempty"`
}
// ImageVolume is a volume based on a container image. The container image is
@@ -100,10 +102,17 @@ func GenVolumeMounts(volumeFlag []string) (map[string]spec.Mount, map[string]*Na
if strings.HasPrefix(src, "/") || strings.HasPrefix(src, ".") {
// This is not a named volume
overlayFlag := false
+ chownFlag := false
for _, o := range options {
if o == "O" {
overlayFlag = true
- if len(options) > 1 {
+
+ joinedOpts := strings.Join(options, "")
+ if strings.Contains(joinedOpts, "U") {
+ chownFlag = true
+ }
+
+ if len(options) > 2 || (len(options) == 2 && !chownFlag) {
return nil, nil, nil, errors.New("can't use 'O' with other options")
}
}
@@ -113,6 +122,8 @@ func GenVolumeMounts(volumeFlag []string) (map[string]spec.Mount, map[string]*Na
newOverlayVol := new(OverlayVolume)
newOverlayVol.Destination = cleanDest
newOverlayVol.Source = src
+ newOverlayVol.Options = options
+
if _, ok := overlayVolumes[newOverlayVol.Destination]; ok {
return nil, nil, nil, errors.Wrapf(errDuplicateDest, newOverlayVol.Destination)
}
diff --git a/pkg/util/mountOpts.go b/pkg/util/mountOpts.go
index b3a38f286..f13dc94ec 100644
--- a/pkg/util/mountOpts.go
+++ b/pkg/util/mountOpts.go
@@ -25,7 +25,7 @@ type defaultMountOptions struct {
// The sourcePath variable, if not empty, contains a bind mount source.
func ProcessOptions(options []string, isTmpfs bool, sourcePath string) ([]string, error) {
var (
- foundWrite, foundSize, foundProp, foundMode, foundExec, foundSuid, foundDev, foundCopyUp, foundBind, foundZ bool
+ foundWrite, foundSize, foundProp, foundMode, foundExec, foundSuid, foundDev, foundCopyUp, foundBind, foundZ, foundU bool
)
newOptions := make([]string, 0, len(options))
@@ -116,6 +116,11 @@ func ProcessOptions(options []string, isTmpfs bool, sourcePath string) ([]string
return nil, errors.Wrapf(ErrDupeMntOption, "only one of 'z' and 'Z' can be used")
}
foundZ = true
+ case "U":
+ if foundU {
+ return nil, errors.Wrapf(ErrDupeMntOption, "the 'U' option can only be set once")
+ }
+ foundU = true
default:
return nil, errors.Wrapf(ErrBadMntOption, "unknown mount option %q", opt)
}
diff --git a/pkg/util/utils.go b/pkg/util/utils.go
index c6ecd91f6..a4c8f3a64 100644
--- a/pkg/util/utils.go
+++ b/pkg/util/utils.go
@@ -23,6 +23,7 @@ import (
"github.com/containers/storage"
"github.com/containers/storage/pkg/idtools"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
+ "github.com/opencontainers/runtime-spec/specs-go"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"golang.org/x/crypto/ssh/terminal"
@@ -692,3 +693,16 @@ func CoresToPeriodAndQuota(cores float64) (uint64, int64) {
func PeriodAndQuotaToCores(period uint64, quota int64) float64 {
return float64(quota) / float64(period)
}
+
+// IDtoolsToRuntimeSpec converts idtools ID mapping to the one of the runtime spec.
+func IDtoolsToRuntimeSpec(idMaps []idtools.IDMap) (convertedIDMap []specs.LinuxIDMapping) {
+ for _, idmap := range idMaps {
+ tempIDMap := specs.LinuxIDMapping{
+ ContainerID: uint32(idmap.ContainerID),
+ HostID: uint32(idmap.HostID),
+ Size: uint32(idmap.Size),
+ }
+ convertedIDMap = append(convertedIDMap, tempIDMap)
+ }
+ return convertedIDMap
+}
diff --git a/test/e2e/run_volume_test.go b/test/e2e/run_volume_test.go
index 20c43bf4a..454dfdc83 100644
--- a/test/e2e/run_volume_test.go
+++ b/test/e2e/run_volume_test.go
@@ -2,8 +2,10 @@ package integration
import (
"fmt"
+ "io/ioutil"
"os"
"os/exec"
+ "os/user"
"path/filepath"
"strings"
@@ -590,4 +592,55 @@ VOLUME /test/`
Expect(session.ExitCode()).To(Equal(0))
Expect(len(session.OutputToStringArray())).To(Equal(2))
})
+
+ It("podman run with U volume flag", func() {
+ SkipIfRemote("Overlay volumes only work locally")
+
+ u, err := user.Current()
+ Expect(err).To(BeNil())
+ name := u.Username
+ 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")
+ }
+
+ if os.Getenv("container") != "" {
+ Skip("Overlay mounts not supported when running in a container")
+ }
+ if rootless.IsRootless() {
+ if _, err := exec.LookPath("fuse_overlay"); err != nil {
+ Skip("Fuse-Overlayfs required for rootless overlay mount test")
+ }
+ }
+
+ mountPath := filepath.Join(podmanTest.TempDir, "secrets")
+ os.Mkdir(mountPath, 0755)
+ vol := mountPath + ":" + dest + ":U"
+
+ session := podmanTest.Podman([]string{"run", "--rm", "--user", "888:888", "-v", vol, ALPINE, "stat", "-c", "%u:%g", dest})
+ session.WaitWithDefaultTimeout()
+ Expect(session.ExitCode()).To(Equal(0))
+ found, _ := session.GrepString("888:888")
+ Expect(found).Should(BeTrue())
+
+ session = podmanTest.Podman([]string{"run", "--rm", "--user", "888:888", "--userns", "auto", "-v", vol, ALPINE, "stat", "-c", "%u:%g", dest})
+ session.WaitWithDefaultTimeout()
+ Expect(session.ExitCode()).To(Equal(0))
+ found, _ = session.GrepString("888:888")
+ Expect(found).Should(BeTrue())
+
+ vol = vol + ",O"
+ session = podmanTest.Podman([]string{"run", "--rm", "--user", "888:888", "--userns", "keep-id", "-v", vol, ALPINE, "stat", "-c", "%u:%g", dest})
+ session.WaitWithDefaultTimeout()
+ Expect(session.ExitCode()).To(Equal(0))
+ found, _ = session.GrepString("888:888")
+ Expect(found).Should(BeTrue())
+ })
})
diff --git a/vendor/github.com/containers/common/pkg/chown/chown.go b/vendor/github.com/containers/common/pkg/chown/chown.go
new file mode 100644
index 000000000..fe794304e
--- /dev/null
+++ b/vendor/github.com/containers/common/pkg/chown/chown.go
@@ -0,0 +1,122 @@
+package chown
+
+import (
+ "os"
+ "os/user"
+ "path/filepath"
+ "syscall"
+
+ "github.com/containers/storage/pkg/homedir"
+ "github.com/pkg/errors"
+)
+
+// DangerousHostPath validates if a host path is dangerous and should not be modified
+func DangerousHostPath(path string) (bool, error) {
+ excludePaths := map[string]bool{
+ "/": true,
+ "/bin": true,
+ "/boot": true,
+ "/dev": true,
+ "/etc": true,
+ "/etc/passwd": true,
+ "/etc/pki": true,
+ "/etc/shadow": true,
+ "/home": true,
+ "/lib": true,
+ "/lib64": true,
+ "/media": true,
+ "/opt": true,
+ "/proc": true,
+ "/root": true,
+ "/run": true,
+ "/sbin": true,
+ "/srv": true,
+ "/sys": true,
+ "/tmp": true,
+ "/usr": true,
+ "/var": true,
+ "/var/lib": true,
+ "/var/log": true,
+ }
+
+ if home := homedir.Get(); home != "" {
+ excludePaths[home] = true
+ }
+
+ if sudoUser := os.Getenv("SUDO_USER"); sudoUser != "" {
+ if usr, err := user.Lookup(sudoUser); err == nil {
+ excludePaths[usr.HomeDir] = true
+ }
+ }
+
+ absPath, err := filepath.Abs(path)
+ if err != nil {
+ return true, err
+ }
+
+ realPath, err := filepath.EvalSymlinks(absPath)
+ if err != nil {
+ return true, err
+ }
+
+ if excludePaths[realPath] {
+ return true, nil
+ }
+
+ return false, nil
+}
+
+// ChangeHostPathOwnership changes the uid and gid ownership of a directory or file within the host.
+// This is used by the volume U flag to change source volumes ownership
+func ChangeHostPathOwnership(path string, recursive bool, uid, gid int) error {
+ // Validate if host path can be chowned
+ isDangerous, err := DangerousHostPath(path)
+ if err != nil {
+ return errors.Wrapf(err, "failed to validate if host path is dangerous")
+ }
+
+ if isDangerous {
+ return errors.Errorf("chowning host path %q is not allowed. You can manually `chown -R %d:%d %s`", path, uid, gid, path)
+ }
+
+ // Chown host path
+ if recursive {
+ err := filepath.Walk(path, func(filePath string, f os.FileInfo, err error) error {
+ if err != nil {
+ return err
+ }
+
+ // Get current ownership
+ currentUID := int(f.Sys().(*syscall.Stat_t).Uid)
+ currentGID := int(f.Sys().(*syscall.Stat_t).Gid)
+
+ if uid != currentUID || gid != currentGID {
+ return os.Lchown(filePath, uid, gid)
+ }
+
+ return nil
+ })
+
+ if err != nil {
+ return errors.Wrapf(err, "failed to chown recursively host path")
+ }
+ } else {
+ // Get host path info
+ f, err := os.Lstat(path)
+ if err != nil {
+ return errors.Wrapf(err, "failed to get host path information")
+ }
+
+ // Get current ownership
+ currentUID := int(f.Sys().(*syscall.Stat_t).Uid)
+ currentGID := int(f.Sys().(*syscall.Stat_t).Gid)
+
+ if uid != currentUID || gid != currentGID {
+ if err := os.Lchown(path, uid, gid); err != nil {
+ return errors.Wrapf(err, "failed to chown host path")
+ }
+ }
+ }
+
+ return nil
+}
diff --git a/vendor/modules.txt b/vendor/modules.txt
index d767d5e77..c323f09fc 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -95,6 +95,7 @@ github.com/containers/common/pkg/apparmor/internal/supported
github.com/containers/common/pkg/auth
github.com/containers/common/pkg/capabilities
github.com/containers/common/pkg/cgroupv2
+github.com/containers/common/pkg/chown
github.com/containers/common/pkg/completion
github.com/containers/common/pkg/config
github.com/containers/common/pkg/parse
@@ -782,7 +783,7 @@ gopkg.in/yaml.v3
# k8s.io/api v0.0.0-20190620084959-7cf5895f2711
k8s.io/api/apps/v1
k8s.io/api/core/v1
-# k8s.io/apimachinery v0.20.3
+# k8s.io/apimachinery v0.20.4
k8s.io/apimachinery/pkg/api/errors
k8s.io/apimachinery/pkg/api/resource
k8s.io/apimachinery/pkg/apis/meta/v1