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
96 changes: 91 additions & 5 deletions cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"log/slog"
"os"
"os/signal"
"os/user"
"strconv"
"strings"
"syscall"

Expand All @@ -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
Expand Down Expand Up @@ -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")
Expand All @@ -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)
}
Expand Down Expand Up @@ -172,4 +258,4 @@ func Run(ctx context.Context, config Config, args []string) error {
}

return nil
}
}
Binary file added jail
Binary file not shown.
21 changes: 16 additions & 5 deletions jail.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
}
}
}
32 changes: 12 additions & 20 deletions namespace/linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"os"
"os/exec"
"os/user"
"strconv"
"strings"
"syscall"
"time"
Expand All @@ -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 {
Expand All @@ -39,6 +39,7 @@ func NewLinux(config Config) (*Linux, error) {
preparedEnv: preparedEnv,
httpProxyPort: config.HttpProxyPort,
httpsProxyPort: config.HttpsProxyPort,
envConfig: envConfig,
}, nil
}

Expand Down Expand Up @@ -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{
Expand Down
2 changes: 1 addition & 1 deletion namespace/linux_stub.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
44 changes: 18 additions & 26 deletions namespace/macos.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -49,6 +50,7 @@ func NewMacOS(config Config) (*MacOSNetJail, error) {
preparedEnv: preparedEnv,
httpProxyPort: config.HttpProxyPort,
httpsProxyPort: config.HttpsProxyPort,
envConfig: envConfig,
}, nil
}

Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -370,4 +362,4 @@ func (m *MacOSNetJail) cleanupTempFiles() {
if m.mainRulesPath != "" {
os.Remove(m.mainRulesPath)
}
}
}
2 changes: 1 addition & 1 deletion namespace/macos_stub.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
9 changes: 8 additions & 1 deletion namespace/namespace.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Loading
Loading