diff --git a/environment/sudo.go b/environment/sudo.go new file mode 100644 index 0000000..afd196d --- /dev/null +++ b/environment/sudo.go @@ -0,0 +1,175 @@ +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 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 validUserPaths []string + for _, path := range userPaths { + if _, err := os.Stat(path); err == nil { + validUserPaths = append(validUserPaths, path) + logger.Debug("Found user path", "path", path) + } + } + + // 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 { + 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) + } + } + } + } + + // 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 +} + +// 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..9e22c80 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)) @@ -102,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 79f8097..a4927c2 100644 --- a/network/macos.go +++ b/network/macos.go @@ -10,6 +10,8 @@ import ( "strconv" "strings" "syscall" + + "github.com/coder/jail/environment" ) const ( @@ -79,6 +81,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)) @@ -89,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 @@ -334,4 +364,4 @@ func (m *MacOSNetJail) cleanupTempFiles() { if m.mainRulesPath != "" { os.Remove(m.mainRulesPath) } -} +} \ No newline at end of file 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