diff --git a/cli/cli.go b/cli/cli.go index 0554f6d..5b8be40 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -6,6 +6,8 @@ import ( "log/slog" "os" "os/signal" + "os/user" + "strconv" "strings" "syscall" @@ -16,6 +18,87 @@ import ( "github.com/coder/serpent" ) +// SudoData contains processed sudo environment information +type SudoData struct { + // Processed values + IsUnderSudo bool + UserInfo *user.User // Original user info when under sudo + UID int // Parsed UID, 0 if not available + GID int // Parsed GID, 0 if not available + XDGConfigHome string // XDG config home directory +} + +// ToTLSEnvConfig converts SudoData to tls.EnvConfig +func (s SudoData) ToTLSEnvConfig() tls.EnvConfig { + sudoUser := "" + if s.UserInfo != nil { + sudoUser = s.UserInfo.Username + } + return tls.EnvConfig{ + SudoUser: sudoUser, + SudoUID: s.UID, + SudoGID: s.GID, + XDGConfigHome: s.XDGConfigHome, + } +} + +// ToJailEnvConfig converts SudoData to jail.EnvConfig +func (s SudoData) ToJailEnvConfig() jail.EnvConfig { + sudoUser := "" + if s.UserInfo != nil { + sudoUser = s.UserInfo.Username + } + return jail.EnvConfig{ + SudoUser: sudoUser, + SudoUID: s.UID, + SudoGID: s.GID, + } +} + +// readSudoData reads and processes sudo-related environment variables +func readSudoData(logger *slog.Logger) SudoData { + // Read raw environment values + sudoUser := os.Getenv("SUDO_USER") + sudoUID := os.Getenv("SUDO_UID") + sudoGID := os.Getenv("SUDO_GID") + xdgConfigHome := os.Getenv("XDG_CONFIG_HOME") + + data := SudoData{ + IsUnderSudo: sudoUser != "", + XDGConfigHome: xdgConfigHome, + } + + // Process user information if under sudo + if data.IsUnderSudo { + if userInfo, err := user.Lookup(sudoUser); err == nil { + data.UserInfo = userInfo + logger.Debug("Found original user info", "user", sudoUser, "home", userInfo.HomeDir) + } else { + logger.Warn("Failed to lookup original user", "user", sudoUser, "error", err) + } + + // Parse UID + if sudoUID != "" { + if uid, err := strconv.Atoi(sudoUID); err == nil { + data.UID = uid + } else { + logger.Warn("Invalid SUDO_UID, using 0", "sudo_uid", sudoUID, "error", err) + } + } + + // Parse GID + if sudoGID != "" { + if gid, err := strconv.Atoi(sudoGID); err == nil { + data.GID = gid + } else { + logger.Warn("Invalid SUDO_GID, using 0", "sudo_gid", sudoGID, "error", err) + } + } + } + + return data +} + // Config holds all configuration for the CLI type Config struct { AllowStrings []string @@ -94,6 +177,9 @@ func Run(ctx context.Context, config Config, args []string) error { defer cancel() logger := setupLogging(config.LogLevel) + // Read and process sudo environment data once + sudoData := readSudoData(logger) + // Get command arguments if len(args) == 0 { return fmt.Errorf("no command specified") @@ -117,20 +203,20 @@ func Run(ctx context.Context, config Config, args []string) error { // Create auditor auditor := audit.NewLoggingAuditor(logger) - // Create certificate manager - certManager, err := tls.NewCertificateManager(logger) + // Create certificate manager with environment variables + certManager, err := tls.NewCertificateManager(logger, sudoData.ToTLSEnvConfig()) if err != nil { logger.Error("Failed to create certificate manager", "error", err) return fmt.Errorf("failed to create certificate manager: %v", err) } - // Create jail instance + // Create jail instance with environment variables jailInstance, err := jail.New(ctx, jail.Config{ RuleEngine: ruleEngine, Auditor: auditor, CertManager: certManager, Logger: logger, - }) + }, sudoData.ToJailEnvConfig()) if err != nil { return fmt.Errorf("failed to create jail instance: %v", err) } @@ -172,4 +258,4 @@ func Run(ctx context.Context, config Config, args []string) error { } return nil -} +} \ No newline at end of file diff --git a/jail b/jail new file mode 100755 index 0000000..aa8512d Binary files /dev/null and b/jail differ diff --git a/jail.go b/jail.go index 0b2a4b5..7763cbe 100644 --- a/jail.go +++ b/jail.go @@ -22,6 +22,13 @@ type Config struct { Logger *slog.Logger } +// EnvConfig holds environment variable values for jail components +type EnvConfig struct { + SudoUser string + SudoUID int + SudoGID int +} + type Jail struct { commander namespace.Commander proxyServer *proxy.Server @@ -30,7 +37,7 @@ type Jail struct { cancel context.CancelFunc } -func New(ctx context.Context, config Config) (*Jail, error) { +func New(ctx context.Context, config Config, envConfig EnvConfig) (*Jail, error) { // Setup TLS config and write CA certificate to file tlsConfig, caCertPath, configDir, err := config.CertManager.SetupTLSAndWriteCACert() if err != nil { @@ -62,6 +69,10 @@ func New(ctx context.Context, config Config) (*Jail, error) { "REQUESTS_CA_BUNDLE": caCertPath, // Python requests "NODE_EXTRA_CA_CERTS": caCertPath, // Node.js }, + }, namespace.EnvConfig{ + SudoUser: envConfig.SudoUser, + SudoUID: envConfig.SudoUID, + SudoGID: envConfig.SudoGID, }) if err != nil { return nil, fmt.Errorf("failed to create commander: %v", err) @@ -118,13 +129,13 @@ func (j *Jail) Close() error { } // newNamespaceCommander creates a new namespace instance for the current platform -func newNamespaceCommander(config namespace.Config) (namespace.Commander, error) { +func newNamespaceCommander(config namespace.Config, envConfig namespace.EnvConfig) (namespace.Commander, error) { switch runtime.GOOS { case "darwin": - return namespace.NewMacOS(config) + return namespace.NewMacOS(config, envConfig) case "linux": - return namespace.NewLinux(config) + return namespace.NewLinux(config, envConfig) default: return nil, fmt.Errorf("unsupported platform: %s", runtime.GOOS) } -} +} \ No newline at end of file diff --git a/namespace/linux.go b/namespace/linux.go index accc168..dd32824 100644 --- a/namespace/linux.go +++ b/namespace/linux.go @@ -8,7 +8,6 @@ import ( "os" "os/exec" "os/user" - "strconv" "strings" "syscall" "time" @@ -23,10 +22,11 @@ type Linux struct { procAttr *syscall.SysProcAttr httpProxyPort int httpsProxyPort int + envConfig EnvConfig } // NewLinux creates a new Linux network jail instance -func NewLinux(config Config) (*Linux, error) { +func NewLinux(config Config, envConfig EnvConfig) (*Linux, error) { // Initialize preparedEnv with config environment variables preparedEnv := make(map[string]string) for key, value := range config.Env { @@ -39,6 +39,7 @@ func NewLinux(config Config) (*Linux, error) { preparedEnv: preparedEnv, httpProxyPort: config.HttpProxyPort, httpsProxyPort: config.HttpsProxyPort, + envConfig: envConfig, }, nil } @@ -85,36 +86,27 @@ func (l *Linux) Start() error { } // When running under sudo, restore essential user environment variables - sudoUser := os.Getenv("SUDO_USER") - if sudoUser != "" { - user, err := user.Lookup(sudoUser) + if l.envConfig.SudoUser != "" { + user, err := user.Lookup(l.envConfig.SudoUser) if err == nil { // Set HOME to original user's home directory l.preparedEnv["HOME"] = user.HomeDir // Set USER to original username - l.preparedEnv["USER"] = sudoUser + l.preparedEnv["USER"] = l.envConfig.SudoUser // Set LOGNAME to original username (some tools check this instead of USER) - l.preparedEnv["LOGNAME"] = sudoUser - l.logger.Debug("Restored user environment", "home", user.HomeDir, "user", sudoUser) + l.preparedEnv["LOGNAME"] = l.envConfig.SudoUser + l.logger.Debug("Restored user environment", "home", user.HomeDir, "user", l.envConfig.SudoUser) } } // Prepare process credentials once during setup l.logger.Debug("Preparing process credentials") var gid, uid int - sudoUID := os.Getenv("SUDO_UID") - if sudoUID != "" { - uid, err = strconv.Atoi(sudoUID) - if err != nil { - l.logger.Warn("Invalid SUDO_UID, subprocess will run as root", "sudo_uid", sudoUID, "error", err) - } + if l.envConfig.SudoUID != 0 { + uid = l.envConfig.SudoUID } - sudoGID := os.Getenv("SUDO_GID") - if sudoGID != "" { - gid, err = strconv.Atoi(sudoGID) - if err != nil { - l.logger.Warn("Invalid SUDO_GID, subprocess will run as root", "sudo_gid", sudoGID, "error", err) - } + if l.envConfig.SudoGID != 0 { + gid = l.envConfig.SudoGID } l.procAttr = &syscall.SysProcAttr{ Credential: &syscall.Credential{ diff --git a/namespace/linux_stub.go b/namespace/linux_stub.go index 29a304b..9f24f15 100644 --- a/namespace/linux_stub.go +++ b/namespace/linux_stub.go @@ -7,6 +7,6 @@ import ( ) // NewLinux is not available on non-Linux platforms -func NewLinux(_ Config) (*noop, error) { +func NewLinux(_ Config, _ EnvConfig) (*noop, error) { return nil, fmt.Errorf("linux network jail not supported on this platform") } diff --git a/namespace/macos.go b/namespace/macos.go index a9db968..f6bb19e 100644 --- a/namespace/macos.go +++ b/namespace/macos.go @@ -28,10 +28,11 @@ type MacOSNetJail struct { procAttr *syscall.SysProcAttr httpProxyPort int httpsProxyPort int + envConfig EnvConfig } // NewMacOS creates a new macOS network jail instance -func NewMacOS(config Config) (*MacOSNetJail, error) { +func NewMacOS(config Config, envConfig EnvConfig) (*MacOSNetJail, error) { ns := newNamespaceName() pfRulesPath := fmt.Sprintf("/tmp/%s.pf", ns) mainRulesPath := fmt.Sprintf("/tmp/%s_main.pf", ns) @@ -49,6 +50,7 @@ func NewMacOS(config Config) (*MacOSNetJail, error) { preparedEnv: preparedEnv, httpProxyPort: config.HttpProxyPort, httpsProxyPort: config.HttpsProxyPort, + envConfig: envConfig, }, nil } @@ -84,47 +86,37 @@ func (m *MacOSNetJail) Start() error { } // When running under sudo, restore essential user environment variables - sudoUser := os.Getenv("SUDO_USER") - if sudoUser != "" { - user, err := user.Lookup(sudoUser) + if m.envConfig.SudoUser != "" { + user, err := user.Lookup(m.envConfig.SudoUser) if err == nil { // Set HOME to original user's home directory m.preparedEnv["HOME"] = user.HomeDir // Set USER to original username - m.preparedEnv["USER"] = sudoUser + m.preparedEnv["USER"] = m.envConfig.SudoUser // Set LOGNAME to original username (some tools check this instead of USER) - m.preparedEnv["LOGNAME"] = sudoUser - m.logger.Debug("Restored user environment", "home", user.HomeDir, "user", sudoUser) + m.preparedEnv["LOGNAME"] = m.envConfig.SudoUser + m.logger.Debug("Restored user environment", "home", user.HomeDir, "user", m.envConfig.SudoUser) } } - // Prepare process credentials once during setup - m.logger.Debug("Preparing process credentials") + // Set default process attributes (jail group) procAttr := &syscall.SysProcAttr{ Credential: &syscall.Credential{ + Uid: uint32(os.Getuid()), Gid: uint32(m.groupID), }, } // Drop privileges to original user if running under sudo - sudoUID := os.Getenv("SUDO_UID") - if sudoUID != "" { - 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 { - // Use original user ID but KEEP the jail group for network isolation - procAttr = &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) + if m.envConfig.SudoUID != 0 { + // Use original user ID but KEEP the jail group for network isolation + procAttr = &syscall.SysProcAttr{ + Credential: &syscall.Credential{ + Uid: uint32(m.envConfig.SudoUID), + Gid: uint32(m.groupID), // Keep jail group, not original user's group + }, } } - - // Store prepared process attributes for use in Command method m.procAttr = procAttr m.logger.Debug("Setup completed successfully") @@ -370,4 +362,4 @@ func (m *MacOSNetJail) cleanupTempFiles() { if m.mainRulesPath != "" { os.Remove(m.mainRulesPath) } -} +} \ No newline at end of file diff --git a/namespace/macos_stub.go b/namespace/macos_stub.go index 224a9f8..d071ead 100644 --- a/namespace/macos_stub.go +++ b/namespace/macos_stub.go @@ -3,6 +3,6 @@ package namespace // NewMacOS is not available on non-macOS platforms -func NewMacOS(_ Config) (*noop, error) { +func NewMacOS(_ Config, _ EnvConfig) (*noop, error) { panic("macOS network jail not available on this platform") } diff --git a/namespace/namespace.go b/namespace/namespace.go index 6436782..5b61368 100644 --- a/namespace/namespace.go +++ b/namespace/namespace.go @@ -25,6 +25,13 @@ type Config struct { Env map[string]string } +// EnvConfig holds environment variable values for namespace implementations +type EnvConfig struct { + SudoUser string + SudoUID int + SudoGID int +} + func newNamespaceName() string { return fmt.Sprintf("%s_%d", namespacePrefix, time.Now().UnixNano()%10000000) -} +} \ No newline at end of file diff --git a/tls/tls.go b/tls/tls.go index 75f34aa..a8dad77 100644 --- a/tls/tls.go +++ b/tls/tls.go @@ -14,7 +14,6 @@ import ( "os" "os/user" "path/filepath" - "strconv" "sync" "time" ) @@ -23,6 +22,14 @@ type Manager interface { SetupTLSAndWriteCACert() (*tls.Config, string, string, error) } +// EnvConfig holds environment variable values +type EnvConfig struct { + SudoUser string + SudoUID int + SudoGID int + XDGConfigHome string +} + // CertificateManager manages TLS certificates for the proxy type CertificateManager struct { caKey *rsa.PrivateKey @@ -31,11 +38,12 @@ type CertificateManager struct { mutex sync.RWMutex logger *slog.Logger configDir string + envConfig EnvConfig } -// NewCertificateManager creates a new certificate manager -func NewCertificateManager(logger *slog.Logger) (*CertificateManager, error) { - configDir, err := getConfigDir() +// NewCertificateManager creates a new certificate manager with environment configuration +func NewCertificateManager(logger *slog.Logger, envConfig EnvConfig) (*CertificateManager, error) { + configDir, err := getConfigDir(envConfig) if err != nil { return nil, fmt.Errorf("failed to determine config directory: %v", err) } @@ -44,6 +52,7 @@ func NewCertificateManager(logger *slog.Logger) (*CertificateManager, error) { certCache: make(map[string]*tls.Certificate), logger: logger, configDir: configDir, + envConfig: envConfig, } // Load or generate CA certificate @@ -59,7 +68,7 @@ func NewCertificateManager(logger *slog.Logger) (*CertificateManager, error) { // Returns the TLS config, CA cert path, and config directory func (cm *CertificateManager) SetupTLSAndWriteCACert() (*tls.Config, string, string, error) { // Get config directory - configDir, err := getConfigDir() + configDir, err := getConfigDir(cm.envConfig) if err != nil { return nil, "", "", fmt.Errorf("failed to get config directory: %v", err) } @@ -183,19 +192,11 @@ func (cm *CertificateManager) generateCA(keyPath, certPath string) error { } // 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 - err := os.Chown(cm.configDir, uid, gid) - if err != nil { - cm.logger.Warn("Failed to change config directory ownership", "error", err) - } - } - } + if cm.envConfig.SudoUser != "" && cm.envConfig.SudoUID != 0 && cm.envConfig.SudoGID != 0 { + // Change ownership of the config directory to the original user + err := os.Chown(cm.configDir, cm.envConfig.SudoUID, cm.envConfig.SudoGID) + if err != nil { + cm.logger.Warn("Failed to change config directory ownership", "error", err) } } @@ -352,13 +353,13 @@ func (cm *CertificateManager) generateServerCertificate(hostname string) (*tls.C } // getConfigDir returns the configuration directory path -func getConfigDir() (string, error) { +func getConfigDir(envConfig EnvConfig) (string, error) { // 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 != "" { + if envConfig.SudoUser != "" { // Get original user's home directory - if user, err := user.Lookup(sudoUser); err == nil { + if user, err := user.Lookup(envConfig.SudoUser); err == nil { homeDir = user.HomeDir } else { // Fallback to current user if lookup fails @@ -380,11 +381,11 @@ func getConfigDir() (string, error) { // Use platform-specific config directory var configDir string switch { - case os.Getenv("XDG_CONFIG_HOME") != "": - configDir = filepath.Join(os.Getenv("XDG_CONFIG_HOME"), "coder_jail") + case envConfig.XDGConfigHome != "": + configDir = filepath.Join(envConfig.XDGConfigHome, "coder_jail") default: configDir = filepath.Join(homeDir, ".config", "coder_jail") } return configDir, nil -} +} \ No newline at end of file