Merge pull request #7541 from mheon/modify_group

Make an entry in /etc/group when we modify /etc/passwd
This commit is contained in:
OpenShift Merge Robot
2020-09-10 17:05:02 -04:00
committed by GitHub
3 changed files with 432 additions and 85 deletions

View File

@ -36,7 +36,7 @@ import (
"github.com/containers/podman/v2/utils"
"github.com/containers/storage/pkg/archive"
securejoin "github.com/cyphar/filepath-securejoin"
User "github.com/opencontainers/runc/libcontainer/user"
runcuser "github.com/opencontainers/runc/libcontainer/user"
spec "github.com/opencontainers/runtime-spec/specs-go"
"github.com/opencontainers/runtime-tools/generate"
"github.com/opencontainers/selinux/go-selinux/label"
@ -1276,7 +1276,7 @@ func (c *Container) makeBindMounts() error {
// SHM is always added when we mount the container
c.state.BindMounts["/dev/shm"] = c.config.ShmDir
newPasswd, err := c.generatePasswd()
newPasswd, newGroup, err := c.generatePasswdAndGroup()
if err != nil {
return errors.Wrapf(err, "error creating temporary passwd file for container %s", c.ID())
}
@ -1286,9 +1286,16 @@ func (c *Container) makeBindMounts() error {
// If it already exists, delete so we can recreate
delete(c.state.BindMounts, "/etc/passwd")
}
logrus.Debugf("adding entry to /etc/passwd for non existent default user")
c.state.BindMounts["/etc/passwd"] = newPasswd
}
if newGroup != "" {
// Make /etc/group
if _, ok := c.state.BindMounts["/etc/group"]; ok {
// If it already exists, delete so we can recreate
delete(c.state.BindMounts, "/etc/group")
}
c.state.BindMounts["/etc/group"] = newGroup
}
// Make /etc/hostname
// This should never change, so no need to recreate if it exists
@ -1507,23 +1514,171 @@ func (c *Container) getHosts() string {
return hosts
}
// generateCurrentUserPasswdEntry generates an /etc/passwd entry for the user
// running the container engine
func (c *Container) generateCurrentUserPasswdEntry() (string, error) {
uid := rootless.GetRootlessUID()
if uid == 0 {
return "", nil
// generateGroupEntry generates an entry or entries into /etc/group as
// required by container configuration.
// Generatlly speaking, we will make an entry under two circumstances:
// 1. The container is started as a specific user:group, and that group is both
// numeric, and does not already exist in /etc/group.
// 2. It is requested that Libpod add the group that launched Podman to
// /etc/group via AddCurrentUserPasswdEntry (though this does not trigger if
// the group in question already exists in /etc/passwd).
// Returns group entry (as a string that can be appended to /etc/group) and any
// error that occurred.
func (c *Container) generateGroupEntry() (string, error) {
groupString := ""
// Things we *can't* handle: adding the user we added in
// generatePasswdEntry to any *existing* groups.
addedGID := 0
if c.config.AddCurrentUserPasswdEntry {
entry, gid, err := c.generateCurrentUserGroupEntry()
if err != nil {
return "", err
}
groupString += entry
addedGID = gid
}
if c.config.User != "" {
entry, _, err := c.generateUserGroupEntry(addedGID)
if err != nil {
return "", err
}
groupString += entry
}
u, err := user.LookupId(strconv.Itoa(rootless.GetRootlessUID()))
return groupString, nil
}
// Make an entry in /etc/group for the group of the user running podman iff we
// are rootless.
func (c *Container) generateCurrentUserGroupEntry() (string, int, error) {
gid := rootless.GetRootlessGID()
if gid == 0 {
return "", 0, nil
}
g, err := user.LookupGroupId(strconv.Itoa(gid))
if err != nil {
return "", errors.Wrapf(err, "failed to get current user")
return "", 0, errors.Wrapf(err, "failed to get current group")
}
// Lookup group name to see if it exists in the image.
_, err = lookup.GetGroup(c.state.Mountpoint, g.Name)
if err != runcuser.ErrNoGroupEntries {
return "", 0, err
}
// Lookup GID to see if it exists in the image.
_, err = lookup.GetGroup(c.state.Mountpoint, g.Gid)
if err != runcuser.ErrNoGroupEntries {
return "", 0, err
}
// We need to get the username of the rootless user so we can add it to
// the group.
username := ""
uid := rootless.GetRootlessUID()
if uid != 0 {
u, err := user.LookupId(strconv.Itoa(uid))
if err != nil {
return "", 0, errors.Wrapf(err, "failed to get current user to make group entry")
}
username = u.Username
}
// Make the entry.
return fmt.Sprintf("%s:x:%s:%s\n", g.Name, g.Gid, username), gid, nil
}
// Make an entry in /etc/group for the group the container was specified to run
// as.
func (c *Container) generateUserGroupEntry(addedGID int) (string, int, error) {
if c.config.User == "" {
return "", 0, nil
}
splitUser := strings.SplitN(c.config.User, ":", 2)
group := splitUser[0]
if len(splitUser) > 1 {
group = splitUser[1]
}
gid, err := strconv.ParseUint(group, 10, 32)
if err != nil {
return "", 0, nil
}
if addedGID != 0 && addedGID == int(gid) {
return "", 0, nil
}
// Check if the group already exists
_, err = lookup.GetGroup(c.state.Mountpoint, group)
if err != runcuser.ErrNoGroupEntries {
return "", 0, err
}
return fmt.Sprintf("%d:x:%d:%s\n", gid, gid, splitUser[0]), int(gid), nil
}
// generatePasswdEntry generates an entry or entries into /etc/passwd as
// required by container configuration.
// Generally speaking, we will make an entry under two circumstances:
// 1. The container is started as a specific user who is not in /etc/passwd.
// This only triggers if the user is given as a *numeric* ID.
// 2. It is requested that Libpod add the user that launched Podman to
// /etc/passwd via AddCurrentUserPasswdEntry (though this does not trigger if
// the user in question already exists in /etc/passwd) or the UID to be added
// is 0).
// Returns password entry (as a string that can be appended to /etc/passwd) and
// any error that occurred.
func (c *Container) generatePasswdEntry() (string, error) {
passwdString := ""
addedUID := 0
if c.config.AddCurrentUserPasswdEntry {
entry, uid, _, err := c.generateCurrentUserPasswdEntry()
if err != nil {
return "", err
}
passwdString += entry
addedUID = uid
}
if c.config.User != "" {
entry, _, _, err := c.generateUserPasswdEntry(addedUID)
if err != nil {
return "", err
}
passwdString += entry
}
return passwdString, nil
}
// generateCurrentUserPasswdEntry generates an /etc/passwd entry for the user
// running the container engine.
// Returns a passwd entry for the user, and the UID and GID of the added entry.
func (c *Container) generateCurrentUserPasswdEntry() (string, int, int, error) {
uid := rootless.GetRootlessUID()
if uid == 0 {
return "", 0, 0, nil
}
u, err := user.LookupId(strconv.Itoa(uid))
if err != nil {
return "", 0, 0, errors.Wrapf(err, "failed to get current user")
}
// Lookup the user to see if it exists in the container image.
_, err = lookup.GetUser(c.state.Mountpoint, u.Username)
if err != User.ErrNoPasswdEntries {
return "", err
if err != runcuser.ErrNoPasswdEntries {
return "", 0, 0, err
}
// Lookup the UID to see if it exists in the container image.
_, err = lookup.GetUser(c.state.Mountpoint, u.Uid)
if err != runcuser.ErrNoPasswdEntries {
return "", 0, 0, err
}
// If the user's actual home directory exists, or was mounted in - use
@ -1533,18 +1688,22 @@ func (c *Container) generateCurrentUserPasswdEntry() (string, error) {
homeDir = u.HomeDir
}
return fmt.Sprintf("%s:x:%s:%s:%s:%s:/bin/sh\n", u.Username, u.Uid, u.Gid, u.Username, homeDir), nil
return fmt.Sprintf("%s:*:%s:%s:%s:%s:/bin/sh\n", u.Username, u.Uid, u.Gid, u.Username, homeDir), uid, rootless.GetRootlessGID(), nil
}
// generateUserPasswdEntry generates an /etc/passwd entry for the container user
// to run in the container.
func (c *Container) generateUserPasswdEntry() (string, error) {
// The UID and GID of the added entry will also be returned.
// Accepts one argument, that being any UID that has already been added to the
// passwd file by other functions; if it matches the UID we were given, we don't
// need to do anything.
func (c *Container) generateUserPasswdEntry(addedUID int) (string, int, int, error) {
var (
groupspec string
gid int
)
if c.config.User == "" {
return "", nil
return "", 0, 0, nil
}
splitSpec := strings.SplitN(c.config.User, ":", 2)
userspec := splitSpec[0]
@ -1554,13 +1713,17 @@ func (c *Container) generateUserPasswdEntry() (string, error) {
// If a non numeric User, then don't generate passwd
uid, err := strconv.ParseUint(userspec, 10, 32)
if err != nil {
return "", nil
return "", 0, 0, nil
}
if addedUID != 0 && int(uid) == addedUID {
return "", 0, 0, nil
}
// Lookup the user to see if it exists in the container image
_, err = lookup.GetUser(c.state.Mountpoint, userspec)
if err != User.ErrNoPasswdEntries {
return "", err
if err != runcuser.ErrNoPasswdEntries {
return "", 0, 0, err
}
if groupspec != "" {
@ -1570,96 +1733,180 @@ func (c *Container) generateUserPasswdEntry() (string, error) {
} else {
group, err := lookup.GetGroup(c.state.Mountpoint, groupspec)
if err != nil {
return "", errors.Wrapf(err, "unable to get gid %s from group file", groupspec)
return "", 0, 0, errors.Wrapf(err, "unable to get gid %s from group file", groupspec)
}
gid = group.Gid
}
}
return fmt.Sprintf("%d:x:%d:%d:container user:%s:/bin/sh\n", uid, uid, gid, c.WorkingDir()), nil
return fmt.Sprintf("%d:*:%d:%d:container user:%s:/bin/sh\n", uid, uid, gid, c.WorkingDir()), int(uid), gid, nil
}
// generatePasswd generates a container specific passwd file,
// iff g.config.User is a number
func (c *Container) generatePasswd() (string, error) {
// generatePasswdAndGroup generates container-specific passwd and group files
// iff g.config.User is a number or we are configured to make a passwd entry for
// the current user.
// Returns path to file to mount at /etc/passwd, path to file to mount at
// /etc/group, and any error that occurred. If no passwd/group file were
// required, the empty string will be returned for those path (this may occur
// even if no error happened).
// This may modify the mounted container's /etc/passwd and /etc/group instead of
// making copies to bind-mount in, so we don't break useradd (it wants to make a
// copy of /etc/passwd and rename the copy to /etc/passwd, which is impossible
// with a bind mount). This is done in cases where the container is *not*
// read-only. In this case, the function will return nothing ("", "", nil).
func (c *Container) generatePasswdAndGroup() (string, string, error) {
if !c.config.AddCurrentUserPasswdEntry && c.config.User == "" {
return "", nil
return "", "", nil
}
needPasswd := true
needGroup := true
// First, check if there's a mount at /etc/passwd or group, we don't
// want to interfere with user mounts.
if MountExists(c.config.Spec.Mounts, "/etc/passwd") {
return "", nil
needPasswd = false
}
// Re-use passwd if possible
passwdPath := filepath.Join(c.config.StaticDir, "passwd")
if _, err := os.Stat(passwdPath); err == nil {
return passwdPath, nil
if MountExists(c.config.Spec.Mounts, "/etc/group") {
needGroup = false
}
// Check if container has a /etc/passwd - if it doesn't do nothing.
passwdPath, err := securejoin.SecureJoin(c.state.Mountpoint, "/etc/passwd")
if err != nil {
return "", errors.Wrapf(err, "error creating path to container %s /etc/passwd", c.ID())
}
if _, err := os.Stat(passwdPath); err != nil {
if os.IsNotExist(err) {
return "", nil
// Next, check if we already made the files. If we didn, don't need to
// do anything more.
if needPasswd {
passwdPath := filepath.Join(c.config.StaticDir, "passwd")
if _, err := os.Stat(passwdPath); err == nil {
needPasswd = false
}
return "", errors.Wrapf(err, "unable to access container %s /etc/passwd", c.ID())
}
pwd := ""
if c.config.User != "" {
entry, err := c.generateUserPasswdEntry()
if needGroup {
groupPath := filepath.Join(c.config.StaticDir, "group")
if _, err := os.Stat(groupPath); err == nil {
needGroup = false
}
}
// Next, check if the container even has a /etc/passwd or /etc/group.
// If it doesn't we don't want to create them ourselves.
if needPasswd {
exists, err := c.checkFileExistsInRootfs("/etc/passwd")
if err != nil {
return "", err
return "", "", err
}
pwd += entry
needPasswd = exists
}
if c.config.AddCurrentUserPasswdEntry {
entry, err := c.generateCurrentUserPasswdEntry()
if needGroup {
exists, err := c.checkFileExistsInRootfs("/etc/group")
if err != nil {
return "", err
return "", "", err
}
pwd += entry
}
if pwd == "" {
return "", nil
needGroup = exists
}
// If we are *not* read-only - edit /etc/passwd in the container.
// This is *gross* (shows up in changes to the container, will be
// committed to images based on the container) but it actually allows us
// to add users to the container (a bind mount breaks useradd).
// We should never get here twice, because generateUserPasswdEntry will
// not return anything if the user already exists in /etc/passwd.
if !c.IsReadOnly() {
containerPasswd, err := securejoin.SecureJoin(c.state.Mountpoint, "/etc/passwd")
// If we don't need a /etc/passwd or /etc/group at this point we can
// just return.
if !needPasswd && !needGroup {
return "", "", nil
}
passwdPath := ""
groupPath := ""
ro := c.IsReadOnly()
if needPasswd {
passwdEntry, err := c.generatePasswdEntry()
if err != nil {
return "", errors.Wrapf(err, "error looking up location of container %s /etc/passwd", c.ID())
return "", "", err
}
f, err := os.OpenFile(containerPasswd, os.O_APPEND|os.O_WRONLY, 0600)
needsWrite := passwdEntry != ""
switch {
case ro && needsWrite:
logrus.Debugf("Making /etc/passwd for container %s", c.ID())
originPasswdFile, err := securejoin.SecureJoin(c.state.Mountpoint, "/etc/passwd")
if err != nil {
return "", "", errors.Wrapf(err, "error creating path to container %s /etc/passwd", c.ID())
}
orig, err := ioutil.ReadFile(originPasswdFile)
if err != nil && !os.IsNotExist(err) {
return "", "", errors.Wrapf(err, "unable to read passwd file %s", originPasswdFile)
}
passwdFile, err := c.writeStringToStaticDir("passwd", string(orig)+passwdEntry)
if err != nil {
return "", "", errors.Wrapf(err, "failed to create temporary passwd file")
}
if err := os.Chmod(passwdFile, 0644); err != nil {
return "", "", err
}
passwdPath = passwdFile
case !ro && needsWrite:
logrus.Debugf("Modifying container %s /etc/passwd", c.ID())
containerPasswd, err := securejoin.SecureJoin(c.state.Mountpoint, "/etc/passwd")
if err != nil {
return "", "", errors.Wrapf(err, "error looking up location of container %s /etc/passwd", c.ID())
}
f, err := os.OpenFile(containerPasswd, os.O_APPEND|os.O_WRONLY, 0600)
if err != nil {
return "", "", errors.Wrapf(err, "error opening container %s /etc/passwd", c.ID())
}
defer f.Close()
if _, err := f.WriteString(passwdEntry); err != nil {
return "", "", errors.Wrapf(err, "unable to append to container %s /etc/passwd", c.ID())
}
default:
logrus.Debugf("Not modifying container %s /etc/passwd", c.ID())
}
}
if needGroup {
groupEntry, err := c.generateGroupEntry()
if err != nil {
return "", errors.Wrapf(err, "error opening container %s /etc/passwd", c.ID())
}
defer f.Close()
if _, err := f.WriteString(pwd); err != nil {
return "", errors.Wrapf(err, "unable to append to container %s /etc/passwd", c.ID())
return "", "", err
}
return "", nil
needsWrite := groupEntry != ""
switch {
case ro && needsWrite:
logrus.Debugf("Making /etc/group for container %s", c.ID())
originGroupFile, err := securejoin.SecureJoin(c.state.Mountpoint, "/etc/group")
if err != nil {
return "", "", errors.Wrapf(err, "error creating path to container %s /etc/group", c.ID())
}
orig, err := ioutil.ReadFile(originGroupFile)
if err != nil && !os.IsNotExist(err) {
return "", "", errors.Wrapf(err, "unable to read group file %s", originGroupFile)
}
groupFile, err := c.writeStringToStaticDir("group", string(orig)+groupEntry)
if err != nil {
return "", "", errors.Wrapf(err, "failed to create temporary group file")
}
if err := os.Chmod(groupFile, 0644); err != nil {
return "", "", err
}
groupPath = groupFile
case !ro && needsWrite:
logrus.Debugf("Modifying container %s /etc/group", c.ID())
containerGroup, err := securejoin.SecureJoin(c.state.Mountpoint, "/etc/group")
if err != nil {
return "", "", errors.Wrapf(err, "error looking up location of container %s /etc/group", c.ID())
}
f, err := os.OpenFile(containerGroup, os.O_APPEND|os.O_WRONLY, 0600)
if err != nil {
return "", "", errors.Wrapf(err, "error opening container %s /etc/group", c.ID())
}
defer f.Close()
if _, err := f.WriteString(groupEntry); err != nil {
return "", "", errors.Wrapf(err, "unable to append to container %s /etc/group", c.ID())
}
default:
logrus.Debugf("Not modifying container %s /etc/group", c.ID())
}
}
originPasswdFile := filepath.Join(c.state.Mountpoint, "/etc/passwd")
orig, err := ioutil.ReadFile(originPasswdFile)
if err != nil && !os.IsNotExist(err) {
return "", errors.Wrapf(err, "unable to read passwd file %s", originPasswdFile)
}
passwdFile, err := c.writeStringToStaticDir("passwd", string(orig)+pwd)
if err != nil {
return "", errors.Wrapf(err, "failed to create temporary passwd file")
}
if err := os.Chmod(passwdFile, 0644); err != nil {
return "", err
}
return passwdFile, nil
return passwdPath, groupPath, nil
}
func (c *Container) copyOwnerAndPerms(source, dest string) error {
@ -1751,3 +1998,23 @@ func (c *Container) copyTimezoneFile(zonePath string) (string, error) {
func (c *Container) cleanupOverlayMounts() error {
return overlay.CleanupContent(c.config.StaticDir)
}
// Check if a file exists at the given path in the container's root filesystem.
// Container must already be mounted for this to be used.
func (c *Container) checkFileExistsInRootfs(file string) (bool, error) {
checkPath, err := securejoin.SecureJoin(c.state.Mountpoint, file)
if err != nil {
return false, errors.Wrapf(err, "cannot create path to container %s file %q", c.ID(), file)
}
stat, err := os.Stat(checkPath)
if err != nil {
if os.IsNotExist(err) {
return false, nil
}
return false, errors.Wrapf(err, "error accessing container %s file %q", c.ID(), file)
}
if stat.IsDir() {
return false, nil
}
return true, nil
}

View File

@ -29,16 +29,42 @@ func TestGenerateUserPasswdEntry(t *testing.T) {
Mountpoint: "/does/not/exist/tmp/",
},
}
user, err := c.generateUserPasswdEntry()
user, _, _, err := c.generateUserPasswdEntry(0)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, user, "123:x:123:456:container user:/:/bin/sh\n")
assert.Equal(t, user, "123:*:123:456:container user:/:/bin/sh\n")
c.config.User = "567"
user, err = c.generateUserPasswdEntry()
user, _, _, err = c.generateUserPasswdEntry(0)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, user, "567:x:567:0:container user:/:/bin/sh\n")
assert.Equal(t, user, "567:*:567:0:container user:/:/bin/sh\n")
}
func TestGenerateUserGroupEntry(t *testing.T) {
c := Container{
config: &ContainerConfig{
Spec: &spec.Spec{},
ContainerSecurityConfig: ContainerSecurityConfig{
User: "123:456",
},
},
state: &ContainerState{
Mountpoint: "/does/not/exist/tmp/",
},
}
group, _, err := c.generateUserGroupEntry(0)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, group, "456:x:456:123\n")
c.config.User = "567"
group, _, err = c.generateUserGroupEntry(0)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, group, "567:x:567:567\n")
}

View File

@ -71,4 +71,58 @@ USER 1000`
Expect(session.ExitCode()).To(Equal(0))
Expect(session.OutputToString()).To(Not(ContainSubstring("passwd")))
})
It("podman run with no user specified does not change --group specified", func() {
session := podmanTest.Podman([]string{"run", "--read-only", BB, "mount"})
session.WaitWithDefaultTimeout()
Expect(session.ExitCode()).To(Equal(0))
Expect(session.LineInOutputContains("/etc/group")).To(BeFalse())
})
It("podman run group specified in container", func() {
session := podmanTest.Podman([]string{"run", "--read-only", "-u", "root:bin", BB, "mount"})
session.WaitWithDefaultTimeout()
Expect(session.ExitCode()).To(Equal(0))
Expect(session.LineInOutputContains("/etc/group")).To(BeFalse())
})
It("podman run non-numeric group not specified in container", func() {
session := podmanTest.Podman([]string{"run", "--read-only", "-u", "root:doesnotexist", BB, "mount"})
session.WaitWithDefaultTimeout()
Expect(session.ExitCode()).To(Not(Equal(0)))
})
It("podman run numeric group specified in container", func() {
session := podmanTest.Podman([]string{"run", "--read-only", "-u", "root:11", BB, "mount"})
session.WaitWithDefaultTimeout()
Expect(session.ExitCode()).To(Equal(0))
Expect(session.LineInOutputContains("/etc/group")).To(BeFalse())
})
It("podman run numeric group not specified in container", func() {
session := podmanTest.Podman([]string{"run", "--read-only", "-u", "20001:20001", BB, "mount"})
session.WaitWithDefaultTimeout()
Expect(session.ExitCode()).To(Equal(0))
Expect(session.LineInOutputContains("/etc/group")).To(BeTrue())
})
It("podman run numeric user not specified in container modifies group", func() {
session := podmanTest.Podman([]string{"run", "--read-only", "-u", "20001", BB, "mount"})
session.WaitWithDefaultTimeout()
Expect(session.ExitCode()).To(Equal(0))
Expect(session.LineInOutputContains("/etc/group")).To(BeTrue())
})
It("podman run numeric group from image and no group file", func() {
SkipIfRemote()
dockerfile := `FROM alpine
RUN rm -f /etc/passwd /etc/shadow /etc/group
USER 1000`
imgName := "testimg"
podmanTest.BuildImage(dockerfile, imgName, "false")
session := podmanTest.Podman([]string{"run", "--rm", imgName, "ls", "/etc/"})
session.WaitWithDefaultTimeout()
Expect(session.ExitCode()).To(Equal(0))
Expect(session.OutputToString()).To(Not(ContainSubstring("/etc/group")))
})
})