From 60ffe201d5b27bda620d9c00904c2b342da6452c Mon Sep 17 00:00:00 2001 From: "blink-so[bot]" <211532188+blink-so[bot]@users.noreply.github.com> Date: Tue, 9 Sep 2025 19:26:16 +0000 Subject: [PATCH 1/5] feat: preserve original user environment when running under sudo When jail is executed with sudo, the subprocess now receives the original user's environment variables (USER, HOME, PATH, XDG directories) instead of root's environment. This ensures that tools and applications behave as if they were run by the original user. Changes: - Add environment package with sudo detection and user environment restoration - Update Linux jail implementation to restore user environment - Update macOS jail implementation to restore user environment - Preserve important variables: USER, LOGNAME, HOME, PATH, XDG_* directories - Maintain backward compatibility for non-sudo execution Co-authored-by: f0ssel <19379394+f0ssel@users.noreply.github.com> --- environment/sudo.go | 148 ++++++++++++++++++++++++++++++++++++++++++++ network/linux.go | 8 +++ network/macos.go | 11 +++- 3 files changed, 166 insertions(+), 1 deletion(-) create mode 100644 environment/sudo.go diff --git a/environment/sudo.go b/environment/sudo.go new file mode 100644 index 0000000..e2d1e2e --- /dev/null +++ b/environment/sudo.go @@ -0,0 +1,148 @@ +package environment + +import ( + "fmt" + "log/slog" + "os" + "os/user" + "path/filepath" + "strconv" + "strings" +) + +// RestoreOriginalUserEnvironment detects if running under sudo and restores +// the original user's environment variables that are important for subprocess execution. +func RestoreOriginalUserEnvironment(logger *slog.Logger) map[string]string { + restoredEnv := make(map[string]string) + + // Check if running under sudo by looking for SUDO_USER + sudoUser := os.Getenv("SUDO_USER") + if sudoUser == "" { + logger.Debug("Not running under sudo, no environment restoration needed") + return restoredEnv + } + + logger.Debug("Detected sudo execution, restoring original user environment", "sudo_user", sudoUser) + + // Get original user information + originalUser, err := user.Lookup(sudoUser) + if err != nil { + logger.Warn("Failed to lookup original user, skipping environment restoration", "sudo_user", sudoUser, "error", err) + return restoredEnv + } + + // Restore basic user identity variables + restoredEnv["USER"] = sudoUser + restoredEnv["LOGNAME"] = sudoUser + restoredEnv["HOME"] = originalUser.HomeDir + + // Restore original user's UID and GID if available + if sudoUID := os.Getenv("SUDO_UID"); sudoUID != "" { + restoredEnv["UID"] = sudoUID + } + if sudoGID := os.Getenv("SUDO_GID"); sudoGID != "" { + restoredEnv["GID"] = sudoGID + } + + // Try to restore a reasonable PATH for the original user + // This is a best-effort attempt since the original PATH might be complex + restoredPath := restoreUserPath(originalUser, logger) + if restoredPath != "" { + restoredEnv["PATH"] = restoredPath + } + + // Restore XDG directories for the original user + restoreXDGEnvironment(originalUser, restoredEnv) + + // Log what we're restoring + logger.Debug("Restored environment variables for original user", + "user", sudoUser, + "home", originalUser.HomeDir, + "restored_vars", len(restoredEnv)) + + return restoredEnv +} + +// restoreUserPath attempts to construct a reasonable PATH for the original user +func restoreUserPath(originalUser *user.User, logger *slog.Logger) string { + // Start with common system paths + systemPaths := []string{ + "/usr/local/bin", + "/usr/bin", + "/bin", + } + + // Add user-specific paths + userPaths := []string{ + filepath.Join(originalUser.HomeDir, ".local", "bin"), + filepath.Join(originalUser.HomeDir, "bin"), + } + + // Check if user paths exist and add them + var validPaths []string + for _, path := range userPaths { + if _, err := os.Stat(path); err == nil { + validPaths = append(validPaths, path) + } + } + + // Combine user paths + system paths + allPaths := append(validPaths, systemPaths...) + + // Also try to preserve some paths from current PATH that might be user-specific + currentPath := os.Getenv("PATH") + if currentPath != "" { + for _, path := range strings.Split(currentPath, ":") { + // Include paths that contain the user's home directory + if strings.Contains(path, originalUser.HomeDir) { + if _, err := os.Stat(path); err == nil { + allPaths = append([]string{path}, allPaths...) + } + } + } + } + + restoredPath := strings.Join(allPaths, ":") + logger.Debug("Restored PATH for user", "user", originalUser.Username, "path", restoredPath) + return restoredPath +} + +// restoreXDGEnvironment restores XDG Base Directory variables for the original user +func restoreXDGEnvironment(originalUser *user.User, restoredEnv map[string]string) { + homeDir := originalUser.HomeDir + + // Set XDG directories according to the specification + // https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html + restoredEnv["XDG_DATA_HOME"] = filepath.Join(homeDir, ".local", "share") + restoredEnv["XDG_CONFIG_HOME"] = filepath.Join(homeDir, ".config") + restoredEnv["XDG_STATE_HOME"] = filepath.Join(homeDir, ".local", "state") + restoredEnv["XDG_CACHE_HOME"] = filepath.Join(homeDir, ".cache") + + // XDG_RUNTIME_DIR is typically /run/user/{uid} but we'll leave it as-is + // since it requires the actual UID and proper permissions +} + +// GetEffectiveUID returns the effective UID that should be used for the subprocess +// This helps determine if we need to drop privileges when running under sudo +func GetEffectiveUID() (int, error) { + if sudoUID := os.Getenv("SUDO_UID"); sudoUID != "" { + uid, err := strconv.Atoi(sudoUID) + if err != nil { + return 0, fmt.Errorf("invalid SUDO_UID: %v", err) + } + return uid, nil + } + return os.Getuid(), nil +} + +// GetEffectiveGID returns the effective GID that should be used for the subprocess +func GetEffectiveGID() (int, error) { + if sudoGID := os.Getenv("SUDO_GID"); sudoGID != "" { + gid, err := strconv.Atoi(sudoGID) + if err != nil { + return 0, fmt.Errorf("invalid SUDO_GID: %v", err) + } + return gid, nil + } + return os.Getgid(), nil +} diff --git a/network/linux.go b/network/linux.go index 82eacc2..d9a3c3f 100644 --- a/network/linux.go +++ b/network/linux.go @@ -9,6 +9,8 @@ import ( "os/exec" "syscall" "time" + + "github.com/coder/jail/environment" ) const ( @@ -92,6 +94,12 @@ func (l *LinuxJail) Execute(command []string, extraEnv map[string]string) error l.logger.Debug("Setting up environment") env := os.Environ() + // Restore original user environment if running under sudo + restoredUserEnv := environment.RestoreOriginalUserEnvironment(l.logger) + for key, value := range restoredUserEnv { + env = append(env, fmt.Sprintf("%s=%s", key, value)) + } + // Add extra environment variables (including CA cert if provided) for key, value := range extraEnv { env = append(env, fmt.Sprintf("%s=%s", key, value)) diff --git a/network/macos.go b/network/macos.go index 79f8097..2e1da9e 100644 --- a/network/macos.go +++ b/network/macos.go @@ -10,6 +10,9 @@ import ( "strconv" "strings" "syscall" + "time" + + "github.com/coder/jail/environment" ) const ( @@ -79,6 +82,12 @@ func (m *MacOSNetJail) Execute(command []string, extraEnv map[string]string) err m.logger.Debug("Setting up environment") env := os.Environ() + // Restore original user environment if running under sudo + restoredUserEnv := environment.RestoreOriginalUserEnvironment(m.logger) + for key, value := range restoredUserEnv { + env = append(env, fmt.Sprintf("%s=%s", key, value)) + } + // Add extra environment variables (including CA cert if provided) for key, value := range extraEnv { env = append(env, fmt.Sprintf("%s=%s", key, value)) @@ -334,4 +343,4 @@ func (m *MacOSNetJail) cleanupTempFiles() { if m.mainRulesPath != "" { os.Remove(m.mainRulesPath) } -} +} \ No newline at end of file From 8f25301df442c9f57e7509de06bfd044f92883cc Mon Sep 17 00:00:00 2001 From: "blink-so[bot]" <211532188+blink-so[bot]@users.noreply.github.com> Date: Tue, 9 Sep 2025 19:28:02 +0000 Subject: [PATCH 2/5] fix: remove unused time import from macOS network implementation The time import was accidentally added during the environment restoration implementation but is not actually used in the macOS network code. Fixes CI build failure on macOS. Co-authored-by: f0ssel <19379394+f0ssel@users.noreply.github.com> --- network/macos.go | 1 - 1 file changed, 1 deletion(-) diff --git a/network/macos.go b/network/macos.go index 2e1da9e..9e82203 100644 --- a/network/macos.go +++ b/network/macos.go @@ -10,7 +10,6 @@ import ( "strconv" "strings" "syscall" - "time" "github.com/coder/jail/environment" ) From 97839b9f9d25abc3d1870730be2ec37e6ac4446d Mon Sep 17 00:00:00 2001 From: "blink-so[bot]" <211532188+blink-so[bot]@users.noreply.github.com> Date: Tue, 9 Sep 2025 19:30:51 +0000 Subject: [PATCH 3/5] improve: enhance PATH restoration for sudo environment preservation Improved the PATH restoration logic to be more comprehensive: - Added more system paths including sbin directories - Added common user tool paths (Rust, Go, npm global) - Better preservation of tool paths from current PATH (/opt, /snap/bin) - Added duplicate removal while preserving path order - Enhanced debug logging for PATH restoration process This should help resolve issues where user-installed tools like 'claude' are not found when running jail with sudo. Co-authored-by: f0ssel <19379394+f0ssel@users.noreply.github.com> --- environment/sudo.go | 45 ++++++++++++++++++++++++++++++++++++--------- 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/environment/sudo.go b/environment/sudo.go index e2d1e2e..afd196d 100644 --- a/environment/sudo.go +++ b/environment/sudo.go @@ -65,44 +65,71 @@ func RestoreOriginalUserEnvironment(logger *slog.Logger) map[string]string { // restoreUserPath attempts to construct a reasonable PATH for the original user func restoreUserPath(originalUser *user.User, logger *slog.Logger) string { - // Start with common system paths + // Start with comprehensive system paths (in order of preference) systemPaths := []string{ "/usr/local/bin", "/usr/bin", "/bin", + "/usr/local/sbin", + "/usr/sbin", + "/sbin", } // Add user-specific paths userPaths := []string{ filepath.Join(originalUser.HomeDir, ".local", "bin"), filepath.Join(originalUser.HomeDir, "bin"), + filepath.Join(originalUser.HomeDir, ".cargo", "bin"), // Rust tools + filepath.Join(originalUser.HomeDir, "go", "bin"), // Go tools + filepath.Join(originalUser.HomeDir, ".npm-global", "bin"), // npm global tools } // Check if user paths exist and add them - var validPaths []string + var validUserPaths []string for _, path := range userPaths { if _, err := os.Stat(path); err == nil { - validPaths = append(validPaths, path) + validUserPaths = append(validUserPaths, path) + logger.Debug("Found user path", "path", path) } } - // Combine user paths + system paths - allPaths := append(validPaths, systemPaths...) - - // Also try to preserve some paths from current PATH that might be user-specific + // Try to preserve paths from current PATH that might be user-specific or important + var preservedPaths []string currentPath := os.Getenv("PATH") if currentPath != "" { for _, path := range strings.Split(currentPath, ":") { // Include paths that contain the user's home directory if strings.Contains(path, originalUser.HomeDir) { if _, err := os.Stat(path); err == nil { - allPaths = append([]string{path}, allPaths...) + preservedPaths = append(preservedPaths, path) + logger.Debug("Preserved user-specific path from current PATH", "path", path) + } + } + // Also preserve common tool paths that might not be in system paths + if strings.Contains(path, "/opt/") || strings.Contains(path, "/snap/bin") { + if _, err := os.Stat(path); err == nil { + preservedPaths = append(preservedPaths, path) + logger.Debug("Preserved tool path from current PATH", "path", path) } } } } - restoredPath := strings.Join(allPaths, ":") + // Combine all paths: preserved user paths + valid user paths + system paths + allPaths := append(preservedPaths, validUserPaths...) + allPaths = append(allPaths, systemPaths...) + + // Remove duplicates while preserving order + seen := make(map[string]bool) + var uniquePaths []string + for _, path := range allPaths { + if !seen[path] { + seen[path] = true + uniquePaths = append(uniquePaths, path) + } + } + + restoredPath := strings.Join(uniquePaths, ":") logger.Debug("Restored PATH for user", "user", originalUser.Username, "path", restoredPath) return restoredPath } From dcd0450039bf001719889900bb7480d28a9b7c85 Mon Sep 17 00:00:00 2001 From: "blink-so[bot]" <211532188+blink-so[bot]@users.noreply.github.com> Date: Tue, 9 Sep 2025 19:34:51 +0000 Subject: [PATCH 4/5] fix: implement privilege dropping to run subprocess as original user This is the core fix for sudo environment preservation. Previously, we were only restoring environment variables but the subprocess was still running as root. Now we properly drop privileges to the original user. Changes: - Linux: Use syscall.Credential to set UID/GID of subprocess - macOS: Use syscall.Credential to set UID/GID, preserve original group behavior for non-sudo - Both platforms now check SUDO_USER and drop privileges accordingly - Added proper error handling and debug logging for privilege dropping Now 'sudo jail -- whoami' will return the original username instead of 'root'. Co-authored-by: f0ssel <19379394+f0ssel@users.noreply.github.com> --- network/linux.go | 21 +++++++++++++++++++++ network/macos.go | 32 +++++++++++++++++++++++++++----- 2 files changed, 48 insertions(+), 5 deletions(-) diff --git a/network/linux.go b/network/linux.go index d9a3c3f..9e22c80 100644 --- a/network/linux.go +++ b/network/linux.go @@ -110,6 +110,27 @@ func (l *LinuxJail) Execute(command []string, extraEnv map[string]string) error cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr + // Drop privileges to original user if running under sudo + if sudoUser := os.Getenv("SUDO_USER"); sudoUser != "" { + uid, err := environment.GetEffectiveUID() + if err != nil { + l.logger.Warn("Failed to get effective UID, subprocess will run as root", "error", err) + } else { + gid, err := environment.GetEffectiveGID() + if err != nil { + l.logger.Warn("Failed to get effective GID, subprocess will run as root", "error", err) + } else { + cmd.SysProcAttr = &syscall.SysProcAttr{ + Credential: &syscall.Credential{ + Uid: uint32(uid), + Gid: uint32(gid), + }, + } + l.logger.Debug("Dropping privileges to original user", "uid", uid, "gid", gid, "user", sudoUser) + } + } + } + // Start command l.logger.Debug("Starting command", "path", cmd.Path, "args", cmd.Args) err := cmd.Start() diff --git a/network/macos.go b/network/macos.go index 9e82203..a4927c2 100644 --- a/network/macos.go +++ b/network/macos.go @@ -97,11 +97,33 @@ func (m *MacOSNetJail) Execute(command []string, extraEnv map[string]string) err cmd.Stderr = os.Stderr cmd.Stdin = os.Stdin - // Set group ID using syscall (like httpjail does) - cmd.SysProcAttr = &syscall.SysProcAttr{ - Credential: &syscall.Credential{ - Gid: uint32(m.groupID), - }, + // Drop privileges to original user if running under sudo + if sudoUser := os.Getenv("SUDO_USER"); sudoUser != "" { + uid, err := environment.GetEffectiveUID() + if err != nil { + m.logger.Warn("Failed to get effective UID, subprocess will run as root", "error", err) + } else { + gid, err := environment.GetEffectiveGID() + if err != nil { + m.logger.Warn("Failed to get effective GID, subprocess will run as root", "error", err) + } else { + // Set group ID using syscall (like httpjail does) + cmd.SysProcAttr = &syscall.SysProcAttr{ + Credential: &syscall.Credential{ + Uid: uint32(uid), + Gid: uint32(gid), + }, + } + m.logger.Debug("Dropping privileges to original user", "uid", uid, "gid", gid, "user", sudoUser) + } + } + } else { + // Set group ID using syscall (like httpjail does) - original behavior for non-sudo + cmd.SysProcAttr = &syscall.SysProcAttr{ + Credential: &syscall.Credential{ + Gid: uint32(m.groupID), + }, + } } // Start and wait for command to complete From 0383b569bbba0ecbf531440c6263365a948f41a6 Mon Sep 17 00:00:00 2001 From: "blink-so[bot]" <211532188+blink-so[bot]@users.noreply.github.com> Date: Tue, 9 Sep 2025 19:41:13 +0000 Subject: [PATCH 5/5] fix: use original user's home directory for CA certificate storage When running under sudo, the CA certificate was being stored in root's home directory (/root/.config/boundary/) but the subprocess running as the original user couldn't access it, causing certificate verification errors. Now GetConfigDir() detects sudo execution and uses the original user's home directory, so the CA certificate is stored in a location accessible to the subprocess. Fixes curl errors like: 'error setting certificate verify locations: CAfile: /Users/user/.config/boundary/ca-cert.pem' Co-authored-by: f0ssel <19379394+f0ssel@users.noreply.github.com> --- tls/tls.go | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/tls/tls.go b/tls/tls.go index a9e51b0..8f66040 100644 --- a/tls/tls.go +++ b/tls/tls.go @@ -12,6 +12,7 @@ import ( "math/big" "net" "os" + "os/user" "path/filepath" "sync" "time" @@ -295,9 +296,28 @@ func (cm *CertificateManager) generateServerCertificate(hostname string) (*tls.C // GetConfigDir returns the configuration directory path func GetConfigDir() (string, error) { - homeDir, err := os.UserHomeDir() - if err != nil { - return "", fmt.Errorf("failed to get user home directory: %v", err) + // When running under sudo, use the original user's home directory + // so the subprocess can access the CA certificate files + var homeDir string + if sudoUser := os.Getenv("SUDO_USER"); sudoUser != "" { + // Get original user's home directory + if user, err := user.Lookup(sudoUser); err == nil { + homeDir = user.HomeDir + } else { + // Fallback to current user if lookup fails + var err2 error + homeDir, err2 = os.UserHomeDir() + if err2 != nil { + return "", fmt.Errorf("failed to get user home directory: %v", err2) + } + } + } else { + // Normal case - use current user's home + var err error + homeDir, err = os.UserHomeDir() + if err != nil { + return "", fmt.Errorf("failed to get user home directory: %v", err) + } } // Use platform-specific config directory