Skip to content
Merged
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
132 changes: 107 additions & 25 deletions cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,15 @@ import (
"log/slog"
"os"
"os/signal"
"os/user"
"path/filepath"
"strconv"
"strings"
"syscall"

"github.com/coder/jail"
"github.com/coder/jail/audit"
"github.com/coder/jail/namespace"
"github.com/coder/jail/rules"
"github.com/coder/jail/tls"
"github.com/coder/serpent"
Expand Down Expand Up @@ -64,35 +68,12 @@ Examples:
}
}

// setupLogging creates a slog logger with the specified level
func setupLogging(logLevel string) *slog.Logger {
var level slog.Level
switch strings.ToLower(logLevel) {
case "error":
level = slog.LevelError
case "warn":
level = slog.LevelWarn
case "info":
level = slog.LevelInfo
case "debug":
level = slog.LevelDebug
default:
level = slog.LevelWarn // Default to warn if invalid level
}

// Create a standard slog logger with the appropriate level
handler := slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
Level: level,
})

return slog.New(handler)
}

// Run executes the jail command with the given configuration and arguments
func Run(ctx context.Context, config Config, args []string) error {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
logger := setupLogging(config.LogLevel)
userInfo := getUserInfo()

// Get command arguments
if len(args) == 0 {
Expand All @@ -118,7 +99,10 @@ func Run(ctx context.Context, config Config, args []string) error {
auditor := audit.NewLoggingAuditor(logger)

// Create certificate manager
certManager, err := tls.NewCertificateManager(logger)
certManager, err := tls.NewCertificateManager(tls.Config{
Logger: logger,
ConfigDir: userInfo.ConfigDir,
})
if err != nil {
logger.Error("Failed to create certificate manager", "error", err)
return fmt.Errorf("failed to create certificate manager: %v", err)
Expand Down Expand Up @@ -173,3 +157,101 @@ func Run(ctx context.Context, config Config, args []string) error {

return nil
}

func getUserInfo() namespace.UserInfo {
// get the user info of the original user even if we are running under sudo
sudoUser := os.Getenv("SUDO_USER")

// If running under sudo, get original user information
if sudoUser != "" {
user, err := user.Lookup(sudoUser)
if err != nil {
// Fallback to current user if lookup fails
return getCurrentUserInfo()
}

// Parse SUDO_UID and SUDO_GID
uid := 0
gid := 0

if sudoUID := os.Getenv("SUDO_UID"); sudoUID != "" {
if parsedUID, err := strconv.Atoi(sudoUID); err == nil {
uid = parsedUID
}
}

if sudoGID := os.Getenv("SUDO_GID"); sudoGID != "" {
if parsedGID, err := strconv.Atoi(sudoGID); err == nil {
gid = parsedGID
}
}

configDir := getConfigDir(user.HomeDir)

return namespace.UserInfo{
Username: sudoUser,
Uid: uid,
Gid: gid,
HomeDir: user.HomeDir,
ConfigDir: configDir,
}
}

// Not running under sudo, use current user
return getCurrentUserInfo()
}

// setupLogging creates a slog logger with the specified level
func setupLogging(logLevel string) *slog.Logger {
var level slog.Level
switch strings.ToLower(logLevel) {
case "error":
level = slog.LevelError
case "warn":
level = slog.LevelWarn
case "info":
level = slog.LevelInfo
case "debug":
level = slog.LevelDebug
default:
level = slog.LevelWarn // Default to warn if invalid level
}

// Create a standard slog logger with the appropriate level
handler := slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
Level: level,
})

return slog.New(handler)
}

// getCurrentUserInfo gets information for the current user
func getCurrentUserInfo() namespace.UserInfo {
currentUser, err := user.Current()
if err != nil {
// Fallback with empty values if we can't get user info
return namespace.UserInfo{}
}

uid, _ := strconv.Atoi(currentUser.Uid)
gid, _ := strconv.Atoi(currentUser.Gid)

configDir := getConfigDir(currentUser.HomeDir)

return namespace.UserInfo{
Username: currentUser.Username,
Uid: uid,
Gid: gid,
HomeDir: currentUser.HomeDir,
ConfigDir: configDir,
}
}

// getConfigDir determines the config directory based on XDG_CONFIG_HOME or fallback
func getConfigDir(homeDir string) string {
// Use XDG_CONFIG_HOME if set, otherwise fallback to ~/.config/coder_jail
if xdgConfigHome := os.Getenv("XDG_CONFIG_HOME"); xdgConfigHome != "" {
return filepath.Join(xdgConfigHome, "coder_jail")
}
return filepath.Join(homeDir, ".config", "coder_jail")
}
Binary file added jail
Binary file not shown.
49 changes: 13 additions & 36 deletions namespace/linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ import (
"log/slog"
"os"
"os/exec"
"os/user"
"strconv"
"strings"
"syscall"
"time"
Expand All @@ -23,6 +21,10 @@ type Linux struct {
procAttr *syscall.SysProcAttr
httpProxyPort int
httpsProxyPort int
user string
homeDir string
uid int
gid int
}

// NewLinux creates a new Linux network jail instance
Expand Down Expand Up @@ -84,42 +86,17 @@ 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 err == nil {
// Set HOME to original user's home directory
l.preparedEnv["HOME"] = user.HomeDir
// Set USER to original username
l.preparedEnv["USER"] = 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)
}
}
// Set HOME to original user's home directory
l.preparedEnv["HOME"] = l.homeDir
// Set USER to original username
l.preparedEnv["USER"] = l.user
// Set LOGNAME to original username (some tools check this instead of USER)
l.preparedEnv["LOGNAME"] = l.user

// 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)
}
}
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)
}
}
l.procAttr = &syscall.SysProcAttr{
Credential: &syscall.Credential{
Uid: uint32(uid),
Gid: uint32(gid),
Uid: uint32(l.uid),
Gid: uint32(l.gid),
},
}

