sensor: Fix up target app HOME env var when run as user (#426)

Sensor typically needs to run as root while the target app may need to
use a less privileged user. By default, conainer runtimes (like Docker) set
the HOME env var upon container startup based on the container's user
and the corresponding record in the /etc/passwd file in the image (if any, "/"
otherwser). However, instrumented container's user is often different
from the target app's user. Thus, sensor needs extra logic to restore
the right HOME var doing similar computations.

NB: Having HOME env var set is mandatory in accordance with POSIX.
This commit is contained in:
Ivan Velichko 2022-11-10 18:09:56 +01:00 committed by GitHub
parent 00c52c60a1
commit 1bd42e2af4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 130 additions and 23 deletions

View File

@ -390,3 +390,57 @@ func TestRunTargetAsUser(t *testing.T) {
sensor.AssertSensorLogsContain(t, ctx, sensorFullLifecycleSequence...)
sensor.AssertTargetAppLogsContain(t, ctx, "daemon")
}
func TestTargetAppEnvVars(t *testing.T) {
cases := []struct {
image string
user string
home string
}{
// Alpine
{image: imageSimpleCLI, home: "/root"},
{image: imageSimpleCLI, user: "root", home: "/root"},
{image: imageSimpleCLI, user: "0", home: "/root"},
{image: imageSimpleCLI, user: "nobody", home: "/"},
{image: imageSimpleCLI, user: "65534", home: "/"}, // nobody's UID
{image: imageSimpleCLI, user: "bin", home: "/bin"},
{image: imageSimpleCLI, user: "1", home: "/bin"}, // bin's UID
{image: imageSimpleCLI, user: "daemon", home: "/sbin"},
{image: imageSimpleCLI, user: "2", home: "/sbin"}, // daemon's UID
{image: imageSimpleCLI, user: "nosuchuser", home: "/"},
{image: imageSimpleCLI, user: "14567", home: "/"}, // hopefully, no such UID
// Nginx
{image: imageSimpleService, user: "nginx", home: "/nonexistent"},
{image: imageSimpleService, user: "101", home: "/nonexistent"}, // nginx's UID
{image: imageSimpleService, user: "nobody", home: "/"},
{image: imageSimpleService, user: "65534", home: "/"}, // nobody's UID
{image: imageSimpleService, user: "daemon", home: "/usr/sbin"},
{image: imageSimpleService, user: "1", home: "/usr/sbin"}, // daemon's UID
{image: imageSimpleService, user: "nosuchuser", home: "/"},
{image: imageSimpleService, user: "14567", home: "/"}, // hopefully, no such UID
}
for _, tcase := range cases {
func() {
runID := newTestRun(t)
ctx := context.Background()
sensor := testsensor.NewSensorOrFail(t, ctx, t.TempDir(), runID, tcase.image)
defer sensor.Cleanup(t, ctx)
var startOpts []testsensor.StartMonitorOpt
if len(tcase.user) > 0 {
startOpts = append(startOpts, testsensor.WithAppUser(tcase.user))
}
sensor.StartStandaloneOrFail(
t, ctx, []string{"env"},
testsensor.NewMonitorStartCommand(startOpts...),
)
sensor.WaitOrFail(t, ctx)
sensor.AssertSensorLogsContain(t, ctx, sensorFullLifecycleSequence...)
sensor.AssertTargetAppLogsContain(t, ctx, "HOME="+tcase.home)
}()
}
}

View File

@ -7,6 +7,7 @@ import (
"io"
"os"
"os/exec"
"os/user"
"strings"
"syscall"
@ -100,6 +101,11 @@ func Start(
}
}
app.Dir = appDir
app.Stdin = os.Stdin
app.Stdout = appStdout
app.Stderr = appStderr
if appUser != "" {
if app.SysProcAttr == nil {
app.SysProcAttr = &syscall.SysProcAttr{}
@ -107,7 +113,7 @@ func Start(
appUserParts := strings.Split(appUser, ":")
if len(appUserParts) > 0 {
uid, gid, err := system.ResolveUser(appUserParts[0])
uid, gid, home, err := system.ResolveUser(appUserParts[0])
if err == nil {
if len(appUserParts) > 1 {
xgid, err := system.ResolveGroup(appUserParts[1])
@ -129,17 +135,22 @@ func Start(
log.Errorf("launcher.Start: error fixing i/o perms for user (%v/%v) - %v", appUser, uid, err)
}
app.Env = appEnv(home)
} else {
log.Errorf("launcher.Start: error resolving user identity (%v/%v) - %v", appUser, appUserParts[0], err)
}
}
} else {
// This is not exactly the same as leaving app.Env unset.
// When cmd.Env == nil, Go stdlib takes the os.Environ() list
// **AND** adds the PWD=`cmd.Dir` unless Dir is empty. This doesn't
// match the Docker's standard behavior - even when an image has
// the WORKDIR set, there is no PWD env var unless it's set
// explicitly. Thus, before this change was made to the sensor
// logic, instrumented containers would have non-identical ENV list.
app.Env = os.Environ()
}
app.Dir = appDir
app.Stdin = os.Stdin
app.Stdout = appStdout
app.Stderr = appStderr
err := app.Start()
if err != nil {
log.Warnf("launcher.Start: error - %v", err)
@ -149,3 +160,45 @@ func Start(
log.Debugf("launcher.Start: started target app --> PID=%d", app.Process.Pid)
return app, nil
}
func appEnv(appUserHome string) (appEnv []string) {
// Another attempt to make the sensor's presence invisible to the target app.
// Instrumented containers must be started as "root". But it makes Docker
// (and other container runtimes) setting the HOME env var accordingly
// (typically, using "/root" if /etc/passwd record exists or defaulting to "/"
// otherwise to stay POSIX-conformant). However, when the target app needs
// to be run as `appUser` != "root", the HOME env var may very well be different.
// So we need to restore it by reading the corresponding /etc/passwd record.
//
// Note that the above logic is applicable only to the HOME env var. Other
// typical env vars like USER or PATH don't need to be restored. Container
// runtimes typically don't touch the USER var and almost always do set
// the PATH var explicitly (during image building). So we just (implicitly)
// propagate these values to app.Env from os.Environ().
sensorUser, err := user.Current()
if err != nil {
log.WithError(err).Error("launcher.Start: couldn't get current user")
return os.Environ()
}
for _, e := range os.Environ() {
if !strings.HasPrefix(e, "HOME=") {
appEnv = append(appEnv, e) // Just copy everything... except HOME.
continue
}
if "HOME="+sensorUser.HomeDir == e {
// Since current HOME var is equal to the sensor's user HomeDir,
// it's highly likely it wasn't set explicitly in the `docker run`
// command (or alike) and instead was "computed" by the runtime upon
// launching the container. Since the target app user != sensor's user,
// we need to "recompute" it.
appEnv = append(appEnv, "HOME="+appUserHome)
} else {
appEnv = append(appEnv, e) // Likely the HOME var was set explicitly - don't mess with it.
}
}
return appEnv
}

View File

@ -22,31 +22,31 @@ type DistroInfo struct {
DisplayName string `json:"display_name"`
}
func ResolveUser(identity string) (uint32, uint32, error) {
func ResolveUser(identity string) (uid uint32, gid uint32, home string, err error) {
var userInfo *user.User
if _, err := strconv.ParseUint(identity, 10, 32); err == nil {
userInfo, err = user.LookupId(identity)
if err != nil {
return 0, 0, err
return 0, 0, "", err
}
} else {
userInfo, err = user.Lookup(identity)
if err != nil {
return 0, 0, err
return 0, 0, "", err
}
}
uid, err := strconv.ParseUint(userInfo.Uid, 10, 32)
uid64, err := strconv.ParseUint(userInfo.Uid, 10, 32)
if err != nil {
return 0, 0, err
return 0, 0, "", err
}
gid, err := strconv.ParseUint(userInfo.Gid, 10, 32)
gid64, err := strconv.ParseUint(userInfo.Gid, 10, 32)
if err != nil {
return 0, 0, err
return 0, 0, "", err
}
return uint32(uid), uint32(gid), nil
return uint32(uid64), uint32(gid64), userInfo.HomeDir, nil
}
func ResolveGroup(identity string) (uint32, error) {

View File

@ -5,9 +5,9 @@ import (
"github.com/docker-slim/docker-slim/pkg/ipc/command"
)
type startMonitorOpt func(*command.StartMonitor)
type StartMonitorOpt func(*command.StartMonitor)
func WithSaneDefaults() startMonitorOpt {
func WithSaneDefaults() StartMonitorOpt {
return func(cmd *command.StartMonitor) {
cmd.RTASourcePT = true
cmd.KeepPerms = true
@ -20,39 +20,39 @@ func WithSaneDefaults() startMonitorOpt {
}
}
func WithAppNameArgs(name string, arg ...string) startMonitorOpt {
func WithAppNameArgs(name string, arg ...string) StartMonitorOpt {
return func(cmd *command.StartMonitor) {
cmd.AppName = name
cmd.AppArgs = arg
}
}
func WithAppUser(user string) startMonitorOpt {
func WithAppUser(user string) StartMonitorOpt {
return func(cmd *command.StartMonitor) {
cmd.AppUser = user
cmd.RunTargetAsUser = true
}
}
func WithAppStdoutToFile() startMonitorOpt {
func WithAppStdoutToFile() StartMonitorOpt {
return func(cmd *command.StartMonitor) {
cmd.AppStdoutToFile = true
}
}
func WithAppStderrToFile() startMonitorOpt {
func WithAppStderrToFile() StartMonitorOpt {
return func(cmd *command.StartMonitor) {
cmd.AppStderrToFile = true
}
}
func WithPreserves(path ...string) startMonitorOpt {
func WithPreserves(path ...string) StartMonitorOpt {
return func(cmd *command.StartMonitor) {
cmd.Preserves = commands.ParsePaths(path)
}
}
func NewMonitorStartCommand(opts ...startMonitorOpt) command.StartMonitor {
func NewMonitorStartCommand(opts ...StartMonitorOpt) command.StartMonitor {
cmd := command.StartMonitor{}
for _, opt := range opts {

View File

@ -5,7 +5,7 @@ GO_TEST_FLAGS = # E.g.: make test-e2e-sensor GO_TEST_FLAGS='-run TestXyz'
# run sensor only e2e tests
test-e2e-sensor:
go test -v -tags e2e -count 20 -timeout 30m $(GO_TEST_FLAGS) $(CURDIR)/pkg/app/sensor
go test -v -tags e2e -count 10 -timeout 30m $(GO_TEST_FLAGS) $(CURDIR)/pkg/app/sensor
# run all e2e tests at once
.PHONY: