From 4d50f366ff6e5f3a46e35e22d18656038cc4f0a1 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:44:14 +0000 Subject: [PATCH 1/7] fix: drop privileges to original user when running with sudo When jail is executed with sudo, the subprocess now runs as the original user instead of root. This is a minimal implementation that only handles privilege dropping without environment manipulation. Changes: - Linux: Check SUDO_UID/SUDO_GID and use syscall.Credential to drop privileges - macOS: Same privilege dropping logic, preserve original group behavior for non-sudo - Added proper error handling and debug logging Now 'sudo jail -- whoami' returns the original username instead of 'root'. Co-authored-by: f0ssel <19379394+f0ssel@users.noreply.github.com> --- network/linux.go | 24 ++++++++++++++++++++++++ network/macos.go | 35 +++++++++++++++++++++++++++++------ 2 files changed, 53 insertions(+), 6 deletions(-) diff --git a/network/linux.go b/network/linux.go index 82eacc2..9dee54d 100644 --- a/network/linux.go +++ b/network/linux.go @@ -7,6 +7,7 @@ import ( "log/slog" "os" "os/exec" + "strconv" "syscall" "time" ) @@ -102,6 +103,29 @@ 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 sudoUID := os.Getenv("SUDO_UID"); sudoUID != "" { + if sudoGID := os.Getenv("SUDO_GID"); sudoGID != "" { + uid, err := strconv.Atoi(sudoUID) + if err != nil { + l.logger.Warn("Invalid SUDO_UID, subprocess will run as root", "sudo_uid", sudoUID, "error", err) + } else { + gid, err := strconv.Atoi(sudoGID) + if err != nil { + l.logger.Warn("Invalid SUDO_GID, subprocess will run as root", "sudo_gid", sudoGID, "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) + } + } + } + } + // 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..3631a80 100644 --- a/network/macos.go +++ b/network/macos.go @@ -89,11 +89,34 @@ 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 sudoUID := os.Getenv("SUDO_UID"); sudoUID != "" { + if sudoGID := os.Getenv("SUDO_GID"); sudoGID != "" { + uid, err := strconv.Atoi(sudoUID) + if err != nil { + m.logger.Warn("Invalid SUDO_UID, subprocess will run as root", "sudo_uid", sudoUID, "error", err) + } else { + gid, err := strconv.Atoi(sudoGID) + if err != nil { + m.logger.Warn("Invalid SUDO_GID, subprocess will run as root", "sudo_gid", sudoGID, "error", err) + } else { + 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) + } + } + } + } else { + // Set group ID using syscall (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 +357,4 @@ func (m *MacOSNetJail) cleanupTempFiles() { if m.mainRulesPath != "" { os.Remove(m.mainRulesPath) } -} +} \ No newline at end of file From fbe3a810ddc88684dfb6ce08ec1e615d2b09fe6d 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:57:44 +0000 Subject: [PATCH 2/7] 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 but the subprocess (running as original user) couldn't access it, causing certificate verification errors. Now GetConfigDir() detects sudo execution and uses the original user's home directory, ensuring the subprocess can access the CA certificate. Fixes: curl: (77) error setting certificate verify locations 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 From 4e9ba4011162b952e6b9f3dd5428ea7306fb8682 Mon Sep 17 00:00:00 2001 From: "blink-so[bot]" <211532188+blink-so[bot]@users.noreply.github.com> Date: Tue, 9 Sep 2025 20:01:12 +0000 Subject: [PATCH 3/7] fix: ensure proper directory ownership for CA certificate storage When running under sudo, the config directory was created by root but the subprocess runs as the original user, causing permission issues. Now the directory ownership is changed to the original user after creation, ensuring the subprocess can access certificate files. Co-authored-by: f0ssel <19379394+f0ssel@users.noreply.github.com> --- tls/tls.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tls/tls.go b/tls/tls.go index 8f66040..07ef6ad 100644 --- a/tls/tls.go +++ b/tls/tls.go @@ -14,6 +14,7 @@ import ( "os" "os/user" "path/filepath" + "strconv" "sync" "time" ) @@ -142,6 +143,22 @@ func (cm *CertificateManager) generateCA(keyPath, certPath string) error { return fmt.Errorf("failed to create config directory: %v", err) } + // When running under sudo, ensure the directory is owned by the original user + if sudoUser := os.Getenv("SUDO_USER"); sudoUser != "" { + if sudoUID := os.Getenv("SUDO_UID"); sudoUID != "" { + if sudoGID := os.Getenv("SUDO_GID"); sudoGID != "" { + uid, err1 := strconv.Atoi(sudoUID) + gid, err2 := strconv.Atoi(sudoGID) + if err1 == nil && err2 == nil { + // Change ownership of the config directory to the original user + if err := os.Chown(cm.configDir, uid, gid); err != nil { + cm.logger.Warn("Failed to change config directory ownership", "error", err) + } + } + } + } + } + // Generate private key privateKey, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { From 463dbe8c16f0de20f8368e5d4bbd1c2995d8126e Mon Sep 17 00:00:00 2001 From: "blink-so[bot]" <211532188+blink-so[bot]@users.noreply.github.com> Date: Tue, 9 Sep 2025 20:07:19 +0000 Subject: [PATCH 4/7] fix: restore essential user environment variables (HOME, USER) When running under sudo, tools like claude couldn't find their config files because they were looking in root's HOME directory instead of the original user's home. This adds minimal environment restoration: - HOME: Set to original user's home directory - USER: Set to original username This allows user tools to find their configuration files while keeping the implementation clean and focused. Co-authored-by: f0ssel <19379394+f0ssel@users.noreply.github.com> --- network/linux.go | 12 ++++++++++++ network/macos.go | 12 ++++++++++++ 2 files changed, 24 insertions(+) diff --git a/network/linux.go b/network/linux.go index 9dee54d..3100df4 100644 --- a/network/linux.go +++ b/network/linux.go @@ -7,6 +7,7 @@ import ( "log/slog" "os" "os/exec" + "os/user" "strconv" "syscall" "time" @@ -98,6 +99,17 @@ func (l *LinuxJail) Execute(command []string, extraEnv map[string]string) error env = append(env, fmt.Sprintf("%s=%s", key, value)) } + // When running under sudo, restore essential user environment variables + if sudoUser := os.Getenv("SUDO_USER"); sudoUser != "" { + if user, err := user.Lookup(sudoUser); err == nil { + // Set HOME to original user's home directory + env = append(env, fmt.Sprintf("HOME=%s", user.HomeDir)) + // Set USER to original username + env = append(env, fmt.Sprintf("USER=%s", sudoUser)) + l.logger.Debug("Restored user environment", "home", user.HomeDir, "user", sudoUser) + } + } + cmd.Env = env cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout diff --git a/network/macos.go b/network/macos.go index 3631a80..591493a 100644 --- a/network/macos.go +++ b/network/macos.go @@ -7,6 +7,7 @@ import ( "log/slog" "os" "os/exec" + "os/user" "strconv" "strings" "syscall" @@ -84,6 +85,17 @@ func (m *MacOSNetJail) Execute(command []string, extraEnv map[string]string) err env = append(env, fmt.Sprintf("%s=%s", key, value)) } + // When running under sudo, restore essential user environment variables + if sudoUser := os.Getenv("SUDO_USER"); sudoUser != "" { + if user, err := user.Lookup(sudoUser); err == nil { + // Set HOME to original user's home directory + env = append(env, fmt.Sprintf("HOME=%s", user.HomeDir)) + // Set USER to original username + env = append(env, fmt.Sprintf("USER=%s", sudoUser)) + m.logger.Debug("Restored user environment", "home", user.HomeDir, "user", sudoUser) + } + } + cmd.Env = env cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr From 42dd8bfdb4724730a4ebcdac8fd609552340cafd Mon Sep 17 00:00:00 2001 From: "blink-so[bot]" <211532188+blink-so[bot]@users.noreply.github.com> Date: Tue, 9 Sep 2025 20:13:14 +0000 Subject: [PATCH 5/7] fix: add LOGNAME to environment restoration Some tools check LOGNAME instead of USER to determine the current user. Adding LOGNAME=original_user to ensure consistent user identity across all environment variables. Co-authored-by: f0ssel <19379394+f0ssel@users.noreply.github.com> --- network/linux.go | 2 ++ network/macos.go | 2 ++ 2 files changed, 4 insertions(+) diff --git a/network/linux.go b/network/linux.go index 3100df4..f0d4330 100644 --- a/network/linux.go +++ b/network/linux.go @@ -106,6 +106,8 @@ func (l *LinuxJail) Execute(command []string, extraEnv map[string]string) error env = append(env, fmt.Sprintf("HOME=%s", user.HomeDir)) // Set USER to original username env = append(env, fmt.Sprintf("USER=%s", sudoUser)) + // Set LOGNAME to original username (some tools check this instead of USER) + env = append(env, fmt.Sprintf("LOGNAME=%s", sudoUser)) l.logger.Debug("Restored user environment", "home", user.HomeDir, "user", sudoUser) } } diff --git a/network/macos.go b/network/macos.go index 591493a..2c90ee8 100644 --- a/network/macos.go +++ b/network/macos.go @@ -92,6 +92,8 @@ func (m *MacOSNetJail) Execute(command []string, extraEnv map[string]string) err env = append(env, fmt.Sprintf("HOME=%s", user.HomeDir)) // Set USER to original username env = append(env, fmt.Sprintf("USER=%s", sudoUser)) + // Set LOGNAME to original username (some tools check this instead of USER) + env = append(env, fmt.Sprintf("LOGNAME=%s", sudoUser)) m.logger.Debug("Restored user environment", "home", user.HomeDir, "user", sudoUser) } } From 0283fdc971ef1ede85566a45e34afb87aee89e87 Mon Sep 17 00:00:00 2001 From: "blink-so[bot]" <211532188+blink-so[bot]@users.noreply.github.com> Date: Tue, 9 Sep 2025 20:40:21 +0000 Subject: [PATCH 6/7] fix: preserve jail group when dropping privileges on macOS Previously, we were changing both UID and GID to the original user, which broke network jailing because the subprocess was no longer in the jail group that gets redirected through the proxy. Now we: - Change UID to original user (for correct user identity) - Keep GID as jail group (for network isolation) This maintains both user identity and network jailing functionality. Co-authored-by: f0ssel <19379394+f0ssel@users.noreply.github.com> --- network/macos.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/network/macos.go b/network/macos.go index 2c90ee8..382dc53 100644 --- a/network/macos.go +++ b/network/macos.go @@ -114,13 +114,14 @@ func (m *MacOSNetJail) Execute(command []string, extraEnv map[string]string) err if err != nil { m.logger.Warn("Invalid SUDO_GID, subprocess will run as root", "sudo_gid", sudoGID, "error", err) } else { + // Use original user ID but KEEP the jail group for network isolation cmd.SysProcAttr = &syscall.SysProcAttr{ Credential: &syscall.Credential{ Uid: uint32(uid), - Gid: uint32(gid), + Gid: uint32(m.groupID), // Keep jail group, not original user's group }, } - m.logger.Debug("Dropping privileges to original user", "uid", uid, "gid", gid) + m.logger.Debug("Dropping privileges to original user with jail group", "uid", uid, "jail_gid", m.groupID) } } } From d62fac5b56a712a313236b56c2f664bbfdfad6c8 Mon Sep 17 00:00:00 2001 From: "blink-so[bot]" <211532188+blink-so[bot]@users.noreply.github.com> Date: Tue, 9 Sep 2025 20:41:15 +0000 Subject: [PATCH 7/7] fix: remove unused gid variable in macOS implementation Since we're keeping the jail group instead of using the original user's group, we don't need to parse SUDO_GID anymore. Co-authored-by: f0ssel <19379394+f0ssel@users.noreply.github.com> --- network/macos.go | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/network/macos.go b/network/macos.go index 382dc53..d383e8e 100644 --- a/network/macos.go +++ b/network/macos.go @@ -110,19 +110,14 @@ func (m *MacOSNetJail) Execute(command []string, extraEnv map[string]string) err if err != nil { m.logger.Warn("Invalid SUDO_UID, subprocess will run as root", "sudo_uid", sudoUID, "error", err) } else { - gid, err := strconv.Atoi(sudoGID) - if err != nil { - m.logger.Warn("Invalid SUDO_GID, subprocess will run as root", "sudo_gid", sudoGID, "error", err) - } else { - // Use original user ID but KEEP the jail group for network isolation - cmd.SysProcAttr = &syscall.SysProcAttr{ - Credential: &syscall.Credential{ - Uid: uint32(uid), - Gid: uint32(m.groupID), // Keep jail group, not original user's group - }, - } - m.logger.Debug("Dropping privileges to original user with jail group", "uid", uid, "jail_gid", m.groupID) + // Use original user ID but KEEP the jail group for network isolation + cmd.SysProcAttr = &syscall.SysProcAttr{ + Credential: &syscall.Credential{ + Uid: uint32(uid), + Gid: uint32(m.groupID), // Keep jail group, not original user's group + }, } + m.logger.Debug("Dropping privileges to original user with jail group", "uid", uid, "jail_gid", m.groupID) } } } else {