Expand Down Expand Up @@ -303,4 +280,4 @@ func (l *Linux) removeNamespace() error {
return fmt.Errorf("failed to remove namespace: %v", err)
}
return nil
}
}
61 changes: 19 additions & 42 deletions namespace/macos.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (
"log/slog"
"os"
"os/exec"
"os/user"
"strconv"
"strings"
"syscall"
Expand All @@ -20,14 +19,15 @@ const (

// MacOSNetJail implements network jail using macOS PF (Packet Filter) and group-based isolation
type MacOSNetJail struct {
groupID int
restrictedGid int
pfRulesPath string
mainRulesPath string
logger *slog.Logger
preparedEnv map[string]string
procAttr *syscall.SysProcAttr
httpProxyPort int
httpsProxyPort int
userInfo UserInfo
}

// NewMacOS creates a new macOS network jail instance
Expand All @@ -49,6 +49,7 @@ func NewMacOS(config Config) (*MacOSNetJail, error) {
preparedEnv: preparedEnv,
httpProxyPort: config.HttpProxyPort,
httpsProxyPort: config.HttpsProxyPort,
userInfo: config.UserInfo,
}, nil
}

Expand Down Expand Up @@ -83,47 +84,23 @@ 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 err == nil {
// Set HOME to original user's home directory
m.preparedEnv["HOME"] = user.HomeDir
// Set USER to original username
m.preparedEnv["USER"] = 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)
}
}
// Set HOME to original user's home directory
m.preparedEnv["HOME"] = m.userInfo.HomeDir
// Set USER to original username
m.preparedEnv["USER"] = m.userInfo.Username
// Set LOGNAME to original username (some tools check this instead of USER)
m.preparedEnv["LOGNAME"] = m.userInfo.Username

// Prepare process credentials once during setup
m.logger.Debug("Preparing process credentials")
// Use original user ID but KEEP the jail group for network isolation
procAttr := &syscall.SysProcAttr{
Credential: &syscall.Credential{
Gid: uint32(m.groupID),
Uid: uint32(m.userInfo.Uid),
Gid: uint32(m.restrictedGid),
},
}

// 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)
}
}

// Store prepared process attributes for use in Command method
m.procAttr = procAttr

Expand All @@ -136,7 +113,7 @@ func (m *MacOSNetJail) Command(command []string) *exec.Cmd {
m.logger.Debug("Command called", "command", command)

// Create command directly (no sg wrapper needed)
m.logger.Debug("Creating command with group membership", "groupID", m.groupID)
m.logger.Debug("Creating command with group membership", "groupID", m.restrictedGid)
cmd := exec.Command(command[0], command[1:]...)
m.logger.Debug("Full command args", "args", command)

Expand Down Expand Up @@ -190,7 +167,7 @@ func (m *MacOSNetJail) ensureGroup() error {
if err != nil {
return fmt.Errorf("failed to parse GID: %v", err)
}
m.groupID = gid
m.restrictedGid = gid
return nil
}
}
Expand Down Expand Up @@ -219,7 +196,7 @@ func (m *MacOSNetJail) ensureGroup() error {
if err != nil {
return fmt.Errorf("failed to parse GID: %v", err)
}
m.groupID = gid
m.restrictedGid = gid
return nil
}
}
Expand Down Expand Up @@ -276,15 +253,15 @@ pass out on %s route-to (lo0 127.0.0.1) inet proto tcp from any to any group %d
# Allow all loopback traffic
pass on lo0 all
`,
m.groupID,
m.restrictedGid,
iface,
m.httpsProxyPort, // Use HTTPS proxy port for all TCP traffic
m.groupID,
m.restrictedGid,
iface,
m.groupID,
m.restrictedGid,
)

m.logger.Debug("Comprehensive TCP jailing enabled for macOS", "group_id", m.groupID, "proxy_port", m.httpsProxyPort)
m.logger.Debug("Comprehensive TCP jailing enabled for macOS", "group_id", m.restrictedGid, "proxy_port", m.httpsProxyPort)
return rules, nil
}

Expand Down
14 changes: 14 additions & 0 deletions namespace/name.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package namespace

import (
"fmt"
"time"
)

const (
prefix = "coder_jail"
)

func newNamespaceName() string {
return fmt.Sprintf("%s_%d", prefix, time.Now().UnixNano()%10000000)
}
Loading
Loading