Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
175 changes: 175 additions & 0 deletions environment/sudo.go
Original file line number Diff line number Diff line change
@@ -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
}
29 changes: 29 additions & 0 deletions network/linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import (
"os/exec"
"syscall"
"time"

"github.com/coder/jail/environment"
)

const (
Expand Down Expand Up @@ -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))
Expand All @@ -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()
Expand Down
42 changes: 36 additions & 6 deletions network/macos.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import (
"strconv"
"strings"
"syscall"

"github.com/coder/jail/environment"
)

const (
Expand Down Expand Up @@ -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))
Expand All @@ -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
Expand Down Expand Up @@ -334,4 +364,4 @@ func (m *MacOSNetJail) cleanupTempFiles() {
if m.mainRulesPath != "" {
os.Remove(m.mainRulesPath)
}
}
}
26 changes: 23 additions & 3 deletions tls/tls.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"math/big"
"net"
"os"
"os/user"
"path/filepath"
"sync"
"time"
Expand Down Expand Up @@ -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
Expand Down
Loading