Files
netbird/client/internal/profilemanager/invoking_user.go
T
Nicolas Frati 15a684248c [client] Support arbitrary UIDs in rootless image (#7440)
* [client] Support arbitrary UIDs in rootless image

* [client] Keep rootless executables root-owned

* [client] Harden arbitrary UID image validation

* [client] Preserve executable access in rootless image

Keep the binary and entrypoint executable when deployments override the runtime group. Retain root ownership so non-root users cannot modify either file.

* [client] Verify rootless state reuse with a stable UID

Persisted profiles remain scoped to the creating UID. Verify same-UID container recreation without broadening application permissions, and document the Kubernetes volume permission behavior observed on OpenShift. Remove unused synthetic-user home metadata.

* [client] Separate image changes from invoking user fix

Keep this PR limited to resolving unmapped non-root invoking users. Move container permissions and their smoke test to a dependent image branch so they can be reviewed separately.

* [client] Restore invoking process user test

Retain coverage for successful current-user lookup without sudo. Numeric-identity fallback tests do not cover this existing behavior.
2026-09-10 12:02:19 +02:00

122 lines
3.9 KiB
Go

package profilemanager
import (
"fmt"
"os"
"os/user"
"path/filepath"
"runtime"
"strconv"
log "github.com/sirupsen/logrus"
)
const envSudoUser = "SUDO_USER"
var (
currentUser = user.Current
getegid = os.Getegid
geteuid = os.Geteuid
lookupUser = user.Lookup
)
// InvokingUser returns the user a CLI invocation acts for. Under sudo that is
// the user who ran sudo, not root: privileged flags force commands through
// sudo, and resolving profiles as root would silently switch the daemon to
// root's (default) profile instead of the invoking user's. An unmapped positive
// process UID uses its numeric kernel identity; root, sudo lookup failures, and
// unavailable platform identities still fail closed. Privilege decisions stay
// on the kernel credentials of the daemon connection, which SUDO_USER (a plain
// environment variable) can never influence; a forged value only selects a
// profile root could select anyway.
func InvokingUser() (*user.User, error) {
if u, ok := sudoInvokingUser(); ok {
return u, nil
}
// Fail closed instead of falling through to root: every caller feeds this
// username into profile-path resolution, so a lookup failure would resolve
// (and create) a root-owned profile namespace and switch the daemon onto it
// behind the invoking user's back.
if sudoActive() {
return nil, fmt.Errorf("resolve sudo invoking user %q: refusing to fall back to root", os.Getenv(envSudoUser))
}
u, err := currentUser()
if err == nil {
return u, nil
}
uid := geteuid()
if uid <= 0 {
return nil, err
}
log.Debugf("current user lookup for UID %d: %v; using numeric UID", uid, err)
uidString := strconv.Itoa(uid)
return &user.User{
Username: uidString,
Uid: uidString,
Gid: strconv.Itoa(getegid()),
}, nil
}
// IsPlainRoot reports that the process runs as root with no usable sudo
// context: there is no invoking user to act for, so per-user resolution falls
// back to root's own (empty) state. Callers use it to refuse ambiguous
// operations instead of silently acting on the wrong profile.
func IsPlainRoot() bool {
if geteuid() != 0 {
return false
}
_, ok := sudoInvokingUser()
return !ok
}
// MirrorIsAuthoritative reports whether the invoking user's local
// active-profile mirror can be trusted as the profile selector. It cannot under
// sudo (writes to it are skipped, so it goes stale) or as plain root (there is
// no invoking user, so it falls back to root's own default). Callers use it to
// decide whether to read the profile from the mirror or from the daemon.
func MirrorIsAuthoritative() bool {
return !sudoActive() && !IsPlainRoot()
}
// sudoInvokingUser resolves SUDO_USER when the process runs as root under
// sudo. Returns false whenever the sudo context is absent or unusable, in
// which case callers fall back to the process user.
func sudoInvokingUser() (*user.User, bool) {
if !sudoActive() {
return nil, false
}
name := os.Getenv(envSudoUser)
u, err := lookupUser(name)
if err != nil {
log.Warnf("sudo invoking user %q lookup: %v", name, err)
return nil, false
}
return u, true
}
// sudoActive reports a sudo context from the environment alone: write-skip
// decisions key off it so a transient user lookup failure can never flip a
// run from read-only to writing root-owned files into the user's directory.
func sudoActive() bool {
if geteuid() != 0 {
return false
}
name := os.Getenv(envSudoUser)
return name != "" && name != "root"
}
// userBaseConfigDir mirrors os.UserConfigDir for a user other than the process
// owner. Environment overrides (XDG_CONFIG_HOME) cannot be honoured here: under
// sudo the environment is root's, not the invoking user's.
func userBaseConfigDir(u *user.User) (string, error) {
if u.HomeDir == "" {
return "", fmt.Errorf("user %s has no home directory", u.Username)
}
if runtime.GOOS == "darwin" {
return filepath.Join(u.HomeDir, "Library", "Application Support"), nil
}
return filepath.Join(u.HomeDir, ".config"), nil
}