get user and group information using securejoin and runc's user library

for the purposes of performance and security, we use securejoin to contstruct
the root fs's path so that symlinks are what they appear to be and no pointing
to something naughty.

then instead of chrooting to parse /etc/passwd|/etc/group, we now use the runc user/group
methods which saves us quite a bit of performance.

Signed-off-by: baude <bbaude@redhat.com>
This commit is contained in:
baude
2018-10-25 13:39:25 -05:00
parent aa853b2091
commit 1dd7f13dfb
4 changed files with 201 additions and 63 deletions

View File

@ -10,8 +10,8 @@ import (
"time" "time"
"github.com/containers/libpod/libpod/driver" "github.com/containers/libpod/libpod/driver"
"github.com/containers/libpod/pkg/chrootuser"
"github.com/containers/libpod/pkg/inspect" "github.com/containers/libpod/pkg/inspect"
"github.com/containers/libpod/pkg/lookup"
"github.com/containers/storage/pkg/stringid" "github.com/containers/storage/pkg/stringid"
"github.com/docker/docker/daemon/caps" "github.com/docker/docker/daemon/caps"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -292,13 +292,13 @@ func (c *Container) Exec(tty, privileged bool, env, cmd []string, user string) e
// the host // the host
hostUser := "" hostUser := ""
if user != "" { if user != "" {
uid, gid, err := chrootuser.GetUser(c.state.Mountpoint, user) execUser, err := lookup.GetUserGroupInfo(c.state.Mountpoint, user, nil)
if err != nil { if err != nil {
return errors.Wrapf(err, "error getting user to launch exec session as") return err
} }
// runc expects user formatted as uid:gid // runc expects user formatted as uid:gid
hostUser = fmt.Sprintf("%d:%d", uid, gid) hostUser = fmt.Sprintf("%d:%d", execUser.Uid, execUser.Gid)
} }
// Generate exec session ID // Generate exec session ID

View File

@ -5,6 +5,7 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/opencontainers/runc/libcontainer/user"
"io" "io"
"io/ioutil" "io/ioutil"
"os" "os"
@ -14,9 +15,9 @@ import (
"syscall" "syscall"
"github.com/containers/buildah/imagebuildah" "github.com/containers/buildah/imagebuildah"
"github.com/containers/libpod/pkg/chrootuser"
"github.com/containers/libpod/pkg/hooks" "github.com/containers/libpod/pkg/hooks"
"github.com/containers/libpod/pkg/hooks/exec" "github.com/containers/libpod/pkg/hooks/exec"
"github.com/containers/libpod/pkg/lookup"
"github.com/containers/libpod/pkg/resolvconf" "github.com/containers/libpod/pkg/resolvconf"
"github.com/containers/libpod/pkg/rootless" "github.com/containers/libpod/pkg/rootless"
"github.com/containers/libpod/pkg/secrets" "github.com/containers/libpod/pkg/secrets"
@ -1029,7 +1030,8 @@ func (c *Container) writeStringToRundir(destFile, output string) (string, error)
func (c *Container) generatePasswd() (string, error) { func (c *Container) generatePasswd() (string, error) {
var ( var (
groupspec string groupspec string
gid uint32 group *user.Group
gid int
) )
if c.config.User == "" { if c.config.User == "" {
return "", nil return "", nil
@ -1044,21 +1046,27 @@ func (c *Container) generatePasswd() (string, error) {
if err != nil { if err != nil {
return "", nil return "", nil
} }
// if UID exists inside of container rootfs /etc/passwd then // Lookup the user to see if it exists in the container image
// don't generate passwd _, err = lookup.GetUser(c.state.Mountpoint, userspec)
if _, _, err := chrootuser.LookupUIDInContainer(c.state.Mountpoint, uid); err == nil { if err != nil && err != user.ErrNoPasswdEntries {
return "", err
}
if err == nil {
return "", nil return "", nil
} }
if err == nil && groupspec != "" { if groupspec != "" {
if !c.state.Mounted { if !c.state.Mounted {
return "", errors.Wrapf(ErrCtrStateInvalid, "container %s must be mounted in order to translate group field for passwd record", c.ID()) return "", errors.Wrapf(ErrCtrStateInvalid, "container %s must be mounted in order to translate group field for passwd record", c.ID())
} }
gid, err = chrootuser.GetGroup(c.state.Mountpoint, groupspec) group, err = lookup.GetGroup(c.state.Mountpoint, groupspec)
if err != nil { if err != nil {
return "", errors.Wrapf(err, "unable to get gid from %s formporary passwd file") if err == user.ErrNoGroupEntries {
return "", errors.Wrapf(err, "unable to get gid %s from group file", groupspec)
}
return "", err
} }
gid = group.Gid
} }
originPasswdFile := filepath.Join(c.state.Mountpoint, "/etc/passwd") originPasswdFile := filepath.Join(c.state.Mountpoint, "/etc/passwd")
orig, err := ioutil.ReadFile(originPasswdFile) orig, err := ioutil.ReadFile(originPasswdFile)
if err != nil { if err != nil {
@ -1153,6 +1161,7 @@ func (c *Container) generateHosts() (string, error) {
} }
func (c *Container) addLocalVolumes(ctx context.Context, g *generate.Generator) error { func (c *Container) addLocalVolumes(ctx context.Context, g *generate.Generator) error {
var uid, gid int
mountPoint := c.state.Mountpoint mountPoint := c.state.Mountpoint
if !c.state.Mounted { if !c.state.Mounted {
return errors.Wrapf(ErrInternal, "container is not mounted") return errors.Wrapf(ErrInternal, "container is not mounted")
@ -1176,6 +1185,18 @@ func (c *Container) addLocalVolumes(ctx context.Context, g *generate.Generator)
} }
} }
if c.config.User != "" {
if !c.state.Mounted {
return errors.Wrapf(ErrCtrStateInvalid, "container %s must be mounted in order to translate User field", c.ID())
}
execUser, err := lookup.GetUserGroupInfo(c.state.Mountpoint, c.config.User, nil)
if err != nil {
return err
}
uid = execUser.Uid
gid = execUser.Gid
}
for k := range imageData.ContainerConfig.Volumes { for k := range imageData.ContainerConfig.Volumes {
mount := spec.Mount{ mount := spec.Mount{
Destination: k, Destination: k,
@ -1186,19 +1207,6 @@ func (c *Container) addLocalVolumes(ctx context.Context, g *generate.Generator)
continue continue
} }
volumePath := filepath.Join(c.config.StaticDir, "volumes", k) volumePath := filepath.Join(c.config.StaticDir, "volumes", k)
var (
uid uint32
gid uint32
)
if c.config.User != "" {
if !c.state.Mounted {
return errors.Wrapf(ErrCtrStateInvalid, "container %s must be mounted in order to translate User field", c.ID())
}
uid, gid, err = chrootuser.GetUser(c.state.Mountpoint, c.config.User)
if err != nil {
return err
}
}
// Ensure the symlinks are resolved // Ensure the symlinks are resolved
resolvedSymlink, err := imagebuildah.ResolveSymLink(mountPoint, k) resolvedSymlink, err := imagebuildah.ResolveSymLink(mountPoint, k)
@ -1218,7 +1226,7 @@ func (c *Container) addLocalVolumes(ctx context.Context, g *generate.Generator)
return errors.Wrapf(err, "error creating directory %q for volume %q in container %q", volumePath, k, c.ID()) return errors.Wrapf(err, "error creating directory %q for volume %q in container %q", volumePath, k, c.ID())
} }
if err = os.Chown(srcPath, int(uid), int(gid)); err != nil { if err = os.Chown(srcPath, uid, gid); err != nil {
return errors.Wrapf(err, "error chowning directory %q for volume %q in container %q", srcPath, k, c.ID()) return errors.Wrapf(err, "error chowning directory %q for volume %q in container %q", srcPath, k, c.ID())
} }
} }
@ -1228,7 +1236,7 @@ func (c *Container) addLocalVolumes(ctx context.Context, g *generate.Generator)
return errors.Wrapf(err, "error creating directory %q for volume %q in container %q", volumePath, k, c.ID()) return errors.Wrapf(err, "error creating directory %q for volume %q in container %q", volumePath, k, c.ID())
} }
if err = os.Chown(volumePath, int(uid), int(gid)); err != nil { if err = os.Chown(volumePath, uid, gid); err != nil {
return errors.Wrapf(err, "error chowning directory %q for volume %q in container %q", volumePath, k, c.ID()) return errors.Wrapf(err, "error chowning directory %q for volume %q in container %q", volumePath, k, c.ID())
} }

View File

@ -19,12 +19,10 @@ import (
cnitypes "github.com/containernetworking/cni/pkg/types/current" cnitypes "github.com/containernetworking/cni/pkg/types/current"
"github.com/containernetworking/plugins/pkg/ns" "github.com/containernetworking/plugins/pkg/ns"
crioAnnotations "github.com/containers/libpod/pkg/annotations" crioAnnotations "github.com/containers/libpod/pkg/annotations"
"github.com/containers/libpod/pkg/chrootuser"
"github.com/containers/libpod/pkg/criu" "github.com/containers/libpod/pkg/criu"
"github.com/containers/libpod/pkg/lookup"
"github.com/containers/libpod/pkg/rootless" "github.com/containers/libpod/pkg/rootless"
"github.com/containers/storage/pkg/idtools" "github.com/containers/storage/pkg/idtools"
"github.com/cyphar/filepath-securejoin"
"github.com/opencontainers/runc/libcontainer/user"
spec "github.com/opencontainers/runtime-spec/specs-go" spec "github.com/opencontainers/runtime-spec/specs-go"
"github.com/opencontainers/runtime-tools/generate" "github.com/opencontainers/runtime-tools/generate"
"github.com/opencontainers/selinux/go-selinux/label" "github.com/opencontainers/selinux/go-selinux/label"
@ -135,6 +133,10 @@ func (c *Container) cleanupNetwork() error {
// Generate spec for a container // Generate spec for a container
// Accepts a map of the container's dependencies // Accepts a map of the container's dependencies
func (c *Container) generateSpec(ctx context.Context) (*spec.Spec, error) { func (c *Container) generateSpec(ctx context.Context) (*spec.Spec, error) {
execUser, err := lookup.GetUserGroupInfo(c.state.Mountpoint, c.config.User, nil)
if err != nil {
return nil, err
}
g := generate.NewFromSpec(c.config.Spec) g := generate.NewFromSpec(c.config.Spec)
// If network namespace was requested, add it now // If network namespace was requested, add it now
@ -188,7 +190,6 @@ func (c *Container) generateSpec(ctx context.Context) (*spec.Spec, error) {
} }
} }
var err error
if !rootless.IsRootless() { if !rootless.IsRootless() {
if c.state.ExtensionStageHooks, err = c.setupOCIHooks(ctx, g.Config); err != nil { if c.state.ExtensionStageHooks, err = c.setupOCIHooks(ctx, g.Config); err != nil {
return nil, errors.Wrapf(err, "error setting up OCI Hooks") return nil, errors.Wrapf(err, "error setting up OCI Hooks")
@ -206,13 +207,9 @@ func (c *Container) generateSpec(ctx context.Context) (*spec.Spec, error) {
if !c.state.Mounted { if !c.state.Mounted {
return nil, errors.Wrapf(ErrCtrStateInvalid, "container %s must be mounted in order to translate User field", c.ID()) return nil, errors.Wrapf(ErrCtrStateInvalid, "container %s must be mounted in order to translate User field", c.ID())
} }
uid, gid, err := chrootuser.GetUser(c.state.Mountpoint, c.config.User)
if err != nil {
return nil, err
}
// User and Group must go together // User and Group must go together
g.SetProcessUID(uid) g.SetProcessUID(uint32(execUser.Uid))
g.SetProcessGID(gid) g.SetProcessGID(uint32(execUser.Gid))
} }
// Add addition groups if c.config.GroupAdd is not empty // Add addition groups if c.config.GroupAdd is not empty
@ -220,11 +217,8 @@ func (c *Container) generateSpec(ctx context.Context) (*spec.Spec, error) {
if !c.state.Mounted { if !c.state.Mounted {
return nil, errors.Wrapf(ErrCtrStateInvalid, "container %s must be mounted in order to add additional groups", c.ID()) return nil, errors.Wrapf(ErrCtrStateInvalid, "container %s must be mounted in order to add additional groups", c.ID())
} }
for _, group := range c.config.Groups { gids, _ := lookup.GetContainerGroups(c.config.Groups, c.state.Mountpoint, nil)
gid, err := chrootuser.GetGroup(c.state.Mountpoint, group) for _, gid := range gids {
if err != nil {
return nil, err
}
g.AddProcessAdditionalGid(gid) g.AddProcessAdditionalGid(gid)
} }
} }
@ -237,26 +231,6 @@ func (c *Container) generateSpec(ctx context.Context) (*spec.Spec, error) {
// Look up and add groups the user belongs to, if a group wasn't directly specified // Look up and add groups the user belongs to, if a group wasn't directly specified
if !rootless.IsRootless() && !strings.Contains(c.config.User, ":") { if !rootless.IsRootless() && !strings.Contains(c.config.User, ":") {
var groupDest, passwdDest string
defaultExecUser := user.ExecUser{
Uid: 0,
Gid: 0,
Home: "/",
}
// Make sure the /etc/group and /etc/passwd destinations are not a symlink to something naughty
if groupDest, err = securejoin.SecureJoin(c.state.Mountpoint, "/etc/group"); err != nil {
logrus.Debug(err)
return nil, err
}
if passwdDest, err = securejoin.SecureJoin(c.state.Mountpoint, "/etc/passwd"); err != nil {
logrus.Debug(err)
return nil, err
}
execUser, err := user.GetExecUserPath(c.config.User, &defaultExecUser, passwdDest, groupDest)
if err != nil {
return nil, err
}
for _, gid := range execUser.Sgids { for _, gid := range execUser.Sgids {
g.AddProcessAdditionalGid(uint32(gid)) g.AddProcessAdditionalGid(uint32(gid))
} }

156
pkg/lookup/lookup.go Normal file
View File

@ -0,0 +1,156 @@
package lookup
import (
"github.com/cyphar/filepath-securejoin"
"github.com/opencontainers/runc/libcontainer/user"
"github.com/sirupsen/logrus"
"strconv"
)
const (
etcpasswd = "/etc/passwd"
etcgroup = "/etc/group"
)
// Overrides allows you to override defaults in GetUserGroupInfo
type Overrides struct {
DefaultUser *user.ExecUser
ContainerEtcPasswdPath string
ContainerEtcGroupPath string
}
// GetUserGroupInfo takes string forms of the the container's mount path and the container user and
// returns a ExecUser with uid, gid, sgids, and home. And override can be provided for defaults.
func GetUserGroupInfo(containerMount, containerUser string, override *Overrides) (*user.ExecUser, error) {
var (
passwdDest, groupDest string
defaultExecUser *user.ExecUser
err error
)
passwdPath := etcpasswd
groupPath := etcgroup
if override != nil {
// Check for an override /etc/passwd path
if override.ContainerEtcPasswdPath != "" {
passwdPath = override.ContainerEtcPasswdPath
}
// Check for an override for /etc/group path
if override.ContainerEtcGroupPath != "" {
groupPath = override.ContainerEtcGroupPath
}
}
// Check for an override default user
if override != nil && override.DefaultUser != nil {
defaultExecUser = override.DefaultUser
} else {
// Define a default container user
//defaultExecUser = &user.ExecUser{
// Uid: 0,
// Gid: 0,
// Home: "/",
defaultExecUser = nil
}
// Make sure the /etc/group and /etc/passwd destinations are not a symlink to something naughty
if passwdDest, err = securejoin.SecureJoin(containerMount, passwdPath); err != nil {
logrus.Debug(err)
return nil, err
}
if groupDest, err = securejoin.SecureJoin(containerMount, groupPath); err != nil {
logrus.Debug(err)
return nil, err
}
return user.GetExecUserPath(containerUser, defaultExecUser, passwdDest, groupDest)
}
// GetContainerGroups uses securejoin to get a list of numerical groupids from a container. Per the runc
// function it calls: If a group name cannot be found, an error will be returned. If a group id cannot be found,
// or the given group data is nil, the id will be returned as-is provided it is in the legal range.
func GetContainerGroups(groups []string, containerMount string, override *Overrides) ([]uint32, error) {
var (
groupDest string
err error
uintgids []uint32
)
groupPath := etcgroup
if override != nil && override.ContainerEtcGroupPath != "" {
groupPath = override.ContainerEtcGroupPath
}
if groupDest, err = securejoin.SecureJoin(containerMount, groupPath); err != nil {
logrus.Debug(err)
return nil, err
}
gids, err := user.GetAdditionalGroupsPath(groups, groupDest)
if err != nil {
return nil, err
}
// For libpod, we want []uint32s
for _, gid := range gids {
uintgids = append(uintgids, uint32(gid))
}
return uintgids, nil
}
// GetUser takes a containermount path and user name or id and returns
// a matching User structure from /etc/passwd. If it cannot locate a user
// with the provided information, an ErrNoPasswdEntries is returned.
func GetUser(containerMount, userIDorName string) (*user.User, error) {
var inputIsName bool
uid, err := strconv.Atoi(userIDorName)
if err != nil {
inputIsName = true
}
passwdDest, err := securejoin.SecureJoin(containerMount, etcpasswd)
if err != nil {
return nil, err
}
users, err := user.ParsePasswdFileFilter(passwdDest, func(u user.User) bool {
if inputIsName {
return u.Name == userIDorName
}
return u.Uid == uid
})
if err != nil {
return nil, err
}
if len(users) > 0 {
return &users[0], nil
}
return nil, user.ErrNoPasswdEntries
}
// GetGroup takes ac ontainermount path and a group name or id and returns
// a match Group struct from /etc/group. if it cannot locate a group,
// an ErrNoGroupEntries error is returned.
func GetGroup(containerMount, groupIDorName string) (*user.Group, error) {
var inputIsName bool
gid, err := strconv.Atoi(groupIDorName)
if err != nil {
inputIsName = true
}
groupDest, err := securejoin.SecureJoin(containerMount, etcgroup)
if err != nil {
return nil, err
}
groups, err := user.ParseGroupFileFilter(groupDest, func(g user.Group) bool {
if inputIsName {
return g.Name == groupIDorName
}
return g.Gid == gid
})
if err != nil {
return nil, err
}
if len(groups) > 0 {
return &groups[0], nil
}
return nil, user.ErrNoGroupEntries
}