mirror of
https://github.com/slimtoolkit/slim.git
synced 2025-06-03 04:00:23 +00:00
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:
parent
00c52c60a1
commit
1bd42e2af4
@ -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)
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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 {
|
||||
|
@ -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:
|
||||
|
Loading…
Reference in New Issue
Block a user