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.AssertSensorLogsContain(t, ctx, sensorFullLifecycleSequence...)
|
||||||
sensor.AssertTargetAppLogsContain(t, ctx, "daemon")
|
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"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"os/user"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
@ -100,6 +101,11 @@ func Start(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
app.Dir = appDir
|
||||||
|
app.Stdin = os.Stdin
|
||||||
|
app.Stdout = appStdout
|
||||||
|
app.Stderr = appStderr
|
||||||
|
|
||||||
if appUser != "" {
|
if appUser != "" {
|
||||||
if app.SysProcAttr == nil {
|
if app.SysProcAttr == nil {
|
||||||
app.SysProcAttr = &syscall.SysProcAttr{}
|
app.SysProcAttr = &syscall.SysProcAttr{}
|
||||||
@ -107,7 +113,7 @@ func Start(
|
|||||||
|
|
||||||
appUserParts := strings.Split(appUser, ":")
|
appUserParts := strings.Split(appUser, ":")
|
||||||
if len(appUserParts) > 0 {
|
if len(appUserParts) > 0 {
|
||||||
uid, gid, err := system.ResolveUser(appUserParts[0])
|
uid, gid, home, err := system.ResolveUser(appUserParts[0])
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if len(appUserParts) > 1 {
|
if len(appUserParts) > 1 {
|
||||||
xgid, err := system.ResolveGroup(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)
|
log.Errorf("launcher.Start: error fixing i/o perms for user (%v/%v) - %v", appUser, uid, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
app.Env = appEnv(home)
|
||||||
} else {
|
} else {
|
||||||
log.Errorf("launcher.Start: error resolving user identity (%v/%v) - %v", appUser, appUserParts[0], err)
|
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()
|
err := app.Start()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warnf("launcher.Start: error - %v", err)
|
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)
|
log.Debugf("launcher.Start: started target app --> PID=%d", app.Process.Pid)
|
||||||
return app, nil
|
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"`
|
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
|
var userInfo *user.User
|
||||||
if _, err := strconv.ParseUint(identity, 10, 32); err == nil {
|
if _, err := strconv.ParseUint(identity, 10, 32); err == nil {
|
||||||
userInfo, err = user.LookupId(identity)
|
userInfo, err = user.LookupId(identity)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, 0, err
|
return 0, 0, "", err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
userInfo, err = user.Lookup(identity)
|
userInfo, err = user.Lookup(identity)
|
||||||
if err != nil {
|
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 {
|
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 {
|
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) {
|
func ResolveGroup(identity string) (uint32, error) {
|
||||||
|
@ -5,9 +5,9 @@ import (
|
|||||||
"github.com/docker-slim/docker-slim/pkg/ipc/command"
|
"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) {
|
return func(cmd *command.StartMonitor) {
|
||||||
cmd.RTASourcePT = true
|
cmd.RTASourcePT = true
|
||||||
cmd.KeepPerms = 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) {
|
return func(cmd *command.StartMonitor) {
|
||||||
cmd.AppName = name
|
cmd.AppName = name
|
||||||
cmd.AppArgs = arg
|
cmd.AppArgs = arg
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func WithAppUser(user string) startMonitorOpt {
|
func WithAppUser(user string) StartMonitorOpt {
|
||||||
return func(cmd *command.StartMonitor) {
|
return func(cmd *command.StartMonitor) {
|
||||||
cmd.AppUser = user
|
cmd.AppUser = user
|
||||||
cmd.RunTargetAsUser = true
|
cmd.RunTargetAsUser = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func WithAppStdoutToFile() startMonitorOpt {
|
func WithAppStdoutToFile() StartMonitorOpt {
|
||||||
return func(cmd *command.StartMonitor) {
|
return func(cmd *command.StartMonitor) {
|
||||||
cmd.AppStdoutToFile = true
|
cmd.AppStdoutToFile = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func WithAppStderrToFile() startMonitorOpt {
|
func WithAppStderrToFile() StartMonitorOpt {
|
||||||
return func(cmd *command.StartMonitor) {
|
return func(cmd *command.StartMonitor) {
|
||||||
cmd.AppStderrToFile = true
|
cmd.AppStderrToFile = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func WithPreserves(path ...string) startMonitorOpt {
|
func WithPreserves(path ...string) StartMonitorOpt {
|
||||||
return func(cmd *command.StartMonitor) {
|
return func(cmd *command.StartMonitor) {
|
||||||
cmd.Preserves = commands.ParsePaths(path)
|
cmd.Preserves = commands.ParsePaths(path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMonitorStartCommand(opts ...startMonitorOpt) command.StartMonitor {
|
func NewMonitorStartCommand(opts ...StartMonitorOpt) command.StartMonitor {
|
||||||
cmd := command.StartMonitor{}
|
cmd := command.StartMonitor{}
|
||||||
|
|
||||||
for _, opt := range opts {
|
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
|
# run sensor only e2e tests
|
||||||
test-e2e-sensor:
|
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
|
# run all e2e tests at once
|
||||||
.PHONY:
|
.PHONY:
|
||||||
|
Loading…
Reference in New Issue
Block a